RemoteViews提供了一种跨进程更新界面的方式,一般用于通知栏和AppWidget的开发中。
通知栏需要用到的NotificationManager和小部件所用的AppWidgetProvider,都是运行在系统的SystemServer进程之中。我们如果想要对其进行界面更新的话,就需要用到RemoteViews。
要使用RemoteViews,需要以下几个步骤:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
LinearLayout>
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.remote_view);
remoteViews.setTextViewText(R.id.text, "Sample Text"); // 设置TextView文字
remoteViews.setImageViewResource(R.id.image, R.drawable.web_image); // 设置ImageView图片
事实上,这两个方法追溯过去:
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
public void setImageViewResource(int viewId, int srcId) {
setInt(viewId, "setImageResource", srcId);
}
public void setInt(int viewId, String methodName, int value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INT, value));
}
可以看到,都是调用了RemoteViews.addAction(Action)
方法,通过这个方法,把我们需要的操作保存在remoteViews的mAction变量中,然后在远端执行。而ReflectionAction看名字就知道,在远端找到控件之后,是通过反射的方式调用方法。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<ImageView
android:id="@+id/image"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@color/colorAccent" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="50dp"
android:text="text" />
FrameLayout>
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteViews.setTextViewText(R.id.text, "新设置的text");
remoteViews.setImageViewResource(R.id.image, R.color.colorAccent);
Intent intentClick = new Intent(this, DemoActivity_2.class);
remoteViews.setOnClickPendingIntent(R.id.image, PendingIntent.getActivity(this, 0, intentClick, PendingIntent.FLAG_UPDATE_CURRENT));
其中,remoteViews.setOnClickPendingIntent()
方法设置了id为R.id.image的控件的点击事件。
Intent intent = new Intent(this, DemoActivity_1.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentText("Ticker Text")
.setWhen(System.currentTimeMillis())
.setAutoCancel(true)
.setContentIntent(pi)
.setContent(remoteViews)
.build();
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.notify(1, notification);
可以看到,我们的通知布局已经换成我们自定义的layout_notification.xml了:
桌面小部件是通过AppWidgetProvider来实现的,其实质是一个广播接收器BroadcastReceiver。
开发桌面小部件的步骤:
新建布局文件widget.xml;
在res/xml/下新建文件appwidget_provider_info.xml;这个文件的作用是定义小部件的大小、刷新周期、布局等。
定义类MyAppWidgetProvider继承自AppWidgetProvider。定义方式类似于BroadcastReceiver,需要在AndroidManifest.xml中添加该类:
<receiver android:name=".MyAppWidgetProvider">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider_info" />
<intent-filter>
<action android:name="top.littlefogcat.chapter05_action_CLICK" />
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
intent-filter>
receiver>
其中meta-data中定义了小部件的布局,intent-filter中第一项是我们自定义的点击action,第二项为系统规定,只有添加了才能被识别为小部件。
系统在需要用到小部件的时候调用onUpdate()回调,所以我们需要在MyAppWidgetProvider中覆写onUpdate()方法,并调用AppWidgetManager.updateAppWidget(int, RemoteViews)来更新小部件的内容。
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.i(TAG, "onUpdate");
int counter = appWidgetIds.length;
Log.i(TAG, "onUpdate: counter = " + counter);
for (int appWidgetId : appWidgetIds) {
onWidgetUpdate(context, appWidgetManager, appWidgetId);
}
}
private void onWidgetUpdate(Context context, AppWidgetManager manager, int appWidgetId) {
Log.i(TAG, "onWidgetUpdate: appWidgetId = " + appWidgetId);
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.image, pendingIntent);
manager.updateAppWidget(appWidgetId, remoteViews);
}
其他的如onEnabled() onDisabled() onDeleted() 等回调,如果需要处理也可以进行覆写,当然这些action在onReceive()中都能收到,查看源码可以发现,sdk是在进行一系列处理之后分化出了这几个回调。
在发送Notification的时候,我们用到了PendingIntent。(使用AlarmManager设置定时任务的时候也会用到)一个典型的例子:
Intent intent = new Intent(this, DemoActivity_1.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
Notification notification = new Notification.Builder(this)
.setSmallIcon(R.drawable.web_image)
.setContentText("This is PendingIntent")
.setContentIntent(pendingIntent)
.build();
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.notify(1, notification);
在这里,我们定义了一个PendingIntent,通过setContentIntent()方法设置给Notification,这样我们点击这个Notification,就会跳转到DemoActivity_1了。
PendingIntent直译为“待定意图”,也就是说,它同Intent类似,是一种意图,但是PendingIntent是待定的,不是立即发生的。在这个意图生效之前,我们可以通过PendingIntent.cancel()方法来取消它。例如,我们把上面的代码做如下更改:
Intent intent = new Intent(this, DemoActivity_1.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
Notification notification = new Notification.Builder(this)
.setSmallIcon(R.drawable.web_image)
.setContentText("This is PendingIntent")
.setContentIntent(pendingIntent)
.build();
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.notify(1, notification);
// 加入以下代码
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
pendingIntent.cancel();
}
}, 10000);
在10秒之后,调用pendingIntent.cancel(),这时候我们发现,再点击通知,就不会跳转到DemoActivity_1了。
在获取PendingIntent的时候,如PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)
,最后一个参数是flag,其取值和含义如下:
FLAG_ONE_SHOT
这个PendingIntent只能发送一次,然后就会被cancel掉。之后再使用它,将会发送失败。如果这个PendingIntent已经存在并且还没有发送,那么返回它。
FLAG_NO_CREATE
如果这个PendingIntent存在,那么就返回它,否则返回null。
FLAG_CANCEL_CURRENT
如果这个PendingIntent已经存在,那么就取消掉之前的。返回一个新的PendingIntent。
FLAG_UPDATE_CURRENT
如果这个PendingIntent已经存在,那么保持它,并且把Intent中的extras更换成最新的并返回。
需要注意的是,如果两次PendingIntent.getActivity()传入的requestCode和intent(不包括Extras)一样的话,那么就被认为是相同的PendingIntent,即对于第二个PendingIntent来讲,它已经“存在”了。
RemoteViews不是支持所有的View。事实上它支持的View相当有限,包括:
FrameLayout, LinearLayout, RelativeLayout, GridLayout, AnalogClock, Button, Chronometer, ImageButton, ImageView, ProgressBar, TextView, ViewFlipper, ListView, GridView, StackView, AdapterViewFlipper, ViewStub.
其他类型的View传入RemoteViews会抛出异常。
RemoteViews设置View内容是通过反射实现的。例如设置TextView显示文字的方法:
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
同理假设要调用TextView.setHint(String),可以这么做:
remoteViews.setCharSequence(R.id.text, "setHint", "This is Hint");
对于通知和小部件来讲,布局都是在SystemServer进程中,分别通过NotificationManagerService和AppWidgetService加载的。
RemoteViews.mActions记录了对View的具体操作。mActions是一个列表,包含若干个Action,每个Action对应了对View的操作。在远端进程加载布局文件之后,会遍历mAction,并且分别调用每个Action的apply()方法执行操作。
RemoteViews的主要意义在于跨进程的界面显示和更新。
相比于AIDL,RemoteViews的方式高效且简洁,但是缺点是支持的View类型有限。究竟选择什么方式,需要自行抉择。