Android 开发艺术探索笔记之五 -- 理解 RemoteViews

学习内容:

  • RemoteViews 在通知栏和桌面小部件上的应用
  • RemoteViews 的内部机制
  • RemoteViews 的意义

RemoteView 的应用

实际开发中,RemoteViews 主要用在通知栏和桌面小部件的开发过程中。通知栏主要通过 NotificationManager 的 notify 方法实现,桌面小部件则是通过 AppWidgetProvider 来实现,其本质也是一个广播。

通知栏和桌面小部件更新界面时,RemoteView 无法像 View 一样在 Activity 中直接更新,因为界面运行在系统的 SystemServer 进程,需要跨进程更新。

下面简单介绍 RemoteView 的应用

  1. RemoteView 在通知栏上的应用(主要为 自定义布局

    (适配 Android 8.0)

    //创建NotificationManager实例
    NotificationManager mManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    
    //创建NotificationChannel实例
    //参数说明:
    //id:NotificationChannel的唯一标识
    //name:NotificationChannel的名称,在Settings可看到
    //importance:对channel设置重要性,更改见后续表格
    NotificationChannel mChannel = new NotificationChannel("id","name",NotificationManager.IMPORTANCE_DEFAULT);
    mManager.createNotificationChannel(mChannel);
    
    //创建PendingIntent
    Intent intent = new Intent(this,SecondActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT);
    
    //创建RemoteView
    RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.layout_notification);
    remoteViews.setTextViewText(R.id.msg,"xx");
    remoteViews.setImageViewResource(R.id.icon,R.drawable.icon);
    remoteViews.setOnclidePendingIntent(R.id.clickable,pendingIntent);
    
    //创建builder,并设置一系列属性
    Notification.Builder builder = new Notification.Builder(this,"id");
    builder.setSmallIcon(R.drawable.ic_launcher_background)
            .setContentTitle("title")
            .setContentText("text")
            //以上三个为必需的属性
            .setAutoCancel(true);
    
    //Android 7.0 之后需要通过Notification.Builder设置contentView
    builder.setCustomContentView(remoteViews).
        
    //创建通知
    Notification notification = builder.build();
    //推送通知
    mManager.notify(1,notification);
    

    ​ RemoteViews 和 View 不同,每个方法中几乎都要求传入一个 id 参数,比如 setTextViewText(int viewId, CharSequence text),需要传入TextView 的 id。

    直观原因 是因为 RemoteViews 没有提供和 View 类似的 findViewById 这个方法,因此我们无法获取到 RemoteView 中的子 View。(实际原因并非如此,后面详细介绍)

  2. RemoteViews 在桌面小部件上的应用

    利用 AppWidgetProvider,本质是广播。

    1. 定义小部件界面

      在 res/layout/ 新建一个 xml 文件,命名为 widget.xml,名称和内容可自定义,视小部件具体需求而定。

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:orientation="vertical">
      
          <ImageView
              android:id="@+id/imageView1"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:src="@drawable/icon" />
      </LinearLayout>
      
    2. 定义小部件配置信息

      在 res/xml/ 下新建 appwidget_provider_info.xml,名称任意。

      <?xml version="1.0" encoding="utf-8"?>
      <appwidget-provider    xmlns:android="http://schemas.android.com/apk/res/android"
       
              //使用的初始化布局
            android:initialLayout="@layout/widget"
            //小工具的最小尺寸
            android:minHeight="84dp"
            android:minWidth="84dp"
            //自动更新周期,毫秒单位
            android:updatePeriodMillis="864000"/>
      
      
    3. 定义小部件的实现类

      继承 AppWidgetProvider,功能为简单的 点击后随机切换图片。

      public class MyAppWidgetProvider extends AppWidgetProvider {
          public static final String TAG = "ImgAppWidgetProvider";
          public static final String CLICK_ACTION = "cn.hudp.androiddevartnote.action.click";
          private static int index;
      
          @Override
          public void onReceive(Context context, Intent intent) {
              super.onReceive(context, intent);
              if (intent.getAction().equals(CLICK_ACTION)) {
                  RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
                  AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
      
                  updateView(context, remoteViews, appWidgetManager);
              }
          }
      
          @Override
          public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
              super.onUpdate(context, appWidgetManager, appWidgetIds);
              RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
      
              updateView(context, remoteViews, appWidgetManager);
          }
      
          // 随机更新图片
          public void updateView(Context context, RemoteViews remoteViews, AppWidgetManager appWidgetManager) {
              index = (int) (Math.random() * 3);
              if (index == 1) {
                  remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei1);
              } else if (index == 2) {
                  remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei2);
              } else {
                  remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei3);
              }
              Intent clickIntent = new Intent();
              clickIntent.setAction(CLICK_ACTION);
              PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, clickIntent, 0);
              remoteViews.setOnClickPendingIntent(R.id.iv, pendingIntent);
              appWidgetManager.updateAppWidget(new ComponentName(context, MyAppWidgetProvider.class), remoteViews);
          }
      }
      
    4. 在 AndroidManifest.xml 中声明小部件

      原因:本质是广播组件,因此需要注册

      <receiver android:name=".MyAppWidgetProvider">
          <meta-data
              android:name="android.appwidget.provider"
              android:resource="@xml/appwidget_provider_info">
              </meta-data>
          <intent-filter>
            //识别小部件的单击行为
              <action android:name="com.whdalive.action.click" />
               //作为小部件的标识,必须存在
              <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
          </intent-filter>
      </receiver>
      
    5. 广播分发

      当广播到来之后,AppWidgetProvider 会自动根据广播的 Action 通过 onReceive 来自动分发广播,相关方法如下

      1. onEnable: 当该窗口小部件第一次添加到桌面时调用的方法,可添加多次但只在第一次调用。
      2. onUpdate: 小部件被添加时或者每次小部件更新时都会调用一次该方法,小部件的更新时机是有updatePeriodMillis来指定,每个周期小部件就会自动更新一次。
      3. onDeleted: 每删除一次桌面小部件就调用一次。
      4. onDisabled: 当最后一个该类型的小部件被删除时调用该方法。
      5. onRestored:当接收到 ACTION_APPWIDGET_RESTORED 广播,从备份恢复小部件时调用
      6. onAppWidgetOptionsChanged:当接收到 ACTION_APPWIDGET_OPTIONS_CHANGED 广播,小部件的尺寸位置发生变化时调用。
      7. onReceive: 这是广播的内置方法,用于分发具体事件给其他方法。
  3. PendingIntent 概述

    1. 基本介绍

      1. PendingIntent 表示一种处于 Pending(待定、等待、即将发生)状态的意图;
      2. 典型应用场景是给 RemoteViews 添加点击事件,(RemoteViews 运行在远程进程)
      3. 通过 send 和 cancel 方法来发送和取消特定的待定 Intent。
    2. 分类

      1. 启动 Activity -> getActivity(Context context, int requestCode, Intent intent, int flags)

      2. 启动 Service -> getService(Context context, int requestCode, Intent intent, int flags)

      3. 发送广播 -> getBroadcast(Context context, int requestCode, Intent intent, int flags)

     参数说明:

     1. requestCode 表示 PendingIntent 发送方的请求码,多数情况下设置 0 即可,另外 requestCode 会影响到 flags 的效果。
     2. flags 参数:
        1. **FLAG_ONE_SHOP** 当前的PendingIntent只能被使用一次,然后他就会自动cancel,如果后续还有相同的PendingIntent,那么它们的send方法就会调用失败。
        2. **FLAG_NO_CREATE** 当前描述的PendingIntent不会主动创建,如果当前PendingIntent之前存在,那么getActivity、getService和getBroadcast方法会直接返回Null,即获取PendingIntent失败,无法单独使用,平时很少用到。
        3. **FLAG_CANCEL_CURRENT** 当前描述的PendingIntent如果已经存在,那么它们都会被cancel,然后系统会创建一个新的PendingIntent。对于通知栏消息来说,那些被cancel的消息单击后无法打开。
        4. **FLAG_UPDATE_CURRENT** 当前描述的PendingIntent如果已经存在,那么它们都会被更新,即它们的Intent中的Extras会被替换为最新的。
  1. 匹配规则

    1. 如果两个 PendingIntent 内部的 Intent 相同且 requestCode 也相同,那么二者相同
    2. Intent 相同的匹配规则:Intent 的 ComponentName 和 intent-filter 都相同。Extras 不参与 Intent的匹配过程。

RemoteView 的内部机制

  1. 构造方法 public RemoteViews(String packageName, int layoutId)

    参数说明:

    1. packageName:当前应用的包名
    2. layoutId:待加载的布局文件
  2. 限制 -> 支持的 View 类型有限

    1. LayoutFrameLayoutLineanLayoutRelativeLayoutGridLayout
    2. ViewAnalogClockButtonChronometerImageButtonImageViewProgressBarTextViewViewFlipperListViewGridViewStackViewAdapterViewFlipperViewStub
  3. 特殊之处

    1. RemoteView 没有提供 findViewById 方法,因此无法直接访问里面的 View 元素,而必须通过 RemoteViews 所提供的一些列 set 方法来完成,这时因为 RemoteView 在远程进程中显示
    2. 一系列 set 方法 是通过反射来完成的。
  4. 工作流程

    1. 前置:通知栏和桌面小部件分别由 NotificationManager 和 AppWidgetManager 管理,而 NotificationManager 和 AppWidgetManager 通过 Binder 分别和 SystemServer 进程中的 NotificationManagerService(NMS) 以及 AppWidgetService(AWS) 进行通信。布局文件实际是在 NMS 和 AWS 中被加载的,而运行在 SystemServer 中,这就和我们的进程构成了 跨进程通信 的场景。

    2. 具体流程

      1. 首先 RemoteViews 通过 Binder 传递到 System Server 进程(RemoteViews 实现了 Parcelable 接口)。系统会根据 RemoteViews 中的包名等信息去得到该应用的资源。

      2. 然后通过 LayoutInflater 去加载 RemoteViews 中的布局文件。(对于 SystemServer 进程来讲,加载的只是一个普通的 view,只不过对于我们的进程来讲是 远程的)

      3. 接着系统对 View 执行一系列界面更新任务,这些任务通过 set 方法来提交。这些更新不是立刻执行,而是在 RemoteViews 中记录所有更新操作,等到 RemoteViews 被加载以后才能执行。

        到此时,RemoteViews 就可以在 SystemServer 进程中显示了。

      4. 当需要更新 RemoteViews 时,调用一些列 set 方法并通过 NotificationManager 和 AppWidgetManager 来提交更新任务,具体操作也是在 SystemServer 进程中完成。

    3. 进一步说明 -- 跨进程

      1. 系统不直接通过 Binder 支持所有的 View 和 View 操作,否则 View 的方法庞大,同时 IPC 操作会影响效率
      2. 系统提供了一个 Action 概念, Action 实现了 Parcelable 接口,代表一个 View 操作。
      3. 系统首先将 View 操作封装到 Action 对象并将这些对象跨进程传输到远程进程,接着在远程进程中执行 Action 对象中的具体操作。远程进程通过 RemoteViews 的 apply 方法来进行 View 的更新操作,Remoteview 的 apply 方法内部会遍历所有的 Action 对象并调用它们的 apply 方法,进而执行具体的 View 的更新操作。
      4. 此方法避免了 定义大量的 Binder 接口,其次通过远程进程中批量执行修改擦欧总避免了大量 IPC 操作。
    4. 源码说明:

      1. 见原书吧。。
    5. 补充说明

      1. apply 和 reApply 的区别:前者会加载布局并更新界面,后者只会更新界面
      2. 关于点击事件。RemoteViews 中只支持发起 PendingIntent 不支持 onClickListener 那种模式。另外, setOnClickPendingIntent 用于给普通 View 设置点击事件,不能给集合(ListView / StackvView)中的 View 设置点击事件。如果要给 ListView / StackvView 中的 itemview 设置单击事件,必须将 setPendingIntentTemplate 和 setOnClickFillInIntent 组合使用才可以。

RemoteViews 的意义

  1. 从字面上就能猜到:RemoteViews 目的就是为了方便的更新远程 views ,即跨进程更新 UI

    1. 当一个应用需要能够更新另一个应用中的某个界面,这时候如果通过 AIDL实现,那么可能会随着界面更新操作的复杂导致效率变低。这种场景就很适合使用 RemoteViews。

    2. RemoteViews 缺点在于 它只支持一些常见的 View,不支持自定义 View。

    3. 布局文件的加载问题

      1. 同一个应用的多进程情形

        View view = remoteViews.apply(this,mRemoteViewsContent);
        mRemoteViewsContent.addView(view);
        
      2. 不同应用时

        主要是由于两个应用的资源 ID 不一定一致,因此通过资源名称来加载布局文件

        int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName());
        view view = getLayoutInflater().inflate(layoutId,mRemoteViewsContent,flase);
        remoteViews.reapply(this,view);
        mRemoteViewsContent.addView(view);
        
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • 今天听曾仕强教授讲解《论语》中“知之为知之,不知为不知,是知也”时,讲了个故事,首先解释下“知之为知之,不知为不知...
    o鹿鸣阅读 286评论 1 2
  • 《吉檀迦利》在刚开始学的时候,懵懵懂懂,应付着学下去,不懂得它的真正含义,许多比喻都看不出来本体。因为在刚开...
    崔禹喆阅读 229评论 1 1
  • 踏足那会儿,正是隆冬 深夜的梦,全都是不安的躁动 寒风还肆虐的时候,你就坐上火车 赶着离开了,这么久 这么远的路,...
    洛城未晞阅读 194评论 0 1
  • 以前真的不能体会农民对土地的爱,因为田地给我的记忆不深,并随着年龄的成长离我越来越远。一片只是能种点东西的土,...
    54小愈阅读 187评论 0 0