学习内容:
- RemoteViews 在通知栏和桌面小部件上的应用
- RemoteViews 的内部机制
- RemoteViews 的意义
RemoteView 的应用
实际开发中,RemoteViews 主要用在通知栏和桌面小部件的开发过程中。通知栏主要通过 NotificationManager 的 notify 方法实现,桌面小部件则是通过 AppWidgetProvider 来实现,其本质也是一个广播。
通知栏和桌面小部件更新界面时,RemoteView 无法像 View 一样在 Activity 中直接更新,因为界面运行在系统的 SystemServer 进程,需要跨进程更新。
下面简单介绍 RemoteView 的应用
-
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。(实际原因并非如此,后面详细介绍)
-
RemoteViews 在桌面小部件上的应用
利用 AppWidgetProvider,本质是广播。
-
定义小部件界面
在 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>
-
定义小部件配置信息
在 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"/>
-
定义小部件的实现类
继承 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); } }
-
在 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>
-
广播分发
当广播到来之后,AppWidgetProvider 会自动根据广播的 Action 通过 onReceive 来自动分发广播,相关方法如下
- onEnable: 当该窗口小部件第一次添加到桌面时调用的方法,可添加多次但只在第一次调用。
- onUpdate: 小部件被添加时或者每次小部件更新时都会调用一次该方法,小部件的更新时机是有updatePeriodMillis来指定,每个周期小部件就会自动更新一次。
- onDeleted: 每删除一次桌面小部件就调用一次。
- onDisabled: 当最后一个该类型的小部件被删除时调用该方法。
- onRestored:当接收到 ACTION_APPWIDGET_RESTORED 广播,从备份恢复小部件时调用
- onAppWidgetOptionsChanged:当接收到 ACTION_APPWIDGET_OPTIONS_CHANGED 广播,小部件的尺寸位置发生变化时调用。
- onReceive: 这是广播的内置方法,用于分发具体事件给其他方法。
-
-
PendingIntent 概述
-
基本介绍
- PendingIntent 表示一种处于 Pending(待定、等待、即将发生)状态的意图;
- 典型应用场景是给 RemoteViews 添加点击事件,(RemoteViews 运行在远程进程)
- 通过 send 和 cancel 方法来发送和取消特定的待定 Intent。
-
分类
启动 Activity ->
getActivity(Context context, int requestCode, Intent intent, int flags)
启动 Service ->
getService(Context context, int requestCode, Intent intent, int flags)
发送广播 ->
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会被替换为最新的。
-
匹配规则
- 如果两个 PendingIntent 内部的 Intent 相同且 requestCode 也相同,那么二者相同
- Intent 相同的匹配规则:Intent 的 ComponentName 和 intent-filter 都相同。Extras 不参与 Intent的匹配过程。
RemoteView 的内部机制
-
构造方法
public RemoteViews(String packageName, int layoutId)
参数说明:
- packageName:当前应用的包名
- layoutId:待加载的布局文件
-
限制 -> 支持的 View 类型有限
-
Layout :
FrameLayout
,LineanLayout
,RelativeLayout
,GridLayout
-
View:
AnalogClock
,Button
,Chronometer
,ImageButton
,ImageView
,ProgressBar
,TextView
,ViewFlipper
,ListView
,GridView
,StackView
,AdapterViewFlipper
,ViewStub
-
Layout :
-
特殊之处
- RemoteView 没有提供 findViewById 方法,因此无法直接访问里面的 View 元素,而必须通过 RemoteViews 所提供的一些列 set 方法来完成,这时因为 RemoteView 在远程进程中显示
- 一系列 set 方法 是通过反射来完成的。
-
工作流程
前置:通知栏和桌面小部件分别由 NotificationManager 和 AppWidgetManager 管理,而 NotificationManager 和 AppWidgetManager 通过 Binder 分别和 SystemServer 进程中的 NotificationManagerService(NMS) 以及 AppWidgetService(AWS) 进行通信。布局文件实际是在 NMS 和 AWS 中被加载的,而运行在 SystemServer 中,这就和我们的进程构成了 跨进程通信 的场景。
-
具体流程
首先 RemoteViews 通过 Binder 传递到 System Server 进程(RemoteViews 实现了 Parcelable 接口)。系统会根据 RemoteViews 中的包名等信息去得到该应用的资源。
然后通过 LayoutInflater 去加载 RemoteViews 中的布局文件。(对于 SystemServer 进程来讲,加载的只是一个普通的 view,只不过对于我们的进程来讲是 远程的)
-
接着系统对 View 执行一系列界面更新任务,这些任务通过 set 方法来提交。这些更新不是立刻执行,而是在 RemoteViews 中记录所有更新操作,等到 RemoteViews 被加载以后才能执行。
到此时,RemoteViews 就可以在 SystemServer 进程中显示了。
当需要更新 RemoteViews 时,调用一些列 set 方法并通过 NotificationManager 和 AppWidgetManager 来提交更新任务,具体操作也是在 SystemServer 进程中完成。
-
进一步说明 -- 跨进程
- 系统不直接通过 Binder 支持所有的 View 和 View 操作,否则 View 的方法庞大,同时 IPC 操作会影响效率
- 系统提供了一个 Action 概念, Action 实现了 Parcelable 接口,代表一个 View 操作。
- 系统首先将 View 操作封装到 Action 对象并将这些对象跨进程传输到远程进程,接着在远程进程中执行 Action 对象中的具体操作。远程进程通过 RemoteViews 的 apply 方法来进行 View 的更新操作,Remoteview 的 apply 方法内部会遍历所有的 Action 对象并调用它们的 apply 方法,进而执行具体的 View 的更新操作。
- 此方法避免了 定义大量的 Binder 接口,其次通过远程进程中批量执行修改擦欧总避免了大量 IPC 操作。
-
源码说明:
- 见原书吧。。
-
补充说明
- apply 和 reApply 的区别:前者会加载布局并更新界面,后者只会更新界面
- 关于点击事件。RemoteViews 中只支持发起 PendingIntent 不支持 onClickListener 那种模式。另外, setOnClickPendingIntent 用于给普通 View 设置点击事件,不能给集合(ListView / StackvView)中的 View 设置点击事件。如果要给 ListView / StackvView 中的 itemview 设置单击事件,必须将 setPendingIntentTemplate 和 setOnClickFillInIntent 组合使用才可以。
RemoteViews 的意义
-
从字面上就能猜到:RemoteViews 目的就是为了方便的更新远程 views ,即跨进程更新 UI
当一个应用需要能够更新另一个应用中的某个界面,这时候如果通过 AIDL实现,那么可能会随着界面更新操作的复杂导致效率变低。这种场景就很适合使用 RemoteViews。
RemoteViews 缺点在于 它只支持一些常见的 View,不支持自定义 View。
-
布局文件的加载问题
-
同一个应用的多进程情形
View view = remoteViews.apply(this,mRemoteViewsContent); mRemoteViewsContent.addView(view);
-
不同应用时
主要是由于两个应用的资源 ID 不一定一致,因此通过资源名称来加载布局文件
int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName()); view view = getLayoutInflater().inflate(layoutId,mRemoteViewsContent,flase); remoteViews.reapply(this,view); mRemoteViewsContent.addView(view);
-