概述
RemoteViews顧名思義就是遠(yuǎn)程View,它表示的是一個(gè)View結(jié)構(gòu)凌摄,它可以在其他進(jìn)程中顯示浪秘,為了跨進(jìn)程更新它的界面,RemoteViews提供了一組基礎(chǔ)的操作來(lái)實(shí)現(xiàn)這個(gè)效果丁眼。RemoteViews在Android中的使用場(chǎng)景有兩種:通知欄和桌面小部件筷凤。
RemoteViews在通知欄上的應(yīng)用
我們知道通知欄除了默認(rèn)的效果外還支持自定義布局。
使用系統(tǒng)默認(rèn)的樣式彈出一個(gè)通知的方式如下:(android3.0之后)
private void showDefaultNotification() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
// 設(shè)置通知的基本信息:icon苞七、標(biāo)題藐守、內(nèi)容
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentTitle("My notification");
builder.setContentText("Hello World!");
builder.setAutoCancel(true);
// 設(shè)置通知的點(diǎn)擊行為:這里啟動(dòng)一個(gè) Activity
Intent intent = new Intent(this, SecondActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
// 發(fā)送通知 id 需要在應(yīng)用內(nèi)唯一
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(id, builder.build());
}
上述代碼會(huì)彈出一個(gè)系統(tǒng)默認(rèn)樣式的通知,單擊通知后會(huì)打開(kāi)SecondActivity同時(shí)會(huì)清除本身蹂风。效果如圖:
為了滿(mǎn)足個(gè)性化需求卢厂,我們還可能會(huì)用到自定義通知。實(shí)現(xiàn)自定義通知我們首先需要提供一個(gè)布局文件硫眨,然后通過(guò)RemoteViews來(lái)加載這個(gè)布局文件即可改變通知的樣式足淆。
private void showCustomNotification() {
RemoteViews remoteView;
// 構(gòu)建 remoteView
remoteView = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteView.setTextViewText(R.id.tvMsg, "哈shenhuniurou");
remoteView.setImageViewResource(R.id.ivIcon, R.mipmap.ic_launcher_round);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
// 設(shè)置自定義 RemoteViews
builder.setContent(remoteView).setSmallIcon(R.mipmap.ic_launcher);
// 設(shè)置通知的優(yōu)先級(jí)(懸浮通知)
builder.setPriority(NotificationCompat.PRIORITY_MAX);
Uri alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
// 設(shè)置通知的提示音
builder.setSound(alarmSound);
// 設(shè)置通知的點(diǎn)擊行為:這里啟動(dòng)一個(gè) Activity
Intent intent = new Intent(this, SecondActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
builder.setAutoCancel(true);
Notification notification = builder.build();
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(1001, notification);
}
效果如圖所示:
創(chuàng)建RemoteViews對(duì)象我們只需要知道當(dāng)前應(yīng)用包名和布局文件的資源id巢块,比較簡(jiǎn)單,但是要更新RemoteViews就不是那么容易了巧号,因?yàn)槲覀儫o(wú)法直接訪(fǎng)問(wèn)布局文件中的View族奢,而必須通過(guò)RemoteViews提供的特定的方法來(lái)更新View。比如設(shè)置TextView文本內(nèi)容需要用setTextViewText方法丹鸿,設(shè)置ImageView圖片需要通過(guò)setImageViewResource方法越走。也可以給里面的View設(shè)置點(diǎn)擊事件,需要使用PendingIntent并通過(guò)setOnClickPendingIntent方法來(lái)實(shí)現(xiàn)靠欢。之所以更新RemoteViews如此復(fù)雜廊敌,直接原因是因?yàn)镽emoteViews沒(méi)有提供跟View類(lèi)似的findViewById這個(gè)方法,我們無(wú)法獲取到RemoteViews中的子View门怪。
RemoteViews在桌面小部件上的應(yīng)用
現(xiàn)在我要實(shí)現(xiàn)的效果是這樣一個(gè)小部件:
AppWidgetProvider是Android中提供用于實(shí)現(xiàn)桌面小部件的類(lèi)骡澈,它的本質(zhì)其實(shí)是一個(gè)廣播。開(kāi)發(fā)桌面小部件的步驟:
定義小部件布局
在res/layout/下新建一個(gè)布局文件layout_widget.xml掷空,內(nèi)容命名根據(jù)需求自定肋殴。我在里面放了四個(gè)線(xiàn)程布局當(dāng)做按鈕,外面再套一層線(xiàn)性布局橫向排列坦弟。
<?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="wrap_content"
android:orientation="horizontal"
android:weightSum="4">
<LinearLayout
android:id="@+id/btn1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:src="@mipmap/ic_launcher_round" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:text="按鈕1"
android:textColor="@android:color/white" />
</LinearLayout>
<LinearLayout
android:id="@+id/btn2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:src="@mipmap/ic_launcher_round" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:text="按鈕2"
android:textColor="@android:color/white" />
</LinearLayout>
<LinearLayout
android:id="@+id/btn3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:src="@mipmap/ic_launcher_round" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:text="按鈕3"
android:textColor="@android:color/white" />
</LinearLayout>
<LinearLayout
android:id="@+id/btn4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:src="@mipmap/ic_launcher_round" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:text="按鈕4"
android:textColor="@android:color/white" />
</LinearLayout>
</LinearLayout>
定義小部件配置信息
在res/xml/下新建一個(gè)資源文件护锤,命名自定:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget"
android:minHeight="56dp"
android:minWidth="272dp"
android:previewImage="@mipmap/ic_launcher"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="100000"
android:widgetCategory="home_screen">
</appwidget-provider>
解釋下各個(gè)屬性的含義
android:initialLayout:指定小部件的初始化布局
android:minHeight:小部件最小高度
android:minWidth:小部件最小寬度
android:previewImage:小部件列表顯示的圖標(biāo)
android:updatePeriodMillis:小部件自動(dòng)更新的周期
android:widgetCategory:小部件顯示的位置,home_screen表示只在桌面上顯示
定義小部件的實(shí)現(xiàn)類(lèi)
package com.shenhuniurou.remoteviewsdemo;
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.widget.RemoteViews;
/**
* Created by Daniel on 2017/7/1.
*/
public class CustomAppWidgetProvider extends AppWidgetProvider {
public static final String CLICK_WEDGET_ONE = "com.shenhuniurou.appwidgetprovider.click.one";
public static final String CLICK_WEDGET_TWO = "com.shenhuniurou.appwidgetprovider.click.two";
public static final String CLICK_WEDGET_THREE = "com.shenhuniurou.appwidgetprovider.click.three";
public static final String CLICK_WEDGET_FOUR = "com.shenhuniurou.appwidgetprovider.click.four";
public CustomAppWidgetProvider() {
super();
}
@Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
String action = intent.getAction();
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
// 判斷action是否是自己定義的action
if (action.equals(CLICK_WEDGET_ONE)) {
// 點(diǎn)擊的是第一個(gè)按鈕
Intent firstIntent = Intent.makeRestartActivityTask(new ComponentName(context, MainActivity.class));
Intent secondIntent = new Intent(context, SecondActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivities(context, 0, new Intent[] { firstIntent, secondIntent }, PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.btn1, pendingIntent);
} else if (action.equals(CLICK_WEDGET_TWO)) {
// 點(diǎn)擊的是第二個(gè)按鈕
Intent clickIntent = new Intent(context, ThirdActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, clickIntent, 0);
remoteViews.setOnClickPendingIntent(R.id.btn2, pendingIntent);
} else if (action.equals(CLICK_WEDGET_THREE)) {
// 點(diǎn)擊的是第三個(gè)按鈕
Intent clickIntent = new Intent(context, ForthActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, clickIntent, 0);
remoteViews.setOnClickPendingIntent(R.id.btn3, pendingIntent);
} else if (action.equals(CLICK_WEDGET_FOUR)) {
// 點(diǎn)擊的是第四個(gè)按鈕
Intent clickIntent = new Intent(context, FifthActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, clickIntent, 0);
remoteViews.setOnClickPendingIntent(R.id.btn4, pendingIntent);
}
appWidgetManager.updateAppWidget(new ComponentName(context, CustomAppWidgetProvider.class), remoteViews);
}
/**
* 桌面小部件每次更新時(shí)調(diào)用的方法
* @param context
* @param appWidgetManager
* @param appWidgetIds
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
int count = appWidgetIds.length;
for (int i = 0; i < count; i++) {
int appWidgetId = appWidgetIds[i];
onWidgetUpdate(context, appWidgetManager, appWidgetId);
}
}
/**
* 更新桌面小部件
* @param context
* @param appWidgetManager
* @param appWidgetId
*/
private void onWidgetUpdate(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
Intent intent1 = new Intent(CLICK_WEDGET_ONE);
PendingIntent pendingIntent1 = PendingIntent.getBroadcast(context, 0, intent1, PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.btn1, pendingIntent1);
Intent intent2 = new Intent(CLICK_WEDGET_TWO);
PendingIntent pendingIntent2 = PendingIntent.getBroadcast(context, 0, intent2, PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.btn2, pendingIntent2);
Intent intent3 = new Intent(CLICK_WEDGET_THREE);
PendingIntent pendingIntent3 = PendingIntent.getBroadcast(context, 0, intent3, PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.btn3, pendingIntent3);
Intent intent4 = new Intent(CLICK_WEDGET_FOUR);
PendingIntent pendingIntent4 = PendingIntent.getBroadcast(context, 0, intent4, PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.btn4, pendingIntent4);
appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
}
}
在清單文件上聲明小部件
<receiver android:name=".CustomAppWidgetProvider">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/app_widget_provider_info" />
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="com.shenhuniurou.appwidgetprovider.click.one" />
<action android:name="com.shenhuniurou.appwidgetprovider.click.two" />
<action android:name="com.shenhuniurou.appwidgetprovider.click.three" />
<action android:name="com.shenhuniurou.appwidgetprovider.click.four" />
</intent-filter>
</receiver>
這里面meta-data標(biāo)簽中的name屬性是固定的android.appwidget.provider
酿傍,而resource屬性則是我們剛才新建的小部件的配置信息的xml烙懦,intent-filter中的android.appwidget.action.APPWIDGET_UPDATE是必須加的,它作為小部件的標(biāo)識(shí)存在赤炒,這是系統(tǒng)的規(guī)范氯析,否則這個(gè)receiver就不是一個(gè)桌面小部件,并且也無(wú)法出現(xiàn)在手機(jī)的小部件列表里可霎。下面其他的action分別對(duì)應(yīng)各個(gè)按鈕點(diǎn)擊的動(dòng)作魄鸦。
最后實(shí)現(xiàn)的效果圖:
總結(jié)下這個(gè)操作過(guò)程:當(dāng)小部件一被添加到桌面時(shí)會(huì)調(diào)用Provider中的onUpdate方法,在這個(gè)方法中我們會(huì)通過(guò)AppWidgetManager去更新小部件的界面癣朗,但是這個(gè)更新我們是沒(méi)辦法直接更新的拾因,而是通過(guò)RemoteViews來(lái)操作,setOnClickPendingIntent給每個(gè)按鈕設(shè)置了點(diǎn)擊時(shí)會(huì)發(fā)送的廣播動(dòng)作旷余,而在清單文件中我們聲明小部件時(shí)已經(jīng)將這些廣播動(dòng)作都加到intent-filter绢记,所以當(dāng)我們點(diǎn)擊桌面上該小部件中的某個(gè)按鈕時(shí),就會(huì)發(fā)送對(duì)應(yīng)的廣播正卧,而小部件監(jiān)聽(tīng)了這個(gè)廣播蠢熄,接收到廣播后再onReceive方法中根據(jù)動(dòng)作來(lái)分別處理點(diǎn)擊事件。當(dāng)然炉旷,對(duì)小部件的一些其他操作方法(比如onEnabled签孔、onDisabled叉讥、onDeleted)的廣播也會(huì)在onReceive中接收到,然后分發(fā)給不同的方法饥追。(我這里處理點(diǎn)擊事件用的也是RemoteViews的方式图仓,其實(shí)不必,直接使用context.startActivity即可但绕,但如果不是打開(kāi)頁(yè)面救崔,而是要更新小部件的界面,那么就需要繼續(xù)使用RemoteViews來(lái)更新了捏顺。)
PendingIntent
在上面實(shí)現(xiàn)小部件時(shí)我們多次使用到了PendingIntent六孵,這個(gè)東西顧名思義我們可以理解為將要發(fā)生的意圖,就是在某個(gè)待定的時(shí)刻會(huì)發(fā)生幅骄。所以它和Intent的區(qū)別就在于一個(gè)是立即執(zhí)行的一個(gè)是在未來(lái)某個(gè)時(shí)候執(zhí)行劫窒。PendingIntent典型的使用場(chǎng)景是通知中點(diǎn)擊通知時(shí)跳轉(zhuǎn)頁(yè)面,因?yàn)槲覀儾恢烙脩?hù)什么時(shí)候點(diǎn)擊昌执,另外就是給RemoteViews添加單擊事件烛亦,因?yàn)镽emoteViews運(yùn)行在遠(yuǎn)程進(jìn)程中诈泼,所以它不同于普通的View懂拾,不能想View那樣通過(guò)setOnClickListener方法來(lái)給設(shè)置單擊事件,想要給RemoteViews設(shè)置點(diǎn)擊事件铐达,就必須使用PendingIntent岖赋,通過(guò)setOnClickPendingIntent方法來(lái)設(shè)置。PendingIntent是通過(guò)send和cancel方法來(lái)發(fā)送和取消待執(zhí)行的Intent瓮孙。
PendingIntent支持三種待定意圖唐断,啟動(dòng)activity(常見(jiàn)的通知)、啟動(dòng)Service和發(fā)送廣播杭抠。它的主要方法有下面這些:
啟動(dòng)Activity它有兩種脸甘,啟動(dòng)單個(gè)和啟動(dòng)多個(gè),當(dāng)使用getActivities時(shí)偏灿,實(shí)際上啟動(dòng)的是Intent數(shù)組中最后一個(gè)activity丹诀,如果要讓最后一個(gè)activity返回時(shí)不退出app而是退回到上一個(gè)activity,實(shí)現(xiàn)方式可參照我上面第一個(gè)按鈕的點(diǎn)擊處理翁垂。
getActivity铆遭、getService、getBroadcast這三個(gè)方法的參數(shù)意義都是相同的沿猜,第一個(gè)上下文枚荣,第三個(gè)待定的意圖,第二個(gè)requestCode表示PendingIntent發(fā)送方的請(qǐng)求碼啼肩,多數(shù)情況下設(shè)置為0即可橄妆,另外requestCode會(huì)影響到第四個(gè)參數(shù)flags的效果衙伶。flags這個(gè)標(biāo)志位表示執(zhí)行效果。
常見(jiàn)的flags類(lèi)型有FLAG_ONE_SHOT害碾、FLAG_NO_CREATE痕支、FLAG_CANCEL_CURRENT、FLAG_UPDATE_CURRENT蛮原。要理解這四個(gè)標(biāo)志位的含義和區(qū)別卧须,我們首先要弄明白PendingIntent的匹配規(guī)則,也就是什么情況下PendingIntent是相同的儒陨。
匹配規(guī)則:
- 如果兩個(gè)PendingIntent它們內(nèi)部的Intent相同花嘶,且requestCode也相同,那么這兩個(gè)PendingIntent就是相同的蹦漠;
Intent相同的情況:
- 如果兩個(gè)Intent的ComponentName和intent-filter都相同椭员,那么這兩個(gè)Intent就是相同的。(Extras不參與Intent的匹配過(guò)程笛园,就是它不同隘击,只要ComponentName和intent-filter相同,Intent都算相同的研铆。)
FLAG_ONE_SHOT:表示當(dāng)前描述的PendingIntent只能被使用一次埋同,然后它就會(huì)自動(dòng)cancel,如果后續(xù)還有相同的PendingIntent棵红,那么它們的send方法就會(huì)調(diào)用失敗凶赁。如果通知欄消息使用這種標(biāo)記位,同類(lèi)型的通知就只會(huì)被打開(kāi)一次逆甜,后續(xù)的通知將無(wú)法點(diǎn)開(kāi)虱肄。
FLAG_NO_CREATE:表示當(dāng)前描述的PendingIntent不會(huì)主動(dòng)創(chuàng)建,如果當(dāng)前PendingIntent之前不存在交煞,那么getActivities等這些方法會(huì)直接返回null咏窿,獲取PendingIntent失敗。它無(wú)法單獨(dú)使用素征。
FLAG_CANCEL_CURRENT:表示當(dāng)前描述的PendingIntent如果已經(jīng)存在集嵌,就cancel它,然后系統(tǒng)會(huì)創(chuàng)建一個(gè)新的稚茅。
FLAG_UPDATE_CURRENT:表示當(dāng)前描述的PendingIntent如果已經(jīng)存在纸淮,那么它會(huì)被更新,內(nèi)部的Intent中的Extras也會(huì)被更新亚享。
RemoteViews的內(nèi)部機(jī)制
RemoteViews的構(gòu)造方法很多咽块,我們最常見(jiàn)的一個(gè)是
public RemoteViews(String packageName, int layoutId) {
this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
}
只需要包名和待加載的資源文件id,它并不能支持所有類(lèi)型的View欺税,也不支持自定義的View侈沪,它能支持的類(lèi)型如下:
Layout:FrameLyout揭璃、LinearLayout、RelativeLayout亭罪、GridLayout
View:Button瘦馍、ImageView、ImageButton应役、ProgressBar情组、TextView、ListView箩祥、GridView院崇、StackView、ViewStub袍祖、AdapterViewFlipper底瓣、ViewFlipper、AnalogClock蕉陋、Chronometer捐凭。
如果我們?cè)赗emoteViews中使用了它不支持的View不如EditText,那么就會(huì)發(fā)生異常凳鬓。
我們看看RemoteViews的set方法
從這些方法中看出茁肠,原本可以直接調(diào)用的View的方法,現(xiàn)在要通過(guò)RemoteViews的一系列set方法來(lái)完成村视。
我們知道官套,通知欄和桌面小部件分別由NotificationManager和AppWidgetManager來(lái)管理的,而NotificationManager和AppWidgetManager是通過(guò)Binder分別和SystemServer進(jìn)程中的NotificationManagerService以及AppWidgetService進(jìn)行通信蚁孔,因此,通知欄和桌面小部件中的布局文件實(shí)際上是在NotificationManagerService和AppWidgetService中被加載的惋嚎,而他們運(yùn)行在SystemServer中杠氢,這其實(shí)已經(jīng)和我們自己的app進(jìn)程構(gòu)成了跨進(jìn)程通信。
理論分析
首先RemoteViews會(huì)通過(guò)Binder傳遞到SystemServer進(jìn)程另伍,因?yàn)镽emoteViews實(shí)現(xiàn)了Parcelable接口鼻百,可以跨進(jìn)程傳輸,系統(tǒng)會(huì)根據(jù)RemoteViews中的包名等信息去獲取到該app的資源摆尝,然后通過(guò)LayoutInflater去加載RemoteViews中的布局文件温艇。在SystemServer進(jìn)程中加載后的布局文件是一個(gè)普通的View,只不過(guò)對(duì)于我們的app進(jìn)程來(lái)說(shuō)堕汞,它是一個(gè)遠(yuǎn)程View也就是RemoteViews勺爱。接著系統(tǒng)會(huì)對(duì)View執(zhí)行一系列界面更新任務(wù),這些任務(wù)就是之前我們通過(guò)set方法提交的讯检,set方法對(duì)View的更新操作并不是立刻執(zhí)行的琐鲁,在RemoteViews內(nèi)部會(huì)記錄所有的更新操作卫旱,具體的執(zhí)行要等到RemoteViews被完全加載以后,這樣RemoteViews就可以在SystemServer中進(jìn)程中顯示了围段,這就是我們所看到的通知欄消息和桌面小部件顾翼。當(dāng)需要更新RemoteViews時(shí),我們又需要調(diào)用一系列set方法通過(guò)NotificationManager和AppWidgetManager來(lái)提交更新任務(wù)奈泪,具體更新操作也是在SystemServer進(jìn)程中完成的适贸。
理論上講系統(tǒng)完全可以通過(guò)Binder去支持所有的View和View操作,但是這樣做代價(jià)太大涝桅,View的方法太多了取逾,另外大量的IPC操作會(huì)影響效率。為了解決這個(gè)問(wèn)題苹支,系統(tǒng)并沒(méi)有通過(guò)Binder去直接支持View的跨進(jìn)程訪(fǎng)問(wèn)砾隅,而是提供了一個(gè)Action的概念,Action代表一個(gè)View操作债蜜,Action同樣實(shí)現(xiàn)了Parcelable接口晴埂。系統(tǒng)首先將View操作封裝到Action對(duì)象并將這些對(duì)象跨進(jìn)程傳輸?shù)竭h(yuǎn)程進(jìn)程,接著在遠(yuǎn)程進(jìn)程中執(zhí)行Action對(duì)象中的具體操作寻定。在我們的app中每調(diào)用一次set方法儒洛,RemoteViews中就會(huì)添加一個(gè)對(duì)應(yīng)的Action對(duì)象,當(dāng)我們通過(guò)NotificationManager和AppWidgetManager來(lái)提交我們的更新時(shí)狼速,這些Action對(duì)象就會(huì)傳輸?shù)竭h(yuǎn)程進(jìn)程并在遠(yuǎn)程進(jìn)程中依次執(zhí)行琅锻。遠(yuǎn)程進(jìn)程通過(guò)RemoteViews的apply方法來(lái)進(jìn)行View的更新操作,apply方法內(nèi)部是去遍歷所有的Action對(duì)象并調(diào)用它們的apply方法向胡,具體的View更新操作是由Action對(duì)象的apply方法來(lái)完成恼蓬。
上述做法的好處,首先是不需要定義大量的Binder接口僵芹,其次通過(guò)在遠(yuǎn)程進(jìn)程中批量執(zhí)行RemoteViews的更新操作從而避免了大量的IPC操作处硬,這就提高了程序的性能。
源碼分析
首先我們從RemoteViews的set方法入手拇派,比如設(shè)置圖片的方法setImageViewResource它內(nèi)部實(shí)現(xiàn)是這樣的:
/**
* Equivalent to calling ImageView.setImageResource
*
* @param viewId The id of the view whose drawable should change
* @param srcId The new resource id for the drawable
*/
public void setImageViewResource(int viewId, int srcId) {
setInt(viewId, "setImageResource", srcId);
}
上面的代碼中viewId是被操作的View的id荷辕,setImageResource是方法名,srcId是要給這個(gè)ImageView設(shè)置的圖片資源id件豌。這里的方法名和ImageView的setImageResource是一致的疮方。我們?cè)倏纯磗etInt方法的具體實(shí)現(xiàn):
/**
* Call a method taking one int on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setInt(int viewId, String methodName, int value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INT, value));
}
可以看到它內(nèi)部并沒(méi)有對(duì)View進(jìn)行直接操作,而是添加了一個(gè)ReflectionAction對(duì)象茧彤,字面上理解應(yīng)該是一個(gè)反射類(lèi)型的動(dòng)作骡显,再看addAction的實(shí)現(xiàn):
/**
* Add an action to be executed on the remote side when apply is called.
*
* @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<Action>();
}
mActions.add(a);
// update the memory usage stats
a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}
上述代碼可以看到,在RemoteViews內(nèi)部維護(hù)了一個(gè)名為mActions的ArrrayList,所有的對(duì)View更新的操作動(dòng)作都被添加到這個(gè)集合中蟆盐,注意承边,僅僅是添加進(jìn)來(lái)保存,并沒(méi)有去執(zhí)行這些Action石挂。到這里setImageViewResource方法的源碼已經(jīng)結(jié)束了博助,下面我們要弄清楚這些Action的執(zhí)行。我們?cè)倏纯碦emoteViews的apply方法:
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;
}
首先RemoteViews會(huì)通過(guò)LayoutInflater去加載它的布局文件痹愚,加載完之后通過(guò)performApply方法去執(zhí)行一些更新操作富岳。
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集合且執(zhí)行每個(gè)Action的apply方法,應(yīng)該可以看出拯腮,Action的apply方法才是真正操作View更新的地方窖式。
當(dāng)我們調(diào)用RemoteViews的set方法時(shí),并不會(huì)立刻更新它們的界面动壤,而必須要通過(guò)NotificationManager的notify方法以及AppWidgetManager的updateAppWidget方法才能更新它們的界面萝喘。實(shí)際上在AppWidgetManager的updateAppWidget內(nèi)部實(shí)現(xiàn)中,的確是通過(guò)RemoteViews的apply方法和reapply方法來(lái)加載或更新界面的琼懊,apply和reapply的區(qū)別在于:apply會(huì)加載布局并更新界面阁簸,而reapply則只會(huì)更新界面,初始化時(shí)調(diào)用apply方法哼丈,后面的更新則調(diào)用reapply方法启妹。
ReflectionAction是Action的子類(lèi),我們看下它的源碼:
/**
* Base class for the reflection actions.
*/
private final class ReflectionAction extends Action {
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;
}
@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);
}
}
}
它的內(nèi)部實(shí)現(xiàn)有點(diǎn)長(zhǎng)醉旦,我們主要看它的apply方法饶米。
getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
這句代碼就是它以反射的方式來(lái)對(duì)View進(jìn)行操作,getMethod根據(jù)方法名得到反射所需的Method對(duì)象车胡,然后執(zhí)行該方法檬输。
RemoteViews中的單擊事件,只支持發(fā)起PendingIntent吨拍,不支持onClickListener這種方法褪猛。我們需要注意setOnClickPendingIntent、setPendingIntentTemplate和setOnClickFillInIntent這幾個(gè)方法之間的區(qū)別和聯(lián)系羹饰。setOnClickPendingIntent是用于給普通的View設(shè)置點(diǎn)擊事件,但是它不能給ListView或者GridView碳却、StackView中的item設(shè)置點(diǎn)擊事件队秩,因?yàn)殚_(kāi)銷(xiāo)比較大,系統(tǒng)禁止了這種方式昼浦。而setPendingIntentTemplate方法就能給item設(shè)置單擊事件馍资,具體使用請(qǐng)參照這篇文章Android 之窗口小部件高級(jí)篇--App Widget 之 RemoteViews。
RemoteViews的優(yōu)缺點(diǎn)
實(shí)際開(kāi)發(fā)中关噪,跨進(jìn)程通信我們可以選擇AIDL去實(shí)現(xiàn)鸟蟹,但是如果對(duì)界面的更新比較頻繁乌妙,這時(shí)會(huì)有效率問(wèn)題,而且AIDL接口可能會(huì)變得很復(fù)雜建钥,但如果采用RemoteViews來(lái)實(shí)現(xiàn)就沒(méi)有這個(gè)問(wèn)題了藤韵,RemoteViews的缺點(diǎn)就是它僅支持一些常見(jiàn)的View,而對(duì)于自定義View是不支持的熊经。