- 1. RemoteViews 的应用
- 1.1 说一说对 RemoteViews 的理解
- 1.2 如何创建 RemoteViews?
- 1.3 如何更新 RemoteViews 的视图层级里的 View?
- 1.4 开发桌面小部件的步骤是什么?
- 1.5 AppWidgetProvider 的回调方法有哪些?作用分别是什么?
- 1.6 PendingIntent 和 Intent 的区别是什么?
- 1.7 PendingIntent 支持哪三种待定意图?
- 1.8 PendingIntent 的匹配规则是什么?
- 1.9 `FLAG_ONE_SHOT`、`FLAG_NO_CREATE`、`FLAG_CANCEL_CURRENT`、`FLAG_UPDATE_CURRENT` 这 4 个标记位的含义是什么?
- 1.10 对于通知栏消息来说,通知 id,PendingIntent 是否匹配,标记位 flags 参数如何影响通知栏消息?
- 2. RemoteViews 的内部机制
- 2.1 RemoteViews 是否支持所有的 View 类型?是否支持自定义 View?
- 2.2 说一下 RemoteViews 的内部机制
- 2.3 RemoteViews 里的 Action 的作用是什么?
- 2.4 RemoteViews 的 apply 方法和 reapply 方法的区别是什么?
- 2.5 RemoteViews 的 setOnClickPendingIntent、setPendingIntentTemplate、setonClickFillInIntent 的区别和联系是什么?
- 2.6 为什么对通知栏里的 RemoteViews 调用 setRemoteAdapter 方法没有效果?
- 参考
从名字来看,RemoteViews 是一种远程 View,也就是说包含远程和 View 两层意思在里面。具体来说,RemoteViews 表示的是一个 View 结构,它可以在其他进程中显示,由于它在其他进程中显示,RemoteViews 提供了一组基础的操作用于跨进程更新它的界面。
从类结构来看,RemoteViews 并不是 View 的子类,因为它没有继承 View 类,而是实现了 Parcelable 接口和 Filter 接口。
public class RemoteViews implements Parcelable, Filter
文档上的说法:它描述了一个可以展示在另外一个进程里的视图层级。视图层级由一个布局资源文件填充而来,RemoteViews 提供了一些基础的操作用于修改填充的视图层级的内容。
RemoteViews 在 Android 中的应用场景有两种:通知栏和桌面小部件。
1.2 如何创建 RemoteViews?只要提供当前应用的包名和布局文件的资源 id 就可以创建一个 RemoteViews 对象了。
public RemoteViews(String packageName, int layoutId) {
this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
}
1.3 如何更新 RemoteViews 的视图层级里的 View?
必须通过 RemoteViews 所提供的一系列方法来更新 View,比如:
| 操作 | RemoteViews 提供的方法 |
|---|---|
| 设置 TextView 的文本 | setTextViewText(int viewId, CharSequence text) 参数1:TextView 的 id,参数2:要设置的文本 |
| 设置 ImageView 的图片 | setImageViewResource(int viewId, int srcId) 参数1:ImageView 的 id,参数2:要设置的图片资源的 id |
| 给一个控件设置点击事件 | setonClickPendingIntent(int viewId, PendingIntent pendingIntent) 参数1:要设置点击事件的 View 的 id,参数2:一个待定的 Intent |
- 定义小部件的界面
- 定义小部件的配置信息
- 定义小部件的实现类
- 在 AndroidManifest.xml 中声明小部件
| 方法 | 作用 |
|---|---|
| onEnabled(Context context) | 当该窗口小部件第一次添加到桌面时会回调该方法,多次添加小部件时只在第一次回调该方法。 |
| onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) | 小部件被添加时或者每次小部件被更新时都会回调一次该方法,小部件的更新时机由配置信息里的 updatePeriodMillis 来指定,每个周期小部件都会自动更新一次。 |
| onDeleted(Context context, int[] appWidgetIds) | 每删除一次桌面小部件,该方法就会被回调。 |
| onDisabled(Context context) | 当最后一个该类型的桌面小部件被删除时,该方法会被回调。 |
| onReceive(Context context, Intent intent) | 这是广播的内置方法(AppWidgetProvider 是 BroadcastReceiver 的子类),用于分发具体的事件给其他方法,具体来说,onReceive 方法会根据不同的 action 来分别调用 onEnable、onDisable、onUpdate、onDelete 方法。 |
从类结构来看,PendingIntent 并非 Intent 的子类:
public final class PendingIntent implements Parcelable public class Intent implements Parcelable, Cloneable
从发生时机上看,PendingIntent 是在将来的某个不确定的时刻发生,而 Intent 是立刻发生。
从用途上看,要想给 RemoteViews 设置单击事件,就必须使用 PendingIntent,PendingIntent 通过 send 和 cancel 方法来发送和取消特定的待定 Intent。
1.7 PendingIntent 支持哪三种待定意图?| 待定意图 | 接口方法 | 解释 |
|---|---|---|
| 启动 Activity | public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags) | 获得一个 PendingIntent,该待定意图发生时,效果相当于 Context.startActivity(Intent) |
| 启动 Service | public static PendingIntent getService(Context context, int requestCode, Intent intent, int flags) | 获得一个 PendingIntent,该待定意图发生时,效果相当于 Context.startService(Intent) |
| 发送广播 | public static PendingIntent getBroadcast(Context context, int requestCode, Intent intent, int flags) | 获得一个 PendingIntent,该待定意图发生时,效果相当于 Context.sendBroadcast(Intent) |
PendingIntent 的匹配规则是:如果两个 PendingIntent 内部的 Intent 相同并且 requestCode 也相同,那么这两个 PendingIntent 就是相同的。
当一个 Intent 调用 filterEquals(Intent other) 方法与另一个 Intent 比较,返回 true,那么两个 Intent 是相同的,否则,不相同。
public boolean filterEquals(Intent other) {
if (other == null) {
return false;
}
if (!Objects.equals(this.mAction, other.mAction)) return false;
if (!Objects.equals(this.mData, other.mData)) return false;
if (!Objects.equals(this.mType, other.mType)) return false;
if (!Objects.equals(this.mPackage, other.mPackage)) return false;
if (!Objects.equals(this.mComponent, other.mComponent)) return false;
if (!Objects.equals(this.mCategories, other.mCategories)) return false;
return true;
}
可以看到,只要两个 Intent 的 action,data,type,class 和 categories 是相同的,两个 Intent 就是相同的。注意,Intent 的 extra 数据没有参与比较。
参考:PendingIntent 的文档说明。
1.9 FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT、FLAG_UPDATE_CURRENT 这 4 个标记位的含义是什么?-
FLAG_ONE_SHOT
这个标记位表示当前描述的 PendingIntent 仅仅可以被使用一次。如果设置了带有此标记的 PendingIntent,在调用了它的 send() 方法之后,它就会被自动取消;如果后续还有匹配的 PendingIntent 带有此标记,那么它们的 send() 方法就会调用失败。
-
FLAG_NO_CREATE
这个标记位表示如果当前描述的 PendingIntent 不存在,那么直接返回 null 而不是创建一个 PendingIntent。
-
FLAG_CANCEL_CURRENT
这个标记位表示如果当前描述的 PendingIntent 已经存在,则在产生新的 PendingIntent 之前,会把当前的 PendingIntent 取消。
-
FLAG_UPDATE_CURRENT
这个标记位表示如果当前描述的 PendingIntent 已经存在,则会把它保留下来并且把它的 extra 数据替换为新的 Intent 里的 extra 数据。
如果 notify 方法的 id 是相同的,那么不管 PendingIntent 是否匹配,后面的通知都会直接替换前面的通知。
如果 notify 方法的 id 不相同,当 PedingIntent 不匹配时,不管采用何种标记位 flags 参数,这些通知之间都不会相互干扰。
如果 notify 方法的 id 不相同,当 PendingIntent 处于匹配状态时,这时采用的标记位 flags 参数就会起作用了:
- 如果采用 FLAG_ONE_SHOT 标记位,则后续通知中的 PendingIntent 会和第一条通知保持完全一致,包括其中的 extras,单击任何一条通知后,剩下的通知均无法再打开,当所有的通知都被清除后,会再次重复这个过程。
- 如果采用 FLAG_CANCEL_CURRENT 标记位,则只有最新的通知可以打开,之前弹出的所有通知均无法打开。
- 如果采用 FLAG_UPDATE_CURRENT 标记位,则之前弹出的通知中的 PendingIntent 都会被更新,最终它们和最新的一条通知保持完全一致,包括其中的 extras,并且这些通知都是可以打开的。
RemoteViews 目前并不能支持所有的 View 类型,它所支持的所有类型如下:
| 分类 | 支持的类型 |
|---|---|
| Layout | AdapterViewFlipper、frameLayout、GridLayout、GridView、LinearLayout、ListView、RelativeLayout、StackView、ViewFlipper、RadioGroup(Android 31) |
| View | AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextClock、TextView、ViewStub、CheckBox(Android 31)、RadioButton(Android 31)、Switch(Android 31) |
RemoteViews 不支持表里所列类型的子类。
RemoteViews 不支持自定义 View。
从代码上说,只有标注有 @RemoteView 注解的 View 类型,才会被 RemoteViews 支持。
2.2 说一下 RemoteViews 的内部机制- 创建 RemoteViews 对象,并通过 RemoteViews 的 setXXX 方法设置一些基础操作,如设置图片,设置文本等,需要注意的是 set 方法对 View 所做的更新并不是立即执行的,只是在 RemoteViews 内部记录所有的更新操作而已;
- 通过 Binder 把 RemoteViews 传递到 SystemServer 进程,这是因为 RemoteViews 实现了 Parcelable 接口,可以进行跨进程传输;
- 在 SystemServer 进程拿到 RemoteViews 对象,根据该对象中的包名,布局文件通过 LayoutInflater 去加载对应的布局文件,得到对应的 View;
- 在 SystemServer 进程通过 RemoteViews 对象对 View 执行一系列界面更新任务,具体来说是调用了 RemoteViews 对象的 performApply 方法,在这个方法里逐个执行之前设置的更新操作。
Action 在 RemoteViews 里非常重要,可以说 RemoteViews 的重要作用就是对 Action 进行设置和使用。
- Action 可以封装对 View 的操作:RemoteViews 每调用一次 set 方法,都会生成一个封装了对应 View 操作的 Action;
- Action 可以进行跨进程传输,因为 Action 实现了 Parcelable 接口;
- Action 可以执行对 View 具体的更新操作:在远程进程调用 RemoteViews 的 performApply 方法,内部就是遍历所有的 Action 对象并调用它们的 apply 方法,即执行具体的 View 更新操作;
- Action 的使用避免定义大量的 Binder 接口,而且通过在远程进程中批量执行 RemoteViews 的修改操作从而避免了大量的 IPC 操作,提高了系统的性能。
-
作用不同:apply 方法会加载布局并更新界面,而 reapply 只会更新界面;
public View apply(Context context, ViewGroup parent, OnClickHandler handler) { RemoteViews rvToApply = getRemoteViewsToApply(context); View result; ... inflater = inflater.cloneInContext(inflationContext); inflater.setFilter(this); // 加载布局 result = inflater.inflate(rvToApply.getLayoutId(), parent, false); // 更新界面 rvToApply.performApply(result, parent, handler); return result; }public void reapply(Context context, View v, OnClickHandler handler) { RemoteViews rvToApply = getRemoteViewsToApply(context); // 更新界面 rvToApply.performApply(v, (ViewGroup) v.getParent(), handler); } -
调用时机不同:在初始化界面时会调用 apply 方法,而在更新界面时会调用 reapply 方法。
对于通知栏来说,
- 如果是初始化界面会走 baseStatusBar 的 createNotificationViews 方法,内部调用 RemoteViews 的 apply 方法;
- 如果是更新界面会走 baseStatusBar 的 updateNotificationViews 方法,内部调用 RemoteViews 的 reapply 方法。
对于桌面小部件来说,初始化界面和更新界面都是在 AppWidgetHostView 的 updateAppWidget 方法里。
setonClickPendingIntent(int viewId, PendingIntent pendingIntent) 用于给普通的 View 设置单击事件,但是不能给集合(如 ListView 和 StackView)中的 item 设置单击事件,因为比较耗费性能,所以系统不允许这种方式;
如果要给集合(如 ListView 和 StackView)中的 item 设置单击事件,必须将 setPendingIntentTemplate(int viewId, PendingIntent pendingIntentTemplate) 和 setonClickFillInIntent(int viewId, Intent fillInIntent) 组合使用才可以。
2.6 为什么对通知栏里的 RemoteViews 调用 setRemoteAdapter 方法没有效果?这是因为setRemoteAdapter 方法仅仅用于桌面小部件(Can only be used for App Widgets.)。参考:https://stackoverflow.com/questions/42484365/android-listview-in-notification-content-view。
参考- Android AppWidget
这篇文章讲解了 App Widget 的类关系,但是比较简洁。 - Android AppWidget系统框架
这篇文章讲解了 App Widget 的系统框架,里面绘制了大量的 UML 类图。 - Android神奇"控件"一一RemoteViews
使用 RemoteViews,实现跨应用更新 UI,自己觉得不可行。 - 关于PendingIntent你需要知道的那些事 - Android 开发者
- Android 嵌套 Intent - Android 开发者
- 从布局和实现的角度,聊聊 Notification
这篇文章介绍了几种新的通知栏类型,以及通知是如何发送以及显示的。作者使用类包含图清晰地说明了 Notification,StatusBarNotification,NotificationRecord 之间的关系,应该多学习这种理解代码的方式。 - Android 深入理解 Notification 机制
这篇文章介绍了通知是如何发送以及最终显示出来的。 - App Widgets 详解四 RemoteViews、RemoteViewsService和RemoteViewsFactory
这篇文章介绍了使用 ListView 的桌面小部件如何响应条目点击事件。 - 说说 PendingIntent 的内部机制
这篇文章里对于 PendingIntent 的图画得非常好。



