学习内容:
实际开发中,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,名称和内容可自定义,视小部件具体需求而定。
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
"@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/icon" />
定义小部件配置信息
在 res/xml/ 下新建 appwidget_provider_info.xml,名称任意。
"1.0" encoding="utf-8"?>
"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 中声明小部件
原因:本质是广播组件,因此需要注册
".MyAppWidgetProvider">
"android.appwidget.provider"
android:resource="@xml/appwidget_provider_info">
//识别小部件的单击行为
"com.whdalive.action.click" />
//作为小部件的标识,必须存在
"android.appwidget.action.APPWIDGET_UPDATE" />
广播分发
当广播到来之后,AppWidgetProvider 会自动根据广播的 Action 通过 onReceive 来自动分发广播,相关方法如下
PendingIntent 概述
基本介绍
分类
启动 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)
参数说明:
匹配规则
构造方法 public RemoteViews(String packageName, int layoutId)
参数说明:
限制 -> 支持的 View 类型有限
FrameLayout
,LineanLayout
,RelativeLayout
,GridLayout
AnalogClock
,Button
,Chronometer
,ImageButton
,ImageView
,ProgressBar
,TextView
,ViewFlipper
,ListView
,GridView
,StackView
,AdapterViewFlipper
,ViewStub
特殊之处
工作流程
前置:通知栏和桌面小部件分别由 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 进程中完成。
进一步说明 – 跨进程
源码说明:
补充说明
从字面上就能猜到: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);