RemoteViews 是一种远程 View,表示的是一种 View 结构,可以在其他进程中显示。RemoteViews 提供了一组基础的操作用于跨进程更新它的界面,RemoteViews 的使用场景主要是通知栏和桌面小部件。
一、 RemoteViews 的应用。
RemoteViews 在实际开发中,主要用于在通知栏上和桌面小部件的开发。
1、在通知栏上的应用
通知栏的开发,除了默认的效果,还能支持自定义布局。
- 系统默认样式
使用系统默认样式弹出一个通知很简单,代码如下:
private void showNotification(){
Notification.Builder builder1 = new Notification.Builder(MainActivity.this);
builder1.setSmallIcon(R.drawable.ic_launcher_background); //设置图标
builder1.setTicker("显示第二个通知");
builder1.setContentTitle("通知"); //设置标题
builder1.setContentText("点击查看详细内容"); //消息内容
builder1.setWhen(System.currentTimeMillis()); //发送时间
builder1.setDefaults(Notification.DEFAULT_ALL); //设置默认的提示音,振动方式,灯光
builder1.setAutoCancel(true);//打开程序后图标消失
Intent intent =new Intent (MainActivity.this,Center.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT);
builder1.setContentIntent(pendingIntent);
Notification notification1 = builder1.build();
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(124, notification1); // 通过通知管理器发送通知
}
上面的代码就是系统的默认样式的通知,单击通知后,会打开另一个 Activity,同时会清除自身,为了满足个性化需求,我们还可能会自定义通知。自定义通知很简单,我们需要提供一个布局文件,然后通过 RemoteViews 来加载这个布局文件就可以使用这个样式。
- 自定义布局
- 使用自定义布局
private void showCustomNotification(){
Notification.Builder builder1 = new Notification.Builder(MainActivity.this);
builder1.setSmallIcon(R.drawable.ic_launcher_background); //设置图标
builder1.setTicker("显示第二个通知");
builder1.setContentTitle("通知"); //设置标题
builder1.setContentText("点击查看详细内容"); //消息内容
builder1.setWhen(System.currentTimeMillis()); //发送时间
builder1.setDefaults(Notification.DEFAULT_ALL); //设置默认的提示音,振动方式,灯光
builder1.setAutoCancel(true);//打开程序后图标消失
// Intent intent =new Intent (MainActivity.this,Center.class);
//PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT);
RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.layout_notification);
remoteViews.setTextViewText(R.id.title,"RemoteViews");
remoteViews.setImageViewResource(R.id.img,R.mipmap.ic_launcher);
remoteViews.setTextViewText(R.id.content,"RemoteViews content");
PendingIntent openActivity = PendingIntent.getActivity(MainActivity.this,0,new Intent(this,Second.class),
PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.remoteviews,openActivity);
builder1.setContent(remoteViews);
// builder1.setContentIntent(pendingIntent);
Notification notification1 = builder1.build();
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(125, notification1); // 通过通知管理器发送通知
}
通知栏上 RemoteViews 的使用非常简单,只要使用布局就可以实现自定义样式,需要注意的是,RemoteViews 的更新需要使用特定的方法,如上面的 :
remoteViews.setTextViewText(R.id.title,"RemoteViews");
remoteViews.setImageViewResource(R.id.img,R.mipmap.ic_launcher);
分别为更新文本内容和图片,同样的,设置监听事件需要这样:
remoteViews.setOnClickPendingIntent(R.id.remoteviews,openActivity);
这里的 PedingIntent 表示一种待定的 Intent,这个 Intent 包含的意图必须由用户去触发。
2、在桌面小部件上的应用
AppWidgetProvider 是 Android 中提供的用于实现桌面小部件的类,其本质是一个广播,即 BroadcastReceiver。一个桌面小部件的开发步骤如下:
第1步:在res/layout目录下创建一个Widget布局文件.
第2步:创建一个类继承AppWidgetProvider.
第3步:在res/xml目录下创建一个XML文件,用来定义Widget的特性.
第4步:在AndroidManifest.xml中声明Widget.
使用Android Studio的模板功能,可以帮我们自动完成上面这些步骤.
菜单栏: File —> New —> Widget —> App Widget
然后弹出【New Android Component】界面:
Class Name: 类名,继承AppWidgetProvider.[图片]Placement: Widget 放在哪儿.
1.Home-screen and Keyguard: 在主屏幕和锁键上.
2.Home-screen only: 仅在主屏幕上.
3.Keyboard only(API 17+): 仅在锁键上(只支持Android4.2及以上版本).
Resizable(API 12+ ): Widget是否可调整大小,只支持Android 3.1及以上版本.
1.Horizontally and vertically: 水平和垂直显示时可调整.
2.Only Horizontally: 仅水平时可调整.
3.Only vertically: 仅垂直时可调整.
4.Not resizable: 不可调整.
使用默认配置,点击【Finish】后创建成功.
这几个文件如下:
- 布局文件 new_app_provider.xml
- 实现类 NewAppWidget .java
public class NewAppWidget extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
CharSequence widgetText = context.getString(R.string.appwidget_text);
// Construct the RemoteViews object
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
views.setTextViewText(R.id.appwidget_text, widgetText);
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
@Override
public void onEnabled(Context context) {
// Enter relevant functionality for when the first widget is created
}
@Override
public void onDisabled(Context context) {
// Enter relevant functionality for when the last widget is disabled
}
}
- 配置信息 res/xml/new_app_widget_info.xml
- 声明
上面是自动生成的代码,现在修改一下代码,首先修改一下样式:
new_app_widget.xml
接着修改实现类如下:
NewAppWidget.java
public class NewAppWidget extends AppWidgetProvider {
private static final String TAG = "NewAppWidget";
public static final String CLICK_ACTION = "com.hcworld.customview.Click";
RemoteViews views;
/**
* 接收窗口小部件点击时发送的广播
*/
@Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
//这里判断是自己的action,做自己的事情,比如小工具被点击了要干啥,这里是做来一个动画效果
if (intent.getAction().equals(CLICK_ACTION)) {
new Thread(new Runnable() {
@Override
public void run() {
Bitmap srcbBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.image);
for (int i = 0; i < 20; i++) {
float degree = (i * 90)%360;
views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
views.setImageViewBitmap(R.id.imageView1, rotateBitmap(context, srcbBitmap, degree));
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
views.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
appWidgetManager.updateAppWidget(
new ComponentName(context, NewAppWidget.class), views);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
private void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
CharSequence widgetText = context.getString(R.string.appwidget_text);
// Construct the RemoteViews object
views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
// "窗口小部件"点击事件发送的Intent广播
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
views.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
//views.setTextViewText(R.id.appwidget_text, widgetText);
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}
/**
* 每次窗口小部件被点击更新都调用一次该方法
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
/**
* 当该窗口小部件第一次添加到桌面时调用该方法,可添加多次但只第一次调用
*/
@Override
public void onEnabled(Context context) {
// Enter relevant functionality for when the first widget is created
super.onEnabled(context);
}
/**
* 当最后一个该窗口小部件删除时调用该方法,注意是最后一个
*/
@Override
public void onDisabled(Context context) {
// Enter relevant functionality for when the last widget is disabled
super.onDisabled(context);
}
/**
* 每删除一次窗口小部件就调用一次
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
}
private Bitmap rotateBitmap(Context context, Bitmap srcbBitmap, float degree) {
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree);
Bitmap tmpBitmap = Bitmap.createBitmap(srcbBitmap, 0, 0, srcbBitmap.getWidth(),
srcbBitmap.getHeight(), matrix, true);
return tmpBitmap;
}
}
最后,修改 AndroidManifest.xml 文件以接收广播:
上面的代码实现了一个简单的桌面小部件,在小部件上显示一张图片,单击之后,这个图片就会旋转。这个例子主要是为了说明,不管小部件是初始化还是后续更新都要通过 RemoteViews 来完成。
上面就是开发一个桌面小部件的典型过程,就算是负责的小部件开发过程也是一样的,都要是使用 RemoteViews 来实现。
3、PendingIntent 概述
PendingIntent 表示的是待定的 Intent ,它和 Intent 的区别就是 Intent 是立刻发生的,而 PendingIntent 是在将来某个不确定的时刻发生,PendingIntent 的典型使用场景就是 RemoteViews 添加单击事件,因为 RemoteViews 运行在远程进程中,因此 RemoteViews 不同于普通的 View 无法直接向 View 那样通过 setOnClickListener 方法来设置监听。要想给 RemoteViews 设置单击事件,必须通过 PendingIntent,PendingIntent 可以通过 send 和 cancel 方法来发送和取消待定发送的 Intent。
PendingIntent 支持三种待定的意图,启动 Activity、启动 Service 和发送广播,对于三个接口方法,如下:
getActivity(Context context,int requestCode,Intent intent,int flags):
获取一个 PendingIntent ,该待定意图发生时,效果相当于 Context.startActivity(intent);getService(Context context,int requestCode,Intent intent,int flags):
获得一个 PendingIntent,该待定 Intent 发生时,想当于 Context.startService(Intent);getBroadcast(Context context,int requestCode,Intent intent, int flags):
获取一个 PendingIntent ,该待定意图发生时,相当于 Context.sendBroadcast(intent);
这三个方法的参数一样,第一个和第三个参数比较好理解,第二个参数,requestCode 表示 PendingIntent 发送方的请求码,多数情况下设为 0 即可。另外 requestCode 会影响 flags 的效果。flags 的常见类型有: FLAG_ONE_SHOT、FLAG_NO_CANCEL、FLAG_CANCEL_CURRENT 和 FLAG_UPDATE_CURRENT。为了明白这四个标志,首先说一下 PendingIntent 的匹配规则:
- 如果两个 PendingIntent 内部的 Intent 相同,并且 requestCode 也相同,那么这两个 PendingIntent 是相同的。
Intent 的匹配规则是:
- 如果两个 Intent 的 ComponentName 和 intent-filter 相同,那么这两个 Intent 就是相同的。需要注意的是, Extras 不参与 Intent 的匹配过程。
接下来说明这四个标志的含义:
FLAG_ONE_SHOT
当前描述的 PendingIntent 只能被使用一次,然后这个 PendingIntent 就会被 cancel,如果后续还有相同的 PendingIntent ,那么他们的的 send 方法就会调用失败。对于通知栏消息来说,如果采用此标记位,那么同类的通知只能使用一次,后续的通知单击将无法打开。
FLAG_NO_CREATE
当前描述的 PendingIntent 不会主动创建,如果当前 PendingIntent 之前不存在,那么 getActivity、getService、getBroadcast 方法就会直接返回 null,即获取 PendingIntent 失败。这个标志一般不用。
FLAG_CANCEL_CURRENT
当前描述的 PendingIntent 已经存在,那么它们都会被 cancel,然后系统会创建一个新的 PendingIntent,对于通知栏消息来说,那些被 cancel 的消息来说,那些被 cancel 的消息单击之后无法打开。
FLAG_UPDATE_CURRENT
当前描述的 PendingIntent 如果已经存在,那么它们都会被更新,即它们的 Intent 中的 Extras 会被替换成最新的。
二、RemoteViews 的内部机制
RemoteViews 的作用是在其他进程中显示并更新 View 界面,为了更好的理解它的内部机制,我们先来看看它的主要功能。首先看一下构造方法:
public RemoteViews(String packageName,int layoutId);
这里最常见的构造方法,有两个参数:
packageName: 当前应用包名。
layoutId: 待加载的布局文件。
RemoteViews 目前不知道所有的 View 类型,目前支持的类型如下:
- Layout
FrameLayout,LinearLayout,RelativeLayout,GridLayout - View
AnalogClock,Button,Chronometer,ImageButton,ImageView,ProgressBar,TextView,ViewFipper,ListView,GridView,StackView,AdapterViewFlipper,ViewStub。
这些是 RemoteViews 所支持的所有 View 的类型,其他的都不支持如自定义 View 和 EditText 等。
RemoteViews 没有提供 findViewById 方法,因此无法直接访问里卖弄的 View 元素,而必须通过 RemoteViews 所提供的一系列 set 方法来完成,当然这是因为 RemoteViews 在远程级才能中显示,所以无法直接使用 findViewById。
常见的 set 方法如下:
- setTextViewText(int viewId,CharSequence text): 设置 TextVew 文本
- setTextViewTextSize(int viewId,int units,float size): 设置字体大小
- setTextColor(int viewId,int color): 设置颜色
- setImageViewResource(int viewId,int srcId): 设置 ImageView 图片资源。
- setInt(int viewId,String methodName,int value): 发射调用 View 对象的参数类型为 int 的方法
- setLong(int viewId,String methodName,long value): 参数为 long 的方法。
- setBoolean(int viewId,String methodName,boolean value): boolean 的方法。
- setOnClickPendingIntent(int viewId,PedingIntent pendingIntent): 为 View 添加点击事件,只能是 PendingIntent。
下面说一下 RemoteViews 的工作过程。我们知道,通知栏和桌面小部件,分别由 NotificationManager 和 AppWidgetManager 管理,而 Notification 和 AppWidgetManager 通过 Binder 分别与 SystemServer 进程中的NotificationService 和 AppWidgetService 进行通信。由此可见,通知栏和桌面小部件中的布局,实际上是在 NotificationManagerService 和 AppWidgetService 中被加载的。而他们运行在系统的 SystemService 中,这就和我们的进程构成了跨进程通信的场景。
首先,RemoteViews 会通过 Binder 传递到 SystemServer 进程,这是因为 RemoteViews 实现了 Parcelable 接口,因此它可以跨进程传输,系统会根据 RemoteViews 中的包名等信息去得到该应用的资源,然后会通过 LayoutInflater 去加载 RemoteViews 中的布局文件,接着系统会对 View 执行一系列界面更新任务,这些任务就是之前我们通过 set 方法来提交的。set 对 View 的更新不是立即执行的,在 RemteViews 内部会记录所有的更新操作,具体的执行时机要等到 RemoteViews 被加载以后才能执行,这样 RemoteViews 就可以在 SystemServer 进程中显示了。
RemoteViews 的内部机制可以概述为:系统将 View 操作封装到 Action对象,并将这些对象跨进程传输到远程进程,接着在远程进程中执行 Action 对象中的具体操作,在我们应用中没调用一次 set 方法,就会有对应的 Action 对象被添加到 RemoteViews 中,当 RemoteViews 传递到远程进程时,这些 Action 再依次执行。
接下来从源码的角度分析一下 RemoteViews,首先看 setTextViewText 方法,其源码如下:
/**
* 相当于调用TextView.setText
*
*/
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
上面的代码中,viewId 是被操作的 View 的 id,"setText" 是方法名,text 是要给 TextView 设置的文本。
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
从 setCharSequence 的实现可以看出,它的内部并没有对 View 直接操作,而是添加了一个 ReflectionAction 对象,从名字看,这是一个反射类型的动作,addAction 的实现如下:
/**
* 添加一个Action ,它会在远程进程调用apply方法的时候执行
*
* @param a The action to add
*/
private void addAction(Action a) {
if (hasLandscapeAndPortraitLayouts()) {
throw new RuntimeException("RemoteViews specifying separate landscape and portrait" +
" layouts cannot be modified. Instead, fully configure the landscape and" +
" portrait layouts individually before constructing the combined layout.");
}
if (mActions == null) {
mActions = new ArrayList();
}
mActions.add(a);
// update the memory usage stats
a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}
也就是 RemoteViews 内部有一个 mActions 成员,它是一个ArrayList,外界每调用一次 set 方法,就有一个 Action 对象加入到 ArrayList 中。这里只是存起来,没有实际的操作。在分析 ReflectionAction 之前,先看一下 RemoteViews 的 apply 方法及 Action 类的实现,apply 方法如下:
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result;
Context c = prepareContext(context);
LayoutInflater inflater = (LayoutInflater)
c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater = inflater.cloneInContext(c);
//设置过滤器,过滤掉一些不满足条件的View,
//比如用户自定义的View是不能被解析的,会报错
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
rvToApply.performApply(result, parent, handler);
return result;
}
通过上面的代码得知,首先会通过 LayoutInflater 去加载 RemoteViews 中的布局文件,加载完布局文件会通过 performApply 去执行一些更新操作。代码如下:
private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
if (mActions != null) {
handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
final int count = mActions.size();
for (int i = 0; i < count; i++) {
Action a = mActions.get(i);
a.apply(v, parent, handler);
}
}
}
这个方法主要是遍历 mActions 这个列表并执行每个 Action 对象的 apply方法。而 Action 对象的 apply 方法才是真正操作 View 的地方。
RemoteViews在通知栏和桌面小部件中的工作过程和上面描述的过程是一样的,当我们调用RemoteViews的set方法的时候,我们不会更新它们的界面,而是要通过NotificationManager和notify方法和AppWidgetManager的updateAppWidget方法才能更新它们的界面。实际上在AppWigetAManager的updateAppWidget的内部视线中,它们是通过RemoteViews的apply和reapply方法来加载和更新界面的。app会加载并且更新界面,而reapply只会更新界面。通知栏和桌面小插件会在初始化界面的时候调用apply方法,而在后续的更新界面时候会调用reapply方法。
三、RemoteViews 的意义
上面我们分析了 RemoteViews 的内部机制,了解RemoteViews的内部机制可以让我们更加清楚通知栏和桌面小工具的底层实现原理,但是本章对RemoteViews的探讨并没有停止,在本节中,我们将打造一个模拟的通知栏效果,并实现跨进程的UI更新。首先有两个Activity分别运行在不同的进程中,一个名字叫A,另一个叫B,其中A扮演着模拟通知栏的角色,而B则不停地发送通知栏消息,当然这是模拟消息。为了模拟通知栏的效果,我们修改A的process属性使其运行在单独的进程中,这也A和B就构成了多进程通信的情形。我们在B中创建的RemoteViews对象,然后通知A显示这个RemoteViews对象。如何通知A显示B中的RemoteViews呢?我们可以像系统一样采用Binder来实现,但是这里为了简单起见就采用了广播。B每发送一次模拟通知,就会发送一个特定的广播,然后A接收到广播后就开始显示B中定义的RemoteViews对象,这个过程和系统通知栏消息显示的过程几乎一致,或者说这里就是赋值了通知栏显示过程而已。
首先看B的实现,B只要构造RemoteViews对象并将其传输给A即可,这一过程通知栏是采用Binder实现的,但是本例中采用广播来实现的,RemoteViews对象通过Intent传输到A中,代码如下所示。
public class ActivityB extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_b);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_simulated_notification);
remoteViews.setTextViewText(R.id.msg, "msg from process:" + android.os.Process.myPid());
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
new Intent(this, DemoActivity_1.class),
PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(this, 0,
new Intent(this, DemoActivity_2.class),
PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.item_holder, pendingIntent);
remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);
Intent intent = new Intent(Constant.REMOTE_ACTION);
intent.putExtra(Constant.EXTRA_REMOTE_VIEWS,remoteViews);
sendBroadcast(intent);
finish();
}
}
在 A 中,需要接收 B 传递的广播并显示 RemoteViews,代码如下:
public class ActivityA extends AppCompatActivity {
public static final String TAG = "ActivityA";
private LinearLayout mRemoteViewsContent;
private BroadcastReceiver mRemoteViewsReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
RemoteViews remoteViews = intent.getParcelableExtra(Constant.EXTRA_REMOTE_VIEWS);
if (remoteViews != null) {
updateUi(remoteViews);
}
}
};
private void updateUi(RemoteViews remoteViews){
View view = remoteViews.apply(this, mRemoteViewsContent);
mRemoteViewsContent.addView(view);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_activity);
mRemoteViewsContent = (LinearLayout)findViewById(R.id.remote_views_content);
registerReceiver(mRemoteViewsReceiver, new IntentFilter(Constant.REMOTE_ACTION));
}
@Override
protected void onDestroy() {
unregisterReceiver(mRemoteViewsReceiver);
super.onDestroy();
}
public void sendMsg(View view) {
startActivity(new Intent(this, ActivityB.class));
}
}
上述代码很简单,除了注册和检出广播以外,最主要的逻辑其实就是updateUi方法。当A收到广播后,会从Intent中取出RemoteViews对象,然后通过它的apply方法加载布局文件并执行更新操作,最后将得到的View调价到A的布局中即可。可以发现,这个过程很简单,但是通知栏的底层就是这么实现的。
比如现在有两个应用,一个应用需要能够更新另外一个应用中的某个界面,这个时候我们当然可以选择AIDL去实现,但是如果对界面的更新比较繁琐,这个时候就会有效率问题,同时AIDL接口就有可能会变得很复杂。这个时候如果采用RemoteViews来实现就没有问题了。当然RemoteViews也有缺点,那就是它仅支持一些场景的View,对于自定义View它是不支持的。面对这种问题,到底是采用AIDL还是采用RemoteViews,这个要看具体情况,如果界面中的View都是一些简单的且被RemoteViews支持的View,那么可以考虑采用RemoteViews,否则就不适合用RemoteViews了。
如果打算采用RemoteViews来实现两个应用之间的界面更新,那么这里还有一个问题,那就是布局文件的加载问题。在上面的代码中,我们直接通过RemoteViews的apply方法来加载并更新界面,如下所示:
View view = remoteViews.apply(this, mRemoteViewsContent);
mRemoteViewsContent.addView(view);
这种写法在同一个应用的多进程情形下是合适的,但如果A和B属于不同应用,那么B中的布局文件资源ID传输到A中以后很有可能是无效的,因为A中的这个布局文件的资源ID不可能刚好和B中的资源ID一样,面对这种情况,我们就要适当修改RemoteViews的显示过程的代码了。这里给出一种方法,既然资源ID不同,我们就通过资源名称来加载布局文件。首先两个应用要提前约定好RemoteViews中的布局文件的资源名称,比如"layout_simulated_notifiaction",然后A中根据名称查找到对应的布局文件并加载,接着再调用RemoteViews的reapply方法即可将B中对View所做的一些列更新操作全面作用到A中加载的View上面。关于apply和reapply方法的差别再前面以及提到过,这里就不多说了,这样整个跨应用更新界面的流程就走通了,具体效果如下图所示。可以发现B中的布局文件已经成功的再A中显示出来了。修改后的代码如下所示:
int layoutId = getResources().getIdentifier("layout_simulated_notification", "layout", getPackageName());
View view = getLayoutInflater().inflate(layoutId,mRemoteViewsContent,false);
remoteViews.reapply(this,view);
mRemoteViewsContent.addView(view);
参考 《Android 开发艺术探索》 理解 RemoteViews