一、简介
RemoteViews是一种远程View,可以在其他进程中显示,为了能够更新它的界面,RemoteViews提供了一组基础操作用于跨进程更新它的界面。
RemoteViews常用在通知和桌面小组件中。
二、使用
RemoteViews在通知栏上的应用
1、简单使用
<1> 创建NotificationManager对象
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
<2> 创建RemoteViews对象
RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.activity_test_notification_custom);
参数说明:第一个是程序的包名,第二个自定义的布局
<3> 创建通知对象并设置RemoteViews
NotificationCompat.Builder builder ;
//适配android8.0
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
String channelID = "1";
String channelName = "TestRemoteViews";
NotificationChannel channel = new NotificationChannel(channelID,channelName, NotificationManager.IMPORTANCE_HIGH);
manager.createNotificationChannel(channel);
builder = new NotificationCompat.Builder(this,channelID);
}else{
builder = new NotificationCompat.Builder(this,null);
}
builder.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("TestRemoteViews")
.setContentText("");
//设置RemoteViews
.setContent(remoteViews);
Notification notification = builder.build();
<4> 发送通知
manager.notify(1002,builder.build());
这样就完成了自定义布局的通知。
2、更新RemoteViews界面
RemoteViews没有提供findViewById方法,因为RemoteViews在远程进程中显示,因此无法直接访问里面的View元素,而必须通过RemoteViews所提供的一系列set方法来完成。比如:
<1> 更新RemoteViews布局中的TextView内容
remoteViews.setTextViewText(R.id.test_notification_custom_tv,"我是一个自定义的RemoteViews");
参数说明:第一个参数是布局中TextView的ID,第二个参数是需要更新的内容
<2> 为RemoteViews布局中的Button设置点击事件
Intent intent1 = new Intent(this,TestTargetActivity1.class);
PendingIntent pendingIntent1 = PendingIntent.getActivities(this,0,new Intent[]{intent1},PendingIntent.FLAG_UPDATE_CURRENT);
//给第一个按钮设置点击事件,实现跳转到TestTargetActivity1
remoteViews.setOnClickPendingIntent(R.id.test_notification_custom_btn_1,pendingIntent1);
Intent intent2 = new Intent(this,TestTargetActivity2.class);
PendingIntent pendingIntent2 = PendingIntent.getActivities(this,0,new Intent[]{intent2},PendingIntent.FLAG_UPDATE_CURRENT);
//给第二个按钮设置点击事件,实现跳转到TestTargetActivity2
remoteViews.setOnClickPendingIntent(R.id.test_notification_custom_btn_2,pendingIntent2);
<3> 为RemoteViews布局中的ImageView设置图片
//根据图片资源设置图片
remoteViews.setImageViewResource(R.id.test_notification_custom_iv,R.drawable.test);
除了上面介绍的三种方法外,RemoteViews还提供了其他的一些方法
setTextColor(viewId, color) 设置文本颜色
setTextViewTextSize(viewId, units, size) 设置文本大小
setImageViewBitmap(viewId, bitmap) 设置图片
setViewPadding(viewId, left, top, right, bottom) 设置Padding间距
3、注意
RemoteViews设置的布局文件并不支持所有的View(包括自定义view),以下是RemoteViews所支持的View:
layout:
FrameLayout,LinearLayout,RelativeLayout,GridLayout
view:
Button、ImageView、ImageButton、TextView、ProgressBar、ListView、GridView、StackView、ViewStub、AdapterViewFlipper、ViewFlipper、AnalogClock、Chronometer
RemoteViews在桌面小组件上的应用
1、简单使用
<1> 定义小部件布局
在res/layout/下新建一个布局文件layout_widget.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/test_app_widget_custom_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="TextView" />
</LinearLayout>
<2> 定义小部件配置信息
在res/xml/下新建一个资源文件,命名custom_app_widget.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="200dp"
android:minHeight="50dp"
android:initialLayout="@layout/layout_widget"/>
解释下appwidget-provider各个属性的含义:
android:initialLayout:指定小部件的初始化布局
android:minHeight:小部件最小高度
android:minWidth:小部件最小宽度
android:previewImage:小部件列表显示的图标
android:updatePeriodMillis:小部件自动更新的周期
android:widgetCategory:小部件显示的位置,默认为home_screen表示只在桌面上显示
<3> 定义小部件的实现类
public class TestAppWidgetRemoteViews extends AppWidgetProvider{
/**
* 当小组件被添加到屏幕上时回调
*/
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
//启动TestAppWidgetRemoteViewsService服务
context.startService(new Intent(context, TestAppWidgetRemoteViewsService.class));
}
/**
* 当小组件被添加或每次被刷新时回调,更新时机由android:updatePeriodMillis来指定
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
/**
* 当widget小组件从屏幕移除时回调
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
}
/**
* 当最后一个该类型的小组件被从屏幕中移除时回调
*/
@Override
public void onDisabled(Context context) {
super.onDisabled(context);
}
}
<4> 创建TestAppWidgetRemoteViewsService服务
public class TestAppWidgetRemoteViewsService extends Service {
Timer timer;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
updateTime();
}
};
//服务启动时会每个1s去更新一次RemoteViews界面
timer.schedule(timerTask,0,1000);
}
private void updateTime() {
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_widget);
//将RemoteViews界面的TextView内容设置为时间,实现类似钟表的效果
remoteViews.setTextViewText(R.id.test_app_widget_custom_tv,new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
AppWidgetManager manager = AppWidgetManager.getInstance(getApplicationContext());
ComponentName componentName = new ComponentName(this,TestAppWidgetRemoteViews.class);
manager.updateAppWidget(componentName,remoteViews);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
}
<5> 在清单文件上声明小部件和服务
<service android:name=".TestAppWidgetRemoteViewsService"/>
<receiver android:name=".TestAppWidgetRemoteViews">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/custom_app_widget" />
</receiver>
AppWidgetProvider 继承自 BroadcastReceiver,本质是一个广播,所以需要在清单文件中注册。
meta-data标签中的name属性是固定的android.appwidget.provider,resource属性则是刚才新建的小部件的配置信息的xml。
intent-filter中的android.appwidget.action.APPWIDGET_UPDATE是必须加的,它作为小部件的标识存在,这是系统的规范,
否则这个receiver就不是一个桌面小部件,并且也无法出现在手机的小部件列表里。
<6> 添加小部件到桌面
经过上面的5步就可以实现一个类似时钟的小部件,然后只需长按屏幕空白处,在小部件(或小工具)的列表中找到开发的小部件,拖到桌面即可。
三、RemoteViews的内部机制
通知栏和桌面小部件分别由NotificationManager和AppWidgetManager管理,而NotificationManager和AppWidgetManager通过Binder分别与SystemServer进程中的NotificationManagerService和AppWidgetService进行通信。
由此可见,通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService和AppWidgetService中被加载的,而他们运行在系统的SystemService中,这就和我们的进程构成了跨进程通信的场景。
1、在主进程中,创建RemoteViews,由于它实现了Parcelable接口,因此可以通过Binder跨进程传输到SystemServer进程。
2、在SystemServer进程中,系统通过RemoteViews携带的包名属性获取应用资源,并加载RemoteViews携带的布局文件,得到一个View。这样RemoteViews就在SystemServer中完成加载了。
3、如果要对RemoteViews进行操作,可以在主进程中调用RemoteViews提供的set方法,系统将每一个View操作对应地封装成一个Action对象,然后通过Binder传输到SystemServer进程中,
在RemoteViews中添加一个Action对象,当NotificationManager或AppWidgetManager提交更新时,RemoteViews就执行apply方法来更新View,这会遍历所有暂存的Action对象并调用他们的apply方法来执行具体的View更新操作。
四、源码分析:
以一个set方法为例,比如setTextViewText方法。
setTextViewText方法:
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
setTextViewText方法中调用了setCharSequence方法
setCharSequence方法:
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
该方法中调用了addAction方法,传入一个ReflectionAction实例,ReflectionAction继承自Action,它是用反射调用的。
addAction方法:
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<Action>();
}
mActions.add(a);
// update the memory usage stats(更新内存的使用情况)
a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}
addAction方法,用了一个mActions集合来保存Action实例,然后更新已使用内存的统计情况
这样setTextViewText里面的内容就分析完了,通过上面的简单使用知道在设置完RemoteViews后,就是通过调用manager.notify(1002,builder.build());来发送通知了,接下来追踪这个notify方法。
notify方法:
public void notify(int id, Notification notification){
notify(null, id, notification);
}
public void notify(String tag, int id, Notification notification){
notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId()));
}
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user){
INotificationManager service = getService();
String pkg = mContext.getPackageName();
// Fix the notification as best we can.
Notification.addFieldsFromContext(mContext, notification);
if (notification.sound != null) {
notification.sound = notification.sound.getCanonicalUri();
if (StrictMode.vmFileUriExposureEnabled()) {
notification.sound.checkFileUriExposed("Notification.sound");
}
}
fixLegacySmallIcon(notification, pkg);
if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
if (notification.getSmallIcon() == null) {
throw new IllegalArgumentException("Invalid notification (no valid small icon): "+ notification);
}
}
if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
try {
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,copy, user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
这个方法中的内容比较多,最主要的就是INotificationManager service = getService();INotificationManager是系统服务,是在SystemServer进程添加的。
最后会调用enqueueNotificationWithTag方法。RemoteViews在进程间更新UI界面是通过AIDL的方式进行的。之后在这个进程 RemoteViews会执行它的apply方法或者reapply方法。
回到RemoteViews 的apply/reapply方法:
public class RemoteViews implements Parcelable, Filter {
......
public View apply(Context context, ViewGroup parent) {
return apply(context, parent, null);
}
/** @hide */
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result = inflateView(context, rvToApply, parent);
loadTransitionOverride(context, handler);
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);
}
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);
//调用apply方法
a.apply(v, parent, handler);
}
}
}
......
}
apply方法和reapplay方法的区别在于:`apply会加载布局+更新界面,而reapply只更新界面。通知栏和桌面小部件在初始化的时候会调用apply方法,再后续更新的时候则调用reapply方法。
通过上面的分析知道performApply方法中的mActions就是在setTextViewText方法时将ReflectionAction对象添加到集合中的mActions。这里会遍历该集合调用Action的apply方法。
ReflectionAction的apply方法
private final class ReflectionAction extends Action {
...
ReflectionAction(int viewId, String methodName, int type, Object value) {
this.viewId = viewId;
this.methodName = methodName;
this.type = type;
this.value = value;
}
...
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final View view = root.findViewById(viewId);
if (view == null) return;
Class<?> param = getParameterType();
if (param == null) {
throw new ActionException("bad type: " + this.type);
}
try {
getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));//采用反射的方法来更新View
} catch (ActionException e) {
throw e;
} catch (Exception ex) {
throw new ActionException(ex);
}
}
...
}
采用了反射的方法来执行对View的更新操作。
setTextViewText, setBoolean, setLong, setDouble等set方法都使用了ReflectionAction,还有其他Action实现类,比如对应setTextViewSize的TextViewSizeAction,它没有用反射来实现,代码如下:
private class TextViewSizeAction extends Action {
...
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final TextView target = root.findViewById(viewId);
if (target == null) return;
target.setTextSize(units, size);
}
}
...
}
之所以不使用反射来实现TextViewSizeAction,是因为setTextSize这个方法有2个参数,无法复用ReflectionAction。
五、RemoteViews的意义
RemoteViews最大的意义在于方便的跨进程更新UI。
1、当一个应用需要更新另一个应用的某个界面,我们可以选择用AIDL来实现,但如果更新比较频繁,效率会有问题,同时AIDL接口就可能变得很复杂。如果采用RemoteViews就没有这个问题,但RemoteViews仅支持一些常用的View,如果界面的View都是RemoteViews所支持的,那么就可以考虑采用RemoteViews。
2、利用RemoteViews加载其他App的布局文件与资源。
final String pkg = "cn.hudp.remoteviews";//需要加载app的包名
Resources resources = null;
try {
resources = getPackageManager().getResourcesForApplication(pkg);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
if (resources != null) {
int layoutId = resources.getIdentifier("activity_main", "layout", pkg); //获取对于布局文件的id
RemoteViews remoteViews = new RemoteViews(pkg, layoutId);
View view = remoteViews.apply(this, llRemoteViews);//llRemoteViews是View所在的父容器
llRemoteViews.addView(view);
}
了解RemoteViews之后想实现 一个 2个进程间模拟通知栏 其实很简单 原理如下:
<1> 创建两个Activity 在2个进程运行 (这里之所以在一个程序中创建2个进程是为了方便。。)
<2> 在一个Activity中发送一个广播 在广播中put一个remoteViews 因为它是Pracelable对象
<3> 在另一个Activity中接受这个广播 然后取出remoteViews对象 执行它的apply方法 之后把view添加到这个Activity的父布局中即可
如果A和B属于不同应用,那么B中的布局文件的资源id传输到A中以后很有可能是无效的,因为A中的这个布局文件的资源id不可能刚好和B中的资源id一样,
面对这种情况,就要适当的修改Remoteviews的显示过程的代码了。这里给出一种方法,既然资源不相同,那就通过资源名称来加载布局文件。
首先两个应用要提前约定好RemoteViews中的布局的文件名称,比如“layout simulated notification”,然后在A中根据名称找到并加载,
接着再调用Remoteviews 的的reapply方法即可将B中对View所做的一系列更新操作加载到View上了,关于applyHe reapply方法的差别在前面说了,这样历程就OK了
int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName());
View view = getLayoutInflater().inflate(layoutId,mLinearLayout,false);
remoteViews.reapply(this,view);
mLinearLayout.addView(view);