Android开发五《理解RemoteViews》
RemoteViews表示的是一个View结构,它可以在其他进程中显示,它提供了一组基础的操作用于跨进程更新它的界面.
1.png
支持的布局:
- AdapterViewFlipper
- FrameLayout
- GridLayout
- GridView
- LinearLayout
- ListView
- RelativeLayout
- StackView
- ViewFlipper
支持的控件: - AnalogClock
- Button
- Chronometer
- ImageButton
- ImageView
- ProgressBar
- TextClock
- TextView
一、RemoteViews的应用
使用场景:
1、通知栏:通过NotiicationManager的notify方法实现
Intent intent = new Intent(this, NotiActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
String id = "my_channel_01";
CharSequence name = "channel";
String description = "description";
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel mChannel = new NotificationChannel(id, name, importance);
mChannel.setDescription(description);
mChannel.enableLights(true);
mChannel.setLightColor(Color.RED);
mChannel.enableVibration(true);
mChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(mChannel);
RemoteViews remoteView = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteView.setTextColor(R.id.re_text, Color.RED);
remoteView.setTextViewText(R.id.re_text, "remote view demo");
remoteView.setImageViewResource(R.id.re_image, R.drawable.btn_me_share);
remoteView.setOnClickPendingIntent(R.id.notification, pendingIntent);
Notification notification = new Notification.Builder(this, id)
.setAutoCancel(false)
.setContentTitle("title")
.setContentText("describe")
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.btn_me_share)
.setOngoing(true)
.setCustomContentView(remoteView)
.setWhen(System.currentTimeMillis())
.build();
manager.notify(1, notification);
从上面代码发现,RemoteViews的方法使用起来很简单。利用构造函数new RemoteViews(packagename, layoutId) 来关联一个view的布局,并通过一些set 方法更新布局,最后利用notification.Builder().setCustomContentView(RemoteViews) 来设置通知栏的view
2、桌面小部件:通过AppWidgetProvider(本质是一个广播)来实现;
桌面小部件主要是利用RemoteViews和AppWidgetProvider结合使用,而AppWidgetProvider又是extends BroadcastReceiver, 所以再使用的时候,多了一些关于广播的知识。
public class NewAppWidget extends AppWidgetProvider {
private static final String CLICK_ACTION = "com.taohuahua.action.click";
static void updateAppWidget(final Context context, final AppWidgetManager appWidgetManager, final int appWidgetId) {
final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.layout_widget);
Intent anIntent = new Intent();
anIntent.setAction(CLICK_ACTION);
PendingIntent anPendingIntent = PendingIntent.getBroadcast(context, 0, anIntent, 0);
views.setOnClickPendingIntent(R.id.appwidget_text, anPendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
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
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.layout_widget);
if (Objects.equals(intent.getAction(), CLICK_ACTION)) {
Toast.makeText(context, "hello world", Toast.LENGTH_SHORT).show();
//获得appwidget管理实例,用于管理appwidget以便进行更新操作
AppWidgetManager manger = AppWidgetManager.getInstance(context);
// 相当于获得所有本程序创建的appwidget
ComponentName thisName = new ComponentName(context, NewAppWidget.class);
//更新widget
manger.updateAppWidget(thisName, views);
}
}
从代码中可以看出,里面有几个重要的方法:
- onUpdate: 小部件被添加时或者每次更新时调用。更新时间由第二步配置中updatePeriodMills来决定,单位为毫秒。
- onReceive: 广播内置方法,用于分发接收到的事件。
- onEnable: 当该窗口小部件第一次添加时调用。
- onDelete:每删除一次调用一次。
- onDisabled:最后一个该桌面小部件被删除时调用。
所以在onUpdate方法中利用RemoteViews来显示了新的布局,并利用pendingIntent来实现点击小部件控件跳转的方法。
二、RemoteViews的内部机制
RemoteView的作用是在其他进程中显示并更新View的界面,最常用的构造方法:
public RemoteViews(String packageName,int layoutId)
RemoteViews没有提供findViewById方法,必须通过RemoteViews所提供的一系列set方法来完成更新;这些set方法通过反射来完成的.
RemoteViews的部分set方法
通知栏以及小部件分别由NotificationManager和AppWidgetManager管理,而NotificationManager以及AppWidgetManager通过Binder分别和SystemServer进行中的NotificationManagerService以及AppWidgetService进行通信,因此,通知栏以及桌面小部件中的布局文件是在NotificationManagerService以及AppWidgetService中被加载的,而它们运行在系统的systemServer中,这就和我们的进行构成了跨进程通信的场景。
这里没有使用Binder进行进程通信,由于View的方法太多大量的IPC操作会影响效率,这里提供了Action的概念,Action代表一个View的操作,系统将Action操作封装到Action对象并将这些对象跨进程传输到远程进程中,接着直接Action对象中的Action操作。我们使用RemoteViews时,每调用一个set方法,就会添加一个Action对象,当我们通过NotificationManager和AppWidgetManager提交更新时,这些Action对象就会传输到远程进程中并依次执行。
内部机制
在RemoteViews的源码中,可以看到定义了一个Action对象的列表
/**
* An array of actions to perform on the view tree once it has been
* inflated
*/
private ArrayList<Action> mActions;</pre>
而Action的是对远程视图进行的操作的一个封装。因为我们无法通过RemoteViews的findViewById方法来操作视图,所以RemoteViews每次视图的操作都会创建一个action对象添加到列表中。
/**
* Base class for all actions that can be performed on an
* inflated view.
*
* SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!!
*/
private abstract static class Action implements Parcelable {
public abstract void apply(View root, ViewGroup rootParent, OnClickHandler handler) throws ActionException;
public static final int MERGE_REPLACE = 0;
public static final int MERGE_APPEND = 1;
public static final int MERGE_IGNORE = 2;
public int describeContents() {
return 0;
}
/**
* Overridden by each class to report on it's own memory usage
*/
public void updateMemoryUsageEstimate(MemoryUsageCounter counter) {
// We currently only calculate Bitmap memory usage, so by default,
// don't do anything here
}
public void setBitmapCache(BitmapCache bitmapCache) {
// Do nothing
}
public int mergeBehavior() {
return MERGE_REPLACE;
}
public abstract String getActionName();
public String getUniqueKey() {
return (getActionName() + viewId);
}
/**
* This is called on the background thread. It should perform any non-ui computations
* and return the final action which will run on the UI thread.
* Override this if some of the tasks can be performed async.
*/
public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
return this;
}
public boolean prefersAsyncApply() {
return false;
}
/**
* Overridden by subclasses which have (or inherit) an ApplicationInfo instance
* as member variable
*/
public boolean hasSameAppInfo(ApplicationInfo parentInfo) {
return true;
}
int viewId;
}
从源码中可以看出,action提供了一个抽象的方法
public abstract void apply(View root, ViewGroup rootParent, OnClickHandler handler) throws ActionException;
在RemoteViews.java中发现了有很多Action的子类, 这里重点讲解一个类
/**
* Base class for the reflection actions.
*/
private final class ReflectionAction extends Action {
...
}
因为很多更新视图的方法最后都走到
addAction(new ReflectionAction(viewId, methodName, type, value));
可以发现,当RemoteViews通过set方法来更新一个视图时,并没有立即更新,而是添加到action列表中。这样可以大大提高跨进程通信的性能,至于什么时候更新,对于自定义通知栏,需要NotificationManager调用notify()之后;而对于桌面小部件,则需要AppWidgetManager调用updateAppWidget()之后。
最后进入ReflectionAction类中的apply方法看一下,发现内部就是利用反射机制设置相关视图的。
@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));
} catch (ActionException e) {
throw e;
} catch (Exception ex) {
throw new ActionException(ex);
}
}
注意:当我们调用RemoteViews的set方法时,并不会立刻更新它们的界面,必须通过NotificationManager的notify方法以及AppWidgetManager的updateAppWidget方法才会更新他们的界面。
三、PendingIntent概述
peddingIntent表示一种处于pending状态的意图,pending状态表示一种待定、等待、即将发生。就是说接下来有一个Intent将要在某个待定的时刻发生。PendingIntent和Intent的区别在于,PendingIntent是在将来某个时刻发生,而Intent是立刻发生。PendingIntent典型的使用场景是给RemoteViews添加单机事件。由于RemoteViews运行在远程进程中,无法直接调用setOnClickListener方法来设置单击事件,就需要使用pendingIntent,PendingIntent通过send和cancel来发送和取消待定的Intent。
PendingIntent支持的三种待定意图:启动Activity、启动Service以及发送广播
PendingIntent的主要方法
这三个方法的参数意义是相同的,第一个和第三个参数比较好理解,主要是第二个和第四个;
第二个参数requestCode,requestCode表示PendingIntent方的请求码,多数情况设为0即可。requestCode会影响到flags的效果.
第四个参数flags有四种类型:
- FLAG_ONE_SHOT
PendingIntent只被使用一次,然后被自动cancel,后续如果还有相同的PendIntent,那么它们的send方法调用失败。对通知栏消息来说,同类的通知只会使用一次,后续的通知单击后将无法打开。 - FLAG_NO_CREATE
当前描述的PendIntent不会主动创建,如果当前PendIntent之前不存在,那么getActivity,getService,getBroadcast方法会直接返回null,即获取PendingIntent失败。 - FLAG_CANCEL_CURRENT
当前描述的PendIntent如果已经存在,那么它们会被cancel,然后系统创建一个新的PendingIntent。对通知栏消息来说,那些被cancel的消息单击后将无法打开。 - FLAG_UPDATE_CURRENT
当前描述的PendIntent如果已经存在,那么它们都会被更新。即它们的Intent中的Extras会被替换成最新的。
PendingIntent的匹配规则为:
如果两个PendingIntent它们内部的Intent相同并且 requestCode也相同则相同。
Intent的匹配规则为:
如果两个Intent的ComponentName和intent-filter都相同,则它们两个就是相同的。
通过通知栏消息理解这四个标记,分两种情况
manager.notify(1,notification)
1、通知ID是常量
多次调用notify只能弹出一个通知,后续的通知会把前面的通知完全替代掉;
2、通知ID是变量
多次调用notify会弹出多个通知;这也分两种情况
- PendingIntent不匹配
不管采用何种标记位,这些通知之间不会相互干扰;
- PendingIntent不匹配
- PendingIntent匹配状态,标记位为:
FLAG_ONE_SHOT:后续通知中的PendingIntent会和第一条通知保持完全一致,包括其中的Extras,单击任何一条通知后,剩下的通知均无法再打开,当所有的通知都被清除后,会再次重复这个过程
FLAG_CANCEL_CURRENT:只有最新的通知可以打开,之前弹出的所有通知均无法打开;
FLAG_UPDATE_CURRENT:之前弹出的通知中的PendingIntent会被更新,最终它们和最新一条通知保持完全一致,包括其中的Extras,并且这些通知都是可以打开的.
- PendingIntent匹配状态,标记位为: