Android中的RemoteViews

概述

RemoteViews顧名思義就是遠程View,它表示的是一個View結構,它可以在其他進程中顯示,為了跨進程更新它的界面,RemoteViews提供了一組基礎的操作來實現這個效果。RemoteViews在Android中的使用場景有兩種:通知欄和桌面小部件。

RemoteViews在通知欄上的應用

我們知道通知欄除了默認的效果外還支持自定義布局。

使用系統默認的樣式彈出一個通知的方式如下:(android3.0之后)

private void showDefaultNotification() {
    NotificationCompat.Builder builder = new NotificationCompat.Builder(this);

    // 設置通知的基本信息:icon、標題、內容
    builder.setSmallIcon(R.mipmap.ic_launcher);
    builder.setContentTitle("My notification");
    builder.setContentText("Hello World!");
    builder.setAutoCancel(true);

    // 設置通知的點擊行為:這里啟動一個 Activity
    Intent intent = new Intent(this, SecondActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    builder.setContentIntent(pendingIntent);

    // 發送通知 id 需要在應用內唯一
    NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    notificationManager.notify(id, builder.build());
}

上述代碼會彈出一個系統默認樣式的通知,單擊通知后會打開SecondActivity同時會清除本身。效果如圖:

系統默認樣式

為了滿足個性化需求,我們還可能會用到自定義通知。實現自定義通知我們首先需要提供一個布局文件,然后通過RemoteViews來加載這個布局文件即可改變通知的樣式。

private void showCustomNotification() {

    RemoteViews remoteView;

    // 構建 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);

    // 設置自定義 RemoteViews
    builder.setContent(remoteView).setSmallIcon(R.mipmap.ic_launcher);

    // 設置通知的優先級(懸浮通知)
    builder.setPriority(NotificationCompat.PRIORITY_MAX);
    Uri alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
    // 設置通知的提示音
    builder.setSound(alarmSound);


    // 設置通知的點擊行為:這里啟動一個 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);
}

效果如圖所示:

自定義樣式

創建RemoteViews對象我們只需要知道當前應用包名和布局文件的資源id,比較簡單,但是要更新RemoteViews就不是那么容易了,因為我們無法直接訪問布局文件中的View,而必須通過RemoteViews提供的特定的方法來更新View。比如設置TextView文本內容需要用setTextViewText方法,設置ImageView圖片需要通過setImageViewResource方法。也可以給里面的View設置點擊事件,需要使用PendingIntent并通過setOnClickPendingIntent方法來實現。之所以更新RemoteViews如此復雜,直接原因是因為RemoteViews沒有提供跟View類似的findViewById這個方法,我們無法獲取到RemoteViews中的子View。

RemoteViews在桌面小部件上的應用

現在我要實現的效果是這樣一個小部件:

AppWidgetProvider是Android中提供用于實現桌面小部件的類,它的本質其實是一個廣播。開發桌面小部件的步驟:

定義小部件布局

在res/layout/下新建一個布局文件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="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/下新建一個資源文件,命名自定:

<?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>

解釋下各個屬性的含義
android:initialLayout:指定小部件的初始化布局
android:minHeight:小部件最小高度
android:minWidth:小部件最小寬度
android:previewImage:小部件列表顯示的圖標
android:updatePeriodMillis:小部件自動更新的周期
android:widgetCategory:小部件顯示的位置,home_screen表示只在桌面上顯示

定義小部件的實現類

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)) {

            // 點擊的是第一個按鈕
            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)) {

            // 點擊的是第二個按鈕
            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)) {

            // 點擊的是第三個按鈕
            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)) {

            // 點擊的是第四個按鈕
            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);

    }

    /**
     * 桌面小部件每次更新時調用的方法
     * @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標簽中的name屬性是固定的android.appwidget.provider,而resource屬性則是我們剛才新建的小部件的配置信息的xml,intent-filter中的android.appwidget.action.APPWIDGET_UPDATE是必須加的,它作為小部件的標識存在,這是系統的規范,否則這個receiver就不是一個桌面小部件,并且也無法出現在手機的小部件列表里。下面其他的action分別對應各個按鈕點擊的動作。

最后實現的效果圖:

運行效果圖

總結下這個操作過程:當小部件一被添加到桌面時會調用Provider中的onUpdate方法,在這個方法中我們會通過AppWidgetManager去更新小部件的界面,但是這個更新我們是沒辦法直接更新的,而是通過RemoteViews來操作,setOnClickPendingIntent給每個按鈕設置了點擊時會發送的廣播動作,而在清單文件中我們聲明小部件時已經將這些廣播動作都加到intent-filter,所以當我們點擊桌面上該小部件中的某個按鈕時,就會發送對應的廣播,而小部件監聽了這個廣播,接收到廣播后再onReceive方法中根據動作來分別處理點擊事件。當然,對小部件的一些其他操作方法(比如onEnabled、onDisabled、onDeleted)的廣播也會在onReceive中接收到,然后分發給不同的方法。(我這里處理點擊事件用的也是RemoteViews的方式,其實不必,直接使用context.startActivity即可,但如果不是打開頁面,而是要更新小部件的界面,那么就需要繼續使用RemoteViews來更新了。)

PendingIntent

在上面實現小部件時我們多次使用到了PendingIntent,這個東西顧名思義我們可以理解為將要發生的意圖,就是在某個待定的時刻會發生。所以它和Intent的區別就在于一個是立即執行的一個是在未來某個時候執行。PendingIntent典型的使用場景是通知中點擊通知時跳轉頁面,因為我們不知道用戶什么時候點擊,另外就是給RemoteViews添加單擊事件,因為RemoteViews運行在遠程進程中,所以它不同于普通的View,不能想View那樣通過setOnClickListener方法來給設置單擊事件,想要給RemoteViews設置點擊事件,就必須使用PendingIntent,通過setOnClickPendingIntent方法來設置。PendingIntent是通過send和cancel方法來發送和取消待執行的Intent。

PendingIntent支持三種待定意圖,啟動activity(常見的通知)、啟動Service和發送廣播。它的主要方法有下面這些:

PendingIntent的方法

啟動Activity它有兩種,啟動單個和啟動多個,當使用getActivities時,實際上啟動的是Intent數組中最后一個activity,如果要讓最后一個activity返回時不退出app而是退回到上一個activity,實現方式可參照我上面第一個按鈕的點擊處理。

getActivity、getService、getBroadcast這三個方法的參數意義都是相同的,第一個上下文,第三個待定的意圖,第二個requestCode表示PendingIntent發送方的請求碼,多數情況下設置為0即可,另外requestCode會影響到第四個參數flags的效果。flags這個標志位表示執行效果。

常見的flags類型有FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT、FLAG_UPDATE_CURRENT。要理解這四個標志位的含義和區別,我們首先要弄明白PendingIntent的匹配規則,也就是什么情況下PendingIntent是相同的。

匹配規則:

  • 如果兩個PendingIntent它們內部的Intent相同,且requestCode也相同,那么這兩個PendingIntent就是相同的;

Intent相同的情況:

  • 如果兩個Intent的ComponentName和intent-filter都相同,那么這兩個Intent就是相同的。(Extras不參與Intent的匹配過程,就是它不同,只要ComponentName和intent-filter相同,Intent都算相同的。)

FLAG_ONE_SHOT:表示當前描述的PendingIntent只能被使用一次,然后它就會自動cancel,如果后續還有相同的PendingIntent,那么它們的send方法就會調用失敗。如果通知欄消息使用這種標記位,同類型的通知就只會被打開一次,后續的通知將無法點開。

FLAG_NO_CREATE:表示當前描述的PendingIntent不會主動創建,如果當前PendingIntent之前不存在,那么getActivities等這些方法會直接返回null,獲取PendingIntent失敗。它無法單獨使用。

FLAG_CANCEL_CURRENT:表示當前描述的PendingIntent如果已經存在,就cancel它,然后系統會創建一個新的。

FLAG_UPDATE_CURRENT:表示當前描述的PendingIntent如果已經存在,那么它會被更新,內部的Intent中的Extras也會被更新。

RemoteViews的內部機制

RemoteViews的構造方法很多,我們最常見的一個是

public RemoteViews(String packageName, int layoutId) {
    this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
}

只需要包名和待加載的資源文件id,它并不能支持所有類型的View,也不支持自定義的View,它能支持的類型如下:

Layout:FrameLyout、LinearLayout、RelativeLayout、GridLayout

View:Button、ImageView、ImageButton、ProgressBar、TextView、ListView、GridView、StackView、ViewStub、AdapterViewFlipper、ViewFlipper、AnalogClock、Chronometer。

如果我們在RemoteViews中使用了它不支持的View不如EditText,那么就會發生異常。

我們看看RemoteViews的set方法

RemoteView-set方法

從這些方法中看出,原本可以直接調用的View的方法,現在要通過RemoteViews的一系列set方法來完成。

我們知道,通知欄和桌面小部件分別由NotificationManager和AppWidgetManager來管理的,而NotificationManager和AppWidgetManager是通過Binder分別和SystemServer進程中的NotificationManagerService以及AppWidgetService進行通信,因此,通知欄和桌面小部件中的布局文件實際上是在NotificationManagerService和AppWidgetService中被加載的,而他們運行在SystemServer中,這其實已經和我們自己的app進程構成了跨進程通信。

理論分析

首先RemoteViews會通過Binder傳遞到SystemServer進程,因為RemoteViews實現了Parcelable接口,可以跨進程傳輸,系統會根據RemoteViews中的包名等信息去獲取到該app的資源,然后通過LayoutInflater去加載RemoteViews中的布局文件。在SystemServer進程中加載后的布局文件是一個普通的View,只不過對于我們的app進程來說,它是一個遠程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接口。系統首先將View操作封裝到Action對象并將這些對象跨進程傳輸到遠程進程,接著在遠程進程中執行Action對象中的具體操作。在我們的app中每調用一次set方法,RemoteViews中就會添加一個對應的Action對象,當我們通過NotificationManager和AppWidgetManager來提交我們的更新時,這些Action對象就會傳輸到遠程進程并在遠程進程中依次執行。遠程進程通過RemoteViews的apply方法來進行View的更新操作,apply方法內部是去遍歷所有的Action對象并調用它們的apply方法,具體的View更新操作是由Action對象的apply方法來完成。

上述做法的好處,首先是不需要定義大量的Binder接口,其次通過在遠程進程中批量執行RemoteViews的更新操作從而避免了大量的IPC操作,這就提高了程序的性能。

源碼分析

首先我們從RemoteViews的set方法入手,比如設置圖片的方法setImageViewResource它內部實現是這樣的:

/**
 * 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是要給這個ImageView設置的圖片資源id。這里的方法名和ImageView的setImageResource是一致的。我們再看看setInt方法的具體實現:

/**
 * 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));
}

可以看到它內部并沒有對View進行直接操作,而是添加了一個ReflectionAction對象,字面上理解應該是一個反射類型的動作,再看addAction的實現:

/**
 * 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內部維護了一個名為mActions的ArrrayList,所有的對View更新的操作動作都被添加到這個集合中,注意,僅僅是添加進來保存,并沒有去執行這些Action。到這里setImageViewResource方法的源碼已經結束了,下面我們要弄清楚這些Action的執行。我們再看看RemoteViews的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會通過LayoutInflater去加載它的布局文件,加載完之后通過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);
        }
    }
}

這里遍歷了mActions集合且執行每個Action的apply方法,應該可以看出,Action的apply方法才是真正操作View更新的地方。

當我們調用RemoteViews的set方法時,并不會立刻更新它們的界面,而必須要通過NotificationManager的notify方法以及AppWidgetManager的updateAppWidget方法才能更新它們的界面。實際上在AppWidgetManager的updateAppWidget內部實現中,的確是通過RemoteViews的apply方法和reapply方法來加載或更新界面的,apply和reapply的區別在于:apply會加載布局并更新界面,而reapply則只會更新界面,初始化時調用apply方法,后面的更新則調用reapply方法。

ReflectionAction是Action的子類,我們看下它的源碼:

/**
 * 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);
        }
    }
}

它的內部實現有點長,我們主要看它的apply方法。

getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));

這句代碼就是它以反射的方式來對View進行操作,getMethod根據方法名得到反射所需的Method對象,然后執行該方法。

RemoteViews中的單擊事件,只支持發起PendingIntent,不支持onClickListener這種方法。我們需要注意setOnClickPendingIntent、setPendingIntentTemplate和setOnClickFillInIntent這幾個方法之間的區別和聯系。setOnClickPendingIntent是用于給普通的View設置點擊事件,但是它不能給ListView或者GridView、StackView中的item設置點擊事件,因為開銷比較大,系統禁止了這種方式。而setPendingIntentTemplate方法就能給item設置單擊事件,具體使用請參照這篇文章Android 之窗口小部件高級篇--App Widget 之 RemoteViews

RemoteViews的優缺點

實際開發中,跨進程通信我們可以選擇AIDL去實現,但是如果對界面的更新比較頻繁,這時會有效率問題,而且AIDL接口可能會變得很復雜,但如果采用RemoteViews來實現就沒有這個問題了,RemoteViews的缺點就是它僅支持一些常見的View,而對于自定義View是不支持的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容