目录
- 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 方法没有效果?
- 参考
1. RemoteViews 的应用
1.1 说一说对 RemoteViews 的理解
从名字来看,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 |
1.4 开发桌面小部件的步骤是什么?
- 定义小部件的界面
- 定义小部件的配置信息
- 定义小部件的实现类
- 在 AndroidManifest.xml 中声明小部件
1.5 AppWidgetProvider 的回调方法有哪些?作用分别是什么?
方法 | 作用 |
---|---|
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 方法。 |
1.6 PendingIntent 和 Intent 的区别是什么?
从类结构来看,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) |
1.8 PendingIntent 的匹配规则是什么?
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
数据。
1.10 对于通知栏消息来说,通知 id,PendingIntent 是否匹配,标记位 flags 参数如何影响通知栏消息?
如果 notify
方法的 id
是相同的,那么不管 PendingIntent
是否匹配,后面的通知都会直接替换前面的通知。
如果 notify
方法的 id
不相同,当 PedingIntent
不匹配时,不管采用何种标记位 flags 参数,这些通知之间都不会相互干扰。
如果 notify
方法的 id
不相同,当 PendingIntent
处于匹配状态时,这时采用的标记位 flags 参数就会起作用了:
- 如果采用
FLAG_ONE_SHOT
标记位,则后续通知中的PendingIntent
会和第一条通知保持完全一致,包括其中的extras
,单击任何一条通知后,剩下的通知均无法再打开,当所有的通知都被清除后,会再次重复这个过程。 - 如果采用
FLAG_CANCEL_CURRENT
标记位,则只有最新的通知可以打开,之前弹出的所有通知均无法打开。 - 如果采用
FLAG_UPDATE_CURRENT
标记位,则之前弹出的通知中的PendingIntent
都会被更新,最终它们和最新的一条通知保持完全一致,包括其中的extras
,并且这些通知都是可以打开的。
2. RemoteViews 的内部机制
2.1 RemoteViews 是否支持所有的 View 类型?是否支持自定义 View?
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
方法,在这个方法里逐个执行之前设置的更新操作。
2.3 RemoteViews 里的 Action 的作用是什么?
Action
在 RemoteViews
里非常重要,可以说 RemoteViews
的重要作用就是对 Action
进行设置和使用。
Action
可以封装对View
的操作:RemoteViews
每调用一次set
方法,都会生成一个封装了对应View
操作的Action
;Action
可以进行跨进程传输,因为Action
实现了Parcelable
接口;Action
可以执行对View
具体的更新操作:在远程进程调用RemoteViews
的performApply
方法,内部就是遍历所有的Action
对象并调用它们的apply
方法,即执行具体的View
更新操作;Action
的使用避免定义大量的 Binder 接口,而且通过在远程进程中批量执行RemoteViews
的修改操作从而避免了大量的 IPC 操作,提高了系统的性能。
2.4 RemoteViews 的 apply 方法和 reapply 方法的区别是什么?
-
作用不同:
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
方法里。 - 如果是初始化界面会走
2.5 RemoteViews 的 setOnClickPendingIntent、setPendingIntentTemplate、setOnClickFillInIntent 的区别和联系是什么?
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
的图画得非常好。
文章评论