Android中的RemoteViews

概述

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ì)清除本身蹂风。效果如圖:

系統(tǒng)默認(rèn)樣式

為了滿(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)的效果圖:

運(yù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ā)送廣播杭抠。它的主要方法有下面這些:

PendingIntent的方法

啟動(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方法

RemoteView-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是不支持的熊经。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末泽艘,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子镐依,更是在濱河造成了極大的恐慌匹涮,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件槐壳,死亡現(xiàn)場(chǎng)離奇詭異然低,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)务唐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)雳攘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人绍哎,你說(shuō)我怎么就攤上這事来农。” “怎么了崇堰?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵沃于,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我海诲,道長(zhǎng)繁莹,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任特幔,我火速辦了婚禮咨演,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蚯斯。我一直安慰自己薄风,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布拍嵌。 她就那樣靜靜地躺著遭赂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪横辆。 梳的紋絲不亂的頭發(fā)上撇他,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼困肩。 笑死划纽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的锌畸。 我是一名探鬼主播勇劣,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蹋绽!你這毒婦竟也來(lái)了芭毙?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤卸耘,失蹤者是張志新(化名)和其女友劉穎退敦,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蚣抗,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡侈百,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了翰铡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钝域。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖锭魔,靈堂內(nèi)的尸體忽然破棺而出例证,到底是詐尸還是另有隱情,我是刑警寧澤迷捧,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布织咧,位于F島的核電站,受9級(jí)特大地震影響漠秋,放射性物質(zhì)發(fā)生泄漏笙蒙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一庆锦、第九天 我趴在偏房一處隱蔽的房頂上張望捅位。 院中可真熱鬧,春花似錦搂抒、人聲如沸艇搀。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)中符。三九已至,卻和暖如春誉帅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工蚜锨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留档插,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓亚再,卻偏偏與公主長(zhǎng)得像郭膛,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子氛悬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容