不为人知的 RemoteViews

可能标题党了,应该是我们在日常开发中很少会接触到它,这篇文章将首先介绍一下 RemoteViews 在通知栏和桌面小部件的的应用,然后会分析 RemoteViews 的内部机制。那么什么是 RemoteViews 呢? RemoteViews 表示的是一个 View 的结构,它的功能就是用来跨进程更新界面,主要的应用就是桌面小组件和通知栏。
RemoteViews 在实际开发中主要应用在通知栏和桌面小组件。通知栏主要通过 NotificationManager 的 notify 方法实现,它既可以使用默认效果也可以自定义布局。桌面小部件则通过 AppWidgetProvider 实现, Approvider 本质上是一个广播。那么为什么它们更新界面都需要 RemoteViews 呢?那是因为二者的界面都运行在其他进程中(SystemServer进程)。因此他们的界面更新都需要 RemoteViews 。

RemoteViews 的应用实例

RemoteViews 在通知栏上的应用

首先我们提供一个默认通知栏的示例,下列代码会弹出一个系统默认样式的通知,点击后会打开 MainActivity 并消除自身。接下来我们自定义一个通知样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentTitle("这是通知的标题");
builder.setContentText("这是通知内容");
builder.setAutoCancel(true);

Intent intent = new Intent(this, MainActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(intent);
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(resultPendingIntent);
NotificationManager mNotificationManager =(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// mId allows you to update the notification later on.
mNotificationManager.notify(mId, builder.build());

下面我们利用 RemoteViews 给通知添加一个自定义界面。我们的布局很简单就一张图片。

1
2
3
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteViews.setImageViewResource(R.id.iv_img, R.mipmap.img);
builder.setContent(remoteViews);

可以看到 RemoteViews 的使用非常简单,只要提供当前的应用包名和布局即可创建一个 RemoteViews实例。可以看到要更新 RemoteViews 不能通过 findViewById 这种方式。而需要通过 RemoteViews 提供的一些set方法,具体内容大家可以自行查阅相关资料。

RemoteViews 在桌面小部件上的使用

AppProvider 是专门用来实现桌面小部件的类,其本质是一个广播。下面简单介绍一下桌面小部件的开发步骤。

定义小部件界面

界面布局比较简单,就一张图片,我们就不贴源码了。

定义小部件配置信息

在res/xml下新建名称任意的xml文件,如下所示:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/layout_widget"
android:minHeight="48dp"
android:minWidth="48dp"
android:updatePeriodMillis="86400000">

</appwidget-provider>

上面几个参数都比较简单,其中 updatePeriodMillis 定义了小工具的自动更新周期,其以毫秒为单位。

定义小部件的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class TestAppProvider extends AppWidgetProvider {
public static final String TAG = "TestAppProvider";
public static final String CLICK_ACTION = "com.weiqianghu.remoteviews.action.click";

@Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
if (CLICK_ACTION.equals(intent.getAction())) {
new Thread(new Runnable() {
@Override
public void run() {
Bitmap srcBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.img);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
for (int i = 0; i < 37; i++) {
float degree = (i * 10) % 360;
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.layout_widget);
remoteViews.setImageViewBitmap(R.id.iv_img, rotateBitmap(context, srcBitmap, degree));

Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.iv_img, pendingIntent);
appWidgetManager.updateAppWidget(new ComponentName(context, TestAppProvider.class), remoteViews);
SystemClock.sleep(30);
}
}
}).start();
}
}

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);

final int counter = appWidgetIds.length;
for (int i = 0; i < counter; i++) {
int appWidgetId = appWidgetIds[i];
onWidgetUpdate(context, appWidgetManager, appWidgetId);
}
}

private void onWidgetUpdate(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.layout_widget);

Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.iv_img, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
}

private Bitmap rotateBitmap(Context context, Bitmap srcBitmap, float degree) {
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree);
Bitmap tmpBitmap = Bitmap.createBitmap(srcBitmap, 0, 0, srcBitmap.getWidth(), srcBitmap.getHeight(), matrix, true);
return tmpBitmap;
}
}

在 AndroidManifest.xml 中声明小部件

1
2
3
4
5
6
7
8
9
10
<receiver android:name=".TestAppProvider">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider_info">
</meta-data>
<intent-filter>
<action android:name="com.weiqianghu.remoteviews.action.click"/>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
</receiver>

做完了上述几部之后在桌面小部件列表就能找到我们刚才定义的小部件了,可以看到桌面小部件开发还是比较简单的。但是我们应该注意到桌面小部件的初始化和更新都要借助于 RemoteViews 。

PendingIntent 简述

PendingIntent 表示接下来在某个特定时刻有一个 Intent 发生。PendingIntent 的典型使用场景就是给 RemoteViews 添加点击事件。PendingIntent 可以启动 Activity , 发送广播,启动 Service。

RemoteViews 的内部机制

RemoteViews 的作用是在其他进程中显示并更新 View 界面。

构造方法

public RemoteViews(String packageName, int layoutId); 它接受两个参数,第一个表示当前应用的包名,第二个表示待加载的布局文件。其中布局文件支持的 Layout 有: FrameLayout 、 LinearLayout 、 RelativeLayout 、 GridLayout。 支持的常见 View 有:Button、ImageButton、ImageView、ProgressBar、TextView、ListView 、 GridView、ViewStub。RemoteViews不支持它们的子类以及其他View类型,当然不能支持自定义View。由于 RemoteView 在远程进程中显示所有不能直接利用 findViewById 方法进行访问,所有只能使用 RemoteViews 提供的一系列set方法,RemoteViews 提供的set方法使用比较简单,关于 RemoteViews 提供的具体set 方法大家可以查询相关资料。

RemoteViews 的内部机制

由于 RemoteViews 主要应用于桌面小组件和通知栏,因此我们也通过它们分析 RemoteViews 的内部机制。通知栏和桌面小部件分别由 NotificationManager 和 AppWidgetManager 管理,而 NotificationManager 和 AppWidgetManager 通过 Binder 分别和 SystemServer 进程中的 NotificationManagerService 以及 AppWidgetService 进行通信。
首先 RemoteViews 会通过 Binder 传递到 SystemServer 进程,系统会根据 RemoteViews 中的包名等信息去得到应用的资源,然后会通过 LayoutInflater 去加载 RemoteViews 的布局文件,对于 SystemServer 进程来说近加载后的布局文件就是一个普通的view,接着系统会根据 set 方法提交的更新任务去更新界面,其中之前利用 set 方法提交任务的时候并不是立刻执行,而是记录在了 RemoteViews 内部,其中任务是用 Action 封装的。这就是 RemoteViews 的内部机制,更详细的内容可以查阅相关源码。

参考资料:
Android 开发艺术探索 理解 RemoteViews