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 参数:
        3. FLAG_ONE_SHOP 当前的PendingIntent只能被使用一次,然后他就会自动cancel,如果后续还有相同的PendingIntent,那么它们的send方法就会调用失败。
        4. FLAG_NO_CREATE 当前描述的PendingIntent不会主动创建,如果当前PendingIntent之前存在,那么getActivity、getService和getBroadcast方法会直接返回Null,即获取PendingIntent失败,无法单独使用,平时很少用到。
        5. FLAG_CANCEL_CURRENT 当前描述的PendingIntent如果已经存在,那么它们都会被cancel,然后系统会创建一个新的PendingIntent。对于通知栏消息来说,那些被cancel的消息单击后无法打开。
        6. FLAG_UPDATE_CURRENT 当前描述的PendingIntent如果已经存在,那么它们都会被更新,即它们的Intent中的Extras会被替换为最新的。
    3. 匹配规则

      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);

猜你喜欢

转载自blog.csdn.net/whdalive/article/details/81127653