Android開發藝術探索 第5章 理解RemoteViews 讀書筆記

RemoteViews是一種遠程View,可以在其他進程中顯示,為了能夠更新它的界面,RemoteViews提供了一組基礎操作用于跨進程更新它的界面。
本章會介紹RemoteViews在通知欄和桌面小部件上的應用,分析RemoveViews的內部機制,最后分析RemoteViews的意義并給出一個采用RemoteViews來跨進程更新界面的示例。


5.1 RemoteViews的應用

RemoteViews主要用于通知欄和桌面小部件的開發。通知欄主要通過NotificationManager的notify方法來實現;桌面小部件則是通過AppWidgetProvider來實現的,AppWidgetProvider本質上是一個廣播。因為RemoteViews運行在其他進程(SystemService進程),所以無法直接更新界面。

5.1.1 RemoteViews在通知欄上的應用
    Notification notification = new Notification();
    notification.icon = R.mipmap.ic_launcher;
    notification.tickerText = "hello notification";
    notification.when = System.currentTimeMillis();
    notification.flags = Notification.FLAG_AUTO_CANCEL;
    Intent intent = new Intent(this, RemoteViewsActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);//RemoveViews所加載的布局文件
    remoteViews.setTextViewText(R.id.tv, "這是一個Test");//設置文本內容
    remoteViews.setTextColor(R.id.tv, Color.parseColor("#abcdef"));//設置文本顏色
    remoteViews.setImageViewResource(R.id.iv, R.mipmap.ic_launcher);//設置圖片
    PendingIntent openActivity2Pending = PendingIntent.getActivity
            (this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);//設置RemoveViews點擊后啟動界面
    remoteViews.setOnClickPendingIntent(R.id.tv, openActivity2Pending);
    notification.contentView = remoteViews;
    notification.contentIntent = pendingIntent;
    NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    manager.notify(2, notification);
5.1.2 RemoveViews在桌面小部件上的應用
  1. 定義好小部件界面
    在res/layout下新建一個xml文件,命名為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/iv"
        android:layout_width="360dp"
        android:layout_height="360dp"
        android:layout_gravity="center" />
</LinearLayout>
  1. 定義小部件的配置信息
    在res/xml/下新建appwidget_provider_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="360dp"
      android:minWidth="360dp"
      android:updatePeriodMillis="864000"/>
  1. 定義小部件的實現類
    這個類需要繼承AppWidgetProvider;我們這里實現一個簡單的widget,點擊它后,3張圖片隨機切換顯示。
public class ImgAppWidgetProvider extends AppWidgetProvider {
    public static final String TAG = "ImgAppWidgetProvider";
    public static final String CLICK_ACTION = "cn.hudp.androiddevartnote.action.click";
    private static int index;

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        if (intent.getAction().equals(CLICK_ACTION)) {
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

            updateView(context, remoteViews, appWidgetManager);
        }
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);

        updateView(context, remoteViews, appWidgetManager);
    }

    // 由于onReceive 和 onUpdate中部分代碼相同 則抽成一個公用方法
    public void updateView(Context context, RemoteViews remoteViews, AppWidgetManager appWidgetManager) {
        index = (int) (Math.random() * 3);
        if (index == 1) {
            remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei1);
        } else if (index == 2) {
            remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei2);
        } else {
            remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei3);
        }
        Intent clickIntent = new Intent();
        clickIntent.setAction(CLICK_ACTION);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, clickIntent, 0);
        remoteViews.setOnClickPendingIntent(R.id.iv, pendingIntent);
        appWidgetManager.updateAppWidget(new ComponentName(context, ImgAppWidgetProvider.class), remoteViews);
    }
}
  1. 在AndroidManifest.xml中聲明小部件
    因為桌面小部件的本質是一個廣播組件,因此必須要注冊。
<receiver android:name=".RemoveViews_5.ImgAppWidgetProvider">
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/appwidget_provider_info">
        </meta-data>
    <intent-filter>
        <action android:name="cn.hudp.androiddevartnote.action.click" />
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
</receiver>

上面代碼中有兩個action,第一個是用于識別小部件的單擊行為,而第二個則是作為小部件的標識必須存在的;如果不加這個receiver就不是一個桌面小部件并且也無法顯示在手機的小部件中。

  1. 廣播到來的時候,AppWidgetProvider會自動根據廣播的Action通過onReceive方法來分發廣播,也就是調用
    onEnable: 當該窗口小部件第一次添加到桌面時調用的方法,可添加多次但只在第一次調用。
    onUpdate: 小部件被添加時或者每次小部件更新時都會調用一次該方法,小部件的更新時機是有updatePeriodMillis來指定,每個周期小部件就會自動更新一次。
    onDeleted: 每刪除一次桌面小部件就調用一次。
    onDisabled: 當最后一個該類型的小部件被刪除時調用該方法。
    onReceive: 這是廣播的內置方法,用于分發具體事件給其他方法。
5.1.3 PendingIntent概述

PendingIntent表示一種處于pending(待定、等待、即將發生)狀態的意圖;PendingIntent通過send和cancel方法來發送和取消特定的待定Intent。
PendingIntent支持三種待定意圖:啟動Activity、啟動Service和發送廣播。分別對應:

getActivity / getService / getBroadcast
參數相同,都為:(Context context, int requestCode, Intent intent, int flags) 

其中第二個參數,requestCode表示PendingIntent發送方的請求碼,多少情況下為0即可,requestCode會影響到flags的效果。
PendingIntent的匹配規則是:如果兩個PendingIntent他們內部的Intent相同并且requestCode也相同,那么這兩個PendingIntent就是相同的。那么什么情況下Intent相同呢?Intent的匹配規則是,如果兩個Intent的ComponentName和intent-filter都相同;那么這兩個Intent也是相同的。

flags參數的含義:
FLAG_ONE_SHOP 當前的PendingIntent只能被使用一次,然后他就會自動cancel,如果后續還有相同的PendingIntent,那么它們的send方法就會調用失敗。
FLAG_NO_CREATE 當前描述的PendingIntent不會主動創建,如果當前PendingIntent之前存在,那么getActivity、getService和getBroadcast方法會直接返回Null,即獲取PendingIntent失敗,無法單獨使用,平時很少用到。
FLAG_CANCEL_CURRENT 當前描述的PendingIntent如果已經存在,那么它們都會被cancel,然后系統會創建一個新的PendingIntent。對于通知欄消息來說,那些被cancel的消息單擊后無法打開。
FLAG_UPDATE_CURRENT 當前描述的PendingIntent如果已經存在,那么它們都會被更新,即它們的Intent中的Extras會被替換為最新的。

NotificationManager的notify方法分析

manager.notify(1,notification);
  1. 如果notify方法的id是常量,那么不管PendingIntent是否匹配,后面的通知都會替換掉前面的通知。
  2. 如果notify的方法id每次都不一樣,那么當PendingIntent不匹配的時候,不管在何種標記為下,這些通知都不會互相干擾。
  3. 如果PendingIntent處于匹配階段,分情況:
  4. 采用FLAG_ONE_SHOT標記位,那么后續通知中的PendingIntent會和第一條通知保持一致,包括其中的Extras,單擊任何一條通知后,其他通知均無法再打開;當所有通知被清除后。
  5. 采用FLAG_CANCEL_CURRENT標記位,只有最新的通知可以打開,之前彈出的所有通知均無法打開。
  6. 采用FLAG_UPDATE_CURRENT標記位,那么之前彈出的PendingIntent會被更新,最終它們和最新的一條保存完全一致,包括其中的Extras,并且這些通知都是可以打開的。

5.2 RemoteViews的內部機制

  1. RemoteViews的構造方法:public RemoteViews(String packageName,int layoutId),第一個參數表示當前應用的包名,第二個參數表示待加載的布局文件。
  2. RemoveViews并不能支持所有View類型,支持以下:
    Layout:FrameLayout、LinearLayout、RelativeLayout、GridLayout。
    View:Button、ImageButton、ImageView、ProgressBar、TextView、ListView、GridView、ViewStub等(例如EditText是不允許在RemoveViews中使用的,使用會拋異常)。
  3. RemoteView沒有findViewById方法,因此無法訪問里面的View元素,而必須通過RemoteViews所提供的一系列set方法來完成,這是通過反射調用的。
  4. 通知欄和小組件分別由NotificationManager(NM)和AppWidgetManager(AWM)管理,而NM和AWM通過Binder分別和SystemService進程中的NotificationManagerService以及AppWidgetService中加載的,而它們運行在系統的SystemService中,這就和我們進程構成了跨進程通訊。
  5. 工作流程:首先RemoteViews會通過Binder傳遞到SystemService進程,因為RemoteViews實現了Parcelable接口,因此它可以跨進程傳輸,系統會根據RemoteViews的包名等信息拿到該應用的資源;然后通過LayoutInflater去加載RemoteViews中的布局文件。接著系統會對View進行一系列界面更新任務,這些任務就是之前我們通過set來提交的。set方法對View的更新并不會立即執行,會記錄下來,等到RemoteViews被加載以后才會執行。
  6. 為了提高效率,系統沒有直接通過Binder去支持所有的View和View操作。而是提供一個Action概念,Action同樣實現Parcelable接口。系統首先將View操作封裝到Action對象并將這些對象跨進程傳輸到SystemService進程,接著SystemService進程執行Action對象的具體操作。遠程進程通過RemoteViews的apply方法來進行View的更新操作,RemoteViews的apply方法會去遍歷所有的Action對象并調用他們的apply方法。這樣避免了定義大量的Binder接口,也避免了大量IPC操作。
  7. apply和reApply的區別在于:apply會加載布局并更新界面,而reApply則只會更新界面。
  8. 關于單擊事件,RemoteViews中只支持發起PendingIntent,不支持onClickListener那種模式。setOnClickPendingIntent用于給普通的View設置單擊事件,不能給集合(ListView/StackView)中的View設置單擊事件(開銷大,系統禁止了這種方式)。如果要給ListView/StackView中的item設置單擊事件,必須將setPendingIntentTemplate和setOnClickFillInIntent組合使用才可以。

5.3 RemoteViews的意義

RemoteViews最大的意義在于方便的跨進程更新UI。

  • 當一個應用需要更新另一個應用的某個界面,我們可以選擇用AIDL來實現,但如果更新比較頻繁,效率會有問題,同時AIDL接口就可能變得很復雜。如果采用RemoteViews就沒有這個問題,但RemoteViews僅支持一些常用的View,如果界面的View都是RemoteViews所支持的,那么就可以考慮采用RemoteViews。
  • 利用RemoteViews加載其他App的布局文件與資源。
    final String pkg = "cn.hudp.remoteviews";//需要加載app的包名
    Resources resources = null;
    try {
        resources = getPackageManager().getResourcesForApplication(pkg);
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    if (resources != null) {
        int layoutId = resources.getIdentifier("activity_main", "layout", pkg); //獲取對于布局文件的id
        RemoteViews remoteViews = new RemoteViews(pkg, layoutId);
        View view = remoteViews.apply(this, llRemoteViews);//llRemoteViews是View所在的父容器
        llRemoteViews.addView(view);
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容