Android开发艺术探索第五章——理解RemoteViews
这门课的重心在于RemoteViews,RemoteViews可以理解为一种远程的View,其实他和远程的Service是一样的,RemoteViews表示的是一种View的结构,他可以在其他的进程中显示,最常用的就是通知栏和桌面小组件了,我们接下来就细细的品味一下吧!
一.RemoteViews的应用
RemoteViews在实际的开发中,就是通知栏和桌面小组件了,这个大家应该都不陌生,主要是通过NotificationManager的notify方法去实现通知栏,也有通过AppwidthProvider来实现桌面小部件的,其实小部件本质上就是一个广播,他们两个的更新都无法像Activity中直接更新View,这是因为两者都运行在其他大家进程中,确切来说是SystemService中,为了跨进程更新UI,RemoteViews提供了一系列的set方法,我们接下来就是来实际的演示了
1.RemoteViews在通知栏上的应用
首先来看下通知栏,我们先了解一下系统默认的样式
//系统默认样式
private void systemStyleNotirfication() {
Notification notification = new Notification();
notification.icon = R.mipmap.ic_launcher;
notification.tickerText = "Hello LiuGuiLin";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, TestActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
notification.setLatestEventInfo(this, "LiuGuiLin", "This is Notification", pendingIntent);
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notificationManager.notify(1, notification);
}
上述的代码会弹出一个系统默认样式的通知栏,单击之后打开TestActivity,看下实际运行效果
虽说如此,但是往往有些需求是不满足的,所以需要自定义了,自定义其实也很简单,我们看下实际代码
//自定义通知栏
private void viewStyleNotirfication() {
Notification notification = new Notification();
notification.icon = R.mipmap.ic_launcher;
notification.tickerText = "Hello LiuGuiLin";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, TestActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notification_item);
remoteViews.setTextViewText(R.id.tv_title, "This is Title");
remoteViews.setTextViewText(R.id.tv_content, "This is Notification Content");
remoteViews.setImageViewResource(R.id.iv_img, R.mipmap.ic_launcher);
remoteViews.setOnClickPendingIntent(R.id.ll_open, pendingIntent);
notification.contentView = remoteViews;
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notificationManager.notify(2, notification);
}
我们实际看下运行的结果
可以看到,这是我自己写的布局notification_item
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_open"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingLeft="10dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black" />
</LinearLayout>
</LinearLayout>
这就是非常简单的通知栏了
RemoteViews的使用也是非常的简单,只要提供当前应用的包名和布局文件的资源ID就可以创建一个RemoteViews了,如何更新呢?这一点和View还是有很大的不同,RemoteViews更新的时候,无法直接访问里面的View,必须通过RemoteViews所提供的一系列方法来更新,比如设置TextView,那就需要
remoteViews.setTextViewText(R.id.tv_title, "This is Title");
这里一共两个参数,一个是id,一个就是内容了,图片也是类似
remoteViews.setImageViewResource(R.id.iv_img, R.mipmap.ic_launcher);
如果需要点击事件的话,需要setOnClickPendingIntent来触发了,关于PendingIntent,他表示点击就是一种待定的意图,触发后才会操作,我们后面具体介绍
2.RemoteViews 在桌面小部件的应用
AppWidgetProvider是Android提供给我们的用于实现桌面小部件的类,其本质也就是一个广播,,所以实际使用中把他看成一个广播即可,我们来看下怎么去具体的实现一个小部件
1.定义小部件的界面
在res/layout下我们先写个widget.xml这里就是小部件的视图了,所以你尽量的随便写
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/iv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />
<ImageView
android:id="@+id/iv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tv_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Test"
android:textSize="20dp" />
</LinearLayout>
这里我写了两个图片和一个文本
2.定义小部件配置信息
在res/xml中定义一个appwidget_info.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget"
android:minHeight="84dp"
android:minWidth="84dp"
android:updatePeriodMillis="86400000">
</appwidget-provider>
上面的几个参数的含义很明确,android:initialLayout就是加载布局其他两个就是最小的高宽,而updatePeriodMillis就是更新小组件的时间周期
3.定义小组件的实现类
这个类需要继承AppWidgetProvider
package com.liuguilin.remotrview.provider;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.os.SystemClock;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;
import com.liuguilin.remotrview.R;
/*
* 项目名: RemoteViewsSample
* 包名: com.liuguilin.remotrview.provider
* 文件名: AppWidgetImpl
* 创建者: LiuGuiLin
* 创建时间: 2017/1/14 0014 下午 4:35
* 描述: 小组件
*/
public class AppWidgetImpl extends AppWidgetProvider {
public static final String TAG = "AppWidgetImpl";
public static final String CLICK_ACTION = "com.liuguilin.remotrview.provider.click_action";
private AppWidgetManager appWidgetManage;
private float degree;
private Bitmap bitmap;
public AppWidgetImpl() {
super();
}
@Override
public void onReceive(final Context context, final Intent intent) {
super.onReceive(context, intent);
String action = intent.getAction();
Log.i(TAG, "action:" + action);
if (CLICK_ACTION.equals(action)) {
Toast.makeText(context, "click it", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);
appWidgetManage = AppWidgetManager.getInstance(context);
for (int i = 0; i < 37; i++) {
degree = (i * 10) % 360;
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
remoteViews.setImageViewBitmap(R.id.iv1, rotateBitmap(bitmap));
remoteViews.setImageViewBitmap(R.id.iv2, rotateBitmap(bitmap));
Intent intentClick = new Intent(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.iv1, pendingIntent);
appWidgetManage.updateAppWidget(new ComponentName(context, AppWidgetImpl.class), remoteViews);
SystemClock.sleep(30);
}
}
}).start();
}
}
//每次更新都会调用
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.i(TAG, "onUpdate");
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
Intent intentClick = new Intent(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.iv1, pendingIntent);
appWidgetManage.updateAppWidget(new ComponentName(context, AppWidgetImpl.class), remoteViews);
}
//动画
private Bitmap rotateBitmap(Bitmap bitmap) {
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree);
Bitmap temBitmap = Bitmap.createBitmap(this.bitmap, 0, 0, this.bitmap.getWidth(), this.bitmap.getHeight(), matrix, true);
return temBitmap;
}
}
上述的代码实现了一个简单的桌面小部件,这里加了个动画,其他到没什么。我们需要注册才能使用
4.在清单文件中声明小部件
<receiver android:name=".provider.AppWidgetImpl">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info" />
<intent-filter>
<action android:name="com.liuguilin.remotrview.provider.click_action" />
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</receiver>
上面的代码有两个Action,其中第一个是识别小部件的动作,第二个就是他的标识,必须存在,这是系统的规范
AppWidgetProvider 除了最常用的onUpdate方法,还有其他的方法,onEnable,onDisabled,onDeleted以及onReceiver,这些方法都会被onReceiver在适当的时候调用,所以含义如下
- onEnable:当该窗口小部件第一次添加到桌面的时候调用该方法,可添加多次但是只有第一次调用
- onUpdate:小部件被添加或者第一次更新的时候都会调用一次该方法,小部件的更新机制由updatePeriodMillis来指定
- onDeleted:没删除一次小部件都会调用
- onDisabled:当最后一个该类型的小部件会删除时调用
- onReceiver:广播内置的方法
关于AppWidgetProvider 的onReceiver方法的具体分发过程,可以看源码中的实现,如下图,通过下面的代码可以看出,onReceiver会根据不同的Action来分别调用这几个方法
public void onReceive(Context context, Intent intent) {
// Protect against rogue update broadcasts (not really a security issue,
// just filter bad broacasts out so subclasses are less likely to crash).
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (appWidgetIds != null && appWidgetIds.length > 0) {
this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
}
}
} else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
this.onDeleted(context, new int[] { appWidgetId });
}
} else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
&& extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
appWidgetId, widgetExtras);
}
} else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.onEnabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (oldIds != null && oldIds.length > 0) {
this.onRestored(context, oldIds, newIds);
this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
}
}
}
}
上面描述了一个开发桌面小部件的完整过程 ,例子比较简单,实际开发过程中会稍微的复杂一些,但是开发流程都是一样的, 可以发现,桌面小程序在界面上的操作都是通过RemoteViews,不管小部件的界面初始化还是界面的更新都需要依赖他
3.PendingIntent概述
我们在前面多次提到了PendingIntent,归根结底,他和Intent有什么区别呢我们就来说下
顾名思义,PendingIntent表示处于一种等待的意图,即特定,等待的意思,就是说你要在某种条件下才会触发,所以我们很容易的就联想到RemoteViews了
PendingIntent有三种待定的意图,就是Activity,Service,广播,可以看如下图
如图中所示,这三个方法的参数都是一样的,主要理解的是第二个参数requstCode和第四个参数flags,code代表的是发送码,多数情况下为0,而且code会影响到flag,flag常见的有几种我们下面会说,其实最主要是理解匹配规则,
PendingIntent的匹配规则为:如果两个PendingIntent他们内部的Intent相同并且requstCode也相同的话,那么PendingIntent就是相同的,code比较好理解,那什么情况下Intent相同呢,Intent的匹配规则是:如果两个Intent的ComponentName的匹配过程,只要Intent之间的ComponentName和intent-filter相同,那么这两个intent就相同,需要注意的是Extras不参与匹配过程,只要intent之间的name和intent-filter相同就行,我们再来说下flags的参数含义
FLAG_ONE_SHOT
当前描述的PendingIntent只能被使用一次,然后他就会被cancel,如果后续还有相同的PendingIntent,那么他的send方法就会失败,对于通知栏的消息来说,如果采用此标记位,那么同类的通知只能使用一次,后续将无法打开FLAG_NO_CREATE
当前描述的PendingIntent不会主动去创建,如果当前PendingIntent之前不存在,那么getActivity等方法都会直接返回null,即获取PendingIntent失败,这个标记位很少见,他无法单独使用,因此在日常开发当中,并没有太多的意义,这里就不过多的介绍了FLAG_CANCEL_CURRENT
当前描述的PendingIntent如果已经存在,那么就会被cancel,然后系统创建一个新的PendingIntent,对于通知栏来说,那些被cancel的消息将无法被打开FLAG_UPDATE_CURRENT
当前描述的PendingIntent如果已经存在的话,那么他们就会被更新,他们的intent中的extras会被替换成新的
从上面的分析看还是不太好理解这四个标记位,下面结合实际的项目来,这里分析两种情况,如下代码,如果notify的第一个参数id是常量,那么多次通知就只能弹出一个通知,后续会把前面的替换掉,如果每次不一样,就会多弹出几个通知
如果notify方法的id是常量,那么不管PendingIntent是否匹配,后面的通知会替换前面的通知,这个很好理解
如果notify方法的id每次不同,那么当PendingIntent不匹配时,这里的匹配是指PendingIntent中Intent相同切requstCode相同,在这种情况下不管采用了何种标记位,这些通知之间不互相干扰。如果PendingIntent处于匹配状态,这个时候要分情况讨论,如果采用FLAG_ONE_SHOT标记位,那么后续通知中,PendingIntent会和第一条通知保持一致,包括其中的Extras,单击任何一条通知,剩余的都无法打开当所有的通知被清除后,会重复这个过程,如果采用FLAG_CANCEL_CURRENT标记位,那么只有最新的通知可以打开,之前弹出的所有通知均无法打开,如果采用FLAG_UPDATE_CURRENT标记位。那么之前弹出的通知PendingIntent会被更新,最终他们和最新的一条通知保持一致,包括其中的Extras,那么这些通知都可以被打开
二.RemoteViews内部机制
RemoteViews的作用在其他进程中显示并且更新View的界面,为了更好的理解他的内部机制,我们来看一下他的主要功能,首先我们看一下他的构造方法,这里介绍一个最常用的构造方法
public RemoteViews(String packageName, int layoutId) {
this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
}
他接受两个参数,第一个表示当前的包名,第二个是加载的布局,这个很好理解,RemoteViews目前并不能支持所有的View类型,我们看下他支持哪些
Layout
FrameLayout LinearLayout RelativeLayout GridLayoutView
AnalogClock,Button,Chronometer,ImageButton,ImageView,ProgressBar,TextView,ViewFlipper ListView,GridView,stackView,AdapterViewFlipper,ViewStub
上面的描述是RemoteViews锁支持的View类,RemoteViews不支持他们的子类以及其他View的类型,也就是说RemoteViews中不能使用除了上述列表中以外的View,那么通知栏消息无法将弹出并且会抛出异常
RemoteViews也没有提供findViewById方法,因此无法直接访问里面的View元素,而必须通过RemoteViews所提供的一系列set方法来完成,当然这是因为RemoteViews在远程进程中显示,所以没办法直接findViewById,关于set方法,可以看下这表
从这张表可以看出,原本可以直接调用View的方法,现在都需要通过set来完成,而从这些方法的声明来看,很像是通过反射来完成的,事实上也是如此
下面我们继续来说下RemoteViews的内部机制,由于RemoteViews主要用于通知栏和通知栏和桌面小部件,这里说一下他的工作过程,我们知道小部件分别由NotificationManager和AppWidgetProvider管理,而NotificationManager和AppWidgetProvider通过Binder分别为SystemService进程中的NotificationManagerService和AppWidgetService,由此可见,通知栏和小部件实际上是这两个中加载出来的,这就是我们的进程构成了跨进程通信的原理
首先RemoteViews会通过Binder传递到SystemServer进程,这是因为RemoteViews实现了Parcelable接口,因此它可以跨进程传输,系统会根据RemoteViews中的包名等信息去得到该应用的资源。然后会通过Layoutinflater去加载RemoteViews中的布局文件,在SystemServer进程中加载后的布局文件是一个普通的View,只不过相对于我们的进程他是一个RemoteViews而已。接着系统会对View执行一系列界面更新任务,这些任务就是之前我们通过set方法来提交的。set方法对View所做的更新并不是立刻执行的,在RemoteViews内部会记录所有的更新操作,具体的执行时机要等到RemoteViews被加载以后才能执行,这样RemoteViews就可以在SystemServer进程中显示了,这就是我们所看到的通知栏消息或者桌面小部件。当需要更新RemoteViews时,我们需要调用一系列set法并通过NotificationManager和AppWidgetManager来提交更新任务,具体的更新操作也是在SystemServer进程中完成的。
从理论上来说,系统完全可以通过Binder去支持所有的View和View操作,但是这样做的话代价太大,因为View的方法太多了,另外就是大量的IPC操作会影响效率。为了解决这个问题,系统并没有通过Binder去直接支持View的跨进程访问,而是提供了一个Action,Action代表一个View操作,Action同样实现了Parcelable接口。系统首先将Vew操作封装装到Action对象并将这些对象跨进程传输到远程进程,接着在远程进程中执行Action对象中对象,当我们通过NotificationManager和AppWidgetManager来提交我们的更新时,这些Action对象就会传输到远程进程并在远程进程中依次执行,这个过程可以参看下面的图片,远程进程通过RemoteViews的apply方法来进行View的更新操作,RemoteViews的apply方法内部则回去遍历所有的Action对象并调用它们的apply方法,具体的View更新操作是由Acton对apply方法来完成的。上述做法的好处是显而易见的,首先不需要定义大量的Binder,其次通过在远程进程中批量执行RemoteViews的修改操作从而避免了大量的IPC视作这就提高了程序的性能,由此可见,Android系统在这方面的设计的确很精妙。
上面从理论上分析了RemoteViews的内部机制,接下来我们从源码的角度分析下Remoteviews的工作流程。它的构造方法就不用多说了,这里我们首先看他一系列的set方法,比如setTextViewText
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
上述ID中,viewId是被操作的View的id,setText是一个方法名,text是给TextView要设置的文本,可以联想一下,是不是清晰了很多,我们再来看下setCharSequence方法
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
从setCharSequence的实现来看,他的内部并没有对View进行直接的处理,然是添加了一个ReflectionAction独享,从名字来看,这应该是一个反射的动作,再看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);
}
从上述代码可以得知RemoteViews内部有一个mActions 成员,他是一个ArrayList,外界每一次调用一个set方法,他都能保存下来并未对view进行实际的操作,这一点从上面的理论分析就可以知道,我们继续看,我们现在看一下apply的实现以及Action类的实现
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result;
// RemoteViews may be built by an application installed in another
// user. So build a context that loads resources from that user but
// still returns the current users userId so settings like data / time formats
// are loaded without requiring cross user persmissions.
final Context contextForResources = getContextForResources(context);
Context inflationContext = new ContextWrapper(context) {
@Override
public Resources getResources() {
return contextForResources.getResources();
}
@Override
public Resources.Theme getTheme() {
return contextForResources.getTheme();
}
};
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Clone inflater so we load resources from correct context and
// we don't add a filter to the static version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
rvToApply.performApply(result, parent, handler);
return result;
}
从上面的代码可以看出,首先会通过LayoutInflater 去加载RemoteViews中的布局文件,RemoteViews中的布局文件可以通过getLayoutId这个方法获得,加载完布局之后会通过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);
}
}
}
performApply的实现是比较好理解的,他的作用就是遍历mActions 这个列表并执行每一个Action对象的apply方法,还记得mAction吗?每一次的set操作都会对应他里面的一个Action对象,因此也可以断定,Action对象的apply方法就是真正操作View的地方,实际上也是如此
RemoteViews在通知栏和桌面小部件中的工作过程和上面描述的过程是一致的,当我们调用RemoteViews的set方法时,并不会立刻更新它们的界面,而必须要通过NotificationManager的notify方法以及AppWidgetManager的updateAppWidget才能更新它们的界面。实际上在AppWidgetManager的 updateAppWidget的内部实现中,它们的确是通过RemoteViews的apply以及reapply方法来加载或者更新界面的,apply和reApply的区别在于:apply会加载布局并更新界面,而reApply则只会更新界面。通知栏和桌面小插件在初始化界面时会调用apply方法,而在后续的更新界面时则会调用reapply方法。这里先看一
下 BaseStatusBar的updateNotificationViews方法中,如下所示。
private void updateNotificationViews(NotificationData.Entry entry,
StatusBarNotification notification, boolean isHeadsUp) {
final RemoteViews contentView = notification.getNotification().contentView;
final RemoteViews bigContentView = isHeadsUp
? notification.getNotification().headsUpContentView
: notification.getNotification().bigContentView;
final Notification publicVersion = notification.getNotification().publicVersion;
final RemoteViews publicContentView = publicVersion != null ? publicVersion.contentView
: null;
// Reapply the RemoteViews
contentView.reapply(mContext, entry.expanded, mOnClickHandler);
if (bigContentView != null && entry.getBigContentView() != null) {
bigContentView.reapply(mContext, entry.getBigContentView(),
mOnClickHandler);
}
if (publicContentView != null && entry.getPublicContentView() != null) {
publicContentView.reapply(mContext, entry.getPublicContentView(), mOnClickHandler);
}
// update the contentIntent
final PendingIntent contentIntent = notification.getNotification().contentIntent;
if (contentIntent != null) {
final View.OnClickListener listener = makeClicker(contentIntent, notification.getKey(),
isHeadsUp);
entry.row.setOnClickListener(listener);
} else {
entry.row.setOnClickListener(null);
}
entry.row.setStatusBarNotification(notification);
entry.row.notifyContentUpdated();
entry.row.resetHeight();
}
很显然,上述代码表示当通知栏需要更新的时候,会通过RemoteViews的reapply方法来更新
接下来我们看一下AppWidgetHostView的updateAppWidget方法
public void updateAppWidget(int[] appWidgetIds, RemoteViews views) {
if (mService == null) {
return;
}
try {
mService.updateAppWidgetIds(mPackageName, appWidgetIds, views);
}
catch (RemoteException e) {
throw new RuntimeException("system server dead?", e);
}
}
从上面的代码,我们是通过IAppWidgetService的接口去实现的,实际上最红也是通过RemoteViews的reapply方法来更新
了解了apply和reapply的作用之后我们继续看Action的子类实现,首先看下ReflectionAction的具体实现,他的源码如下:
private final class ReflectionAction extends Action {
static final int TAG = 2;
static final int BOOLEAN = 1;
static final int BYTE = 2;
static final int SHORT = 3;
static final int INT = 4;
static final int LONG = 5;
static final int FLOAT = 6;
static final int DOUBLE = 7;
static final int CHAR = 8;
static final int STRING = 9;
static final int CHAR_SEQUENCE = 10;
static final int URI = 11;
// BITMAP actions are never stored in the list of actions. They are only used locally
// to implement BitmapReflectionAction, which eliminates duplicates using BitmapCache.
static final int BITMAP = 12;
static final int BUNDLE = 13;
static final int INTENT = 14;
static final int COLOR_STATE_LIST = 15;
String methodName;
int type;
Object value;
ReflectionAction(int viewId, String methodName, int type, Object value) {
this.viewId = viewId;
this.methodName = methodName;
this.type = type;
this.value = value;
}
ReflectionAction(Parcel in) {
this.viewId = in.readInt();
this.methodName = in.readString();
this.type = in.readInt();
//noinspection ConstantIfStatement
if (false) {
Log.d(LOG_TAG, "read viewId=0x" + Integer.toHexString(this.viewId)
+ " methodName=" + this.methodName + " type=" + this.type);
}
// For some values that may have been null, we first check a flag to see if they were
// written to the parcel.
switch (this.type) {
case BOOLEAN:
this.value = in.readInt() != 0;
break;
case BYTE:
this.value = in.readByte();
break;
case SHORT:
this.value = (short)in.readInt();
break;
case INT:
this.value = in.readInt();
break;
case LONG:
this.value = in.readLong();
break;
case FLOAT:
this.value = in.readFloat();
break;
case DOUBLE:
this.value = in.readDouble();
break;
case CHAR:
this.value = (char)in.readInt();
break;
case STRING:
this.value = in.readString();
break;
case CHAR_SEQUENCE:
this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
break;
case URI:
if (in.readInt() != 0) {
this.value = Uri.CREATOR.createFromParcel(in);
}
break;
case BITMAP:
if (in.readInt() != 0) {
this.value = Bitmap.CREATOR.createFromParcel(in);
}
break;
case BUNDLE:
this.value = in.readBundle();
break;
case INTENT:
if (in.readInt() != 0) {
this.value = Intent.CREATOR.createFromParcel(in);
}
break;
case COLOR_STATE_LIST:
if (in.readInt() != 0) {
this.value = ColorStateList.CREATOR.createFromParcel(in);
}
break;
default:
break;
}
}
public void writeToParcel(Parcel out, int flags) {
out.writeInt(TAG);
out.writeInt(this.viewId);
out.writeString(this.methodName);
out.writeInt(this.type);
//noinspection ConstantIfStatement
if (false) {
Log.d(LOG_TAG, "write viewId=0x" + Integer.toHexString(this.viewId)
+ " methodName=" + this.methodName + " type=" + this.type);
}
// For some values which are null, we record an integer flag to indicate whether
// we have written a valid value to the parcel.
switch (this.type) {
case BOOLEAN:
out.writeInt((Boolean) this.value ? 1 : 0);
break;
case BYTE:
out.writeByte((Byte) this.value);
break;
case SHORT:
out.writeInt((Short) this.value);
break;
case INT:
out.writeInt((Integer) this.value);
break;
case LONG:
out.writeLong((Long) this.value);
break;
case FLOAT:
out.writeFloat((Float) this.value);
break;
case DOUBLE:
out.writeDouble((Double) this.value);
break;
case CHAR:
out.writeInt((int)((Character)this.value).charValue());
break;
case STRING:
out.writeString((String)this.value);
break;
case CHAR_SEQUENCE:
TextUtils.writeToParcel((CharSequence)this.value, out, flags);
break;
case URI:
out.writeInt(this.value != null ? 1 : 0);
if (this.value != null) {
((Uri)this.value).writeToParcel(out, flags);
}
break;
case BITMAP:
out.writeInt(this.value != null ? 1 : 0);
if (this.value != null) {
((Bitmap)this.value).writeToParcel(out, flags);
}
break;
case BUNDLE:
out.writeBundle((Bundle) this.value);
break;
case INTENT:
out.writeInt(this.value != null ? 1 : 0);
if (this.value != null) {
((Intent)this.value).writeToParcel(out, flags);
}
break;
case COLOR_STATE_LIST:
out.writeInt(this.value != null ? 1 : 0);
if (this.value != null) {
((ColorStateList)this.value).writeToParcel(out, flags);
}
default:
break;
}
}
private Class<?> getParameterType() {
switch (this.type) {
case BOOLEAN:
return boolean.class;
case BYTE:
return byte.class;
case SHORT:
return short.class;
case INT:
return int.class;
case LONG:
return long.class;
case FLOAT:
return float.class;
case DOUBLE:
return double.class;
case CHAR:
return char.class;
case STRING:
return String.class;
case CHAR_SEQUENCE:
return CharSequence.class;
case URI:
return Uri.class;
case BITMAP:
return Bitmap.class;
case BUNDLE:
return Bundle.class;
case INTENT:
return Intent.class;
case COLOR_STATE_LIST:
return ColorStateList.class;
default:
return null;
}
}
@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);
}
}
public int mergeBehavior() {
// smoothScrollBy is cumulative, everything else overwites.
if (methodName.equals("smoothScrollBy")) {
return MERGE_APPEND;
} else {
return MERGE_REPLACE;
}
}
public String getActionName() {
// Each type of reflection action corresponds to a setter, so each should be seen as
// unique from the standpoint of merging.
return "ReflectionAction" + this.methodName + this.type;
}
}
通过上述的代码可以看出,ReflectionAction 表示的是一个反射的动作,他通过对View的操作会反射的方式调用,其中getMethod就是根据方法名来反射所需要的方法,其中ReflectionAction 也有很多的set方法,除了ReflectionAction 还有Action,等,这里我们拿TextViewSizeAction来分析,具体实现如下
private class TextViewSizeAction extends Action {
public TextViewSizeAction(int viewId, int units, float size) {
this.viewId = viewId;
this.units = units;
this.size = size;
}
public TextViewSizeAction(Parcel parcel) {
viewId = parcel.readInt();
units = parcel.readInt();
size = parcel.readFloat();
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(TAG);
dest.writeInt(viewId);
dest.writeInt(units);
dest.writeFloat(size);
}
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final TextView target = (TextView) root.findViewById(viewId);
if (target == null) return;
target.setTextSize(units, size);
}
public String getActionName() {
return "TextViewSizeAction";
}
int units;
float size;
public final static int TAG = 13;
}
他的实现是比较简单的,他之所以不用反射来实现,是因为setTextView这个方法有灵感参数,因此无法复用ReflectionAction,因为ReflectionAction 的反射调用只需要一个参数,这里就不一一分析了
关于单击事件,RemoteView中只支持PendingIntent,是不支持其他模式的,我们这里需要注意的就是setOnClickPendingIntent,setPendingIntentTemplate,以及setonClickFillinIntent的区别,首先setOnClickPendingIntent用于给普通view设置单击事件,但是不能给ListView之类的View设置,如果需要,就用后两者
三.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 BActivity extends Activity implements View.OnClickListener {
private Button btn_send;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_b);
initView();
}
private void initView() {
btn_send = (Button) findViewById(R.id.btn_send);
btn_send.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_send:
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_item);
remoteViews.setTextViewText(R.id.textView1, "Hello");
remoteViews.setImageViewResource(R.id.imageview1, R.mipmap.ic_launcher);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, TestActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.imageview1, pendingIntent);
Intent intent = new Intent("send_bro");
sendBroadcast(intent);
break;
}
}
}
A的代码比较简单只是接收一个广播就行
public class AActivity extends Activity {
private LinearLayout mLinearLayout;
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
RemoteViews remoteViews = intent.getParcelableExtra("send_bro");
if (remoteViews != null) {
updateUI(remoteViews);
}
}
};
private void updateUI(RemoteViews remoteViews) {
View view = remoteViews.apply(this, mLinearLayout);
mLinearLayout.addView(view);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_a);
initView();
}
private void initView() {
mLinearLayout = (LinearLayout) findViewById(R.id.mLinearLayout);
IntentFilter intent = new IntentFilter("send_bro");
registerReceiver(mBroadcastReceiver, intent);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mBroadcastReceiver);
}
}
上述代码很简单,除了注册和解除广播以外,最主要的逻辑其实就是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, mLinearLayout);
mLinearLayout.addView(view);
这种写法在同一个应用的多进程情形下是适用的,但是如果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);
到这里,这章也就结束了,理解的很深收益很大,这本书真的不错哦