1. 程式人生 > >Android 開發藝術探索筆記之五 -- 理解 RemoteViews

Android 開發藝術探索筆記之五 -- 理解 RemoteViews

學習內容:

  • RemoteViews 在通知欄和桌面小部件上的應用
  • RemoteViews 的內部機制
  • RemoteViews 的意義

RemoteView 的應用

實際開發中,RemoteViews 主要用在通知欄和桌面小部件的開發過程中。通知欄主要通過 NotificationManager 的 notify 方法實現,桌面小部件則是通過 AppWidgetProvider 來實現,其本質也是一個廣播。

通知欄和桌面小部件更新介面時,RemoteView 無法像 View 一樣在 Activity 中直接更新,因為介面執行在系統的 SystemServer 程序,需要跨程序更新。

下面簡單介紹 RemoteView 的應用

  1. RemoteView 在通知欄上的應用(主要為 自定義佈局

    (適配 Android 8.0)

    //建立NotificationManager例項
    NotificationManager mManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    
    //建立NotificationChannel例項
    //引數說明:
    //id:NotificationChannel的唯一標識
    //name:NotificationChannel的名稱,在Settings可看到
    //importance:對channel設定重要性,更改見後續表格
    NotificationChannel mChannel = new NotificationChannel("id","name",NotificationManager.IMPORTANCE_DEFAULT); mManager.createNotificationChannel(mChannel); //建立PendingIntent Intent intent = new Intent(this,SecondActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); //建立RemoteView
    RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.layout_notification); remoteViews.setTextViewText(R.id.msg,"xx"); remoteViews.setImageViewResource(R.id.icon,R.drawable.icon); remoteViews.setOnclidePendingIntent(R.id.clickable,pendingIntent); //建立builder,並設定一系列屬性 Notification.Builder builder = new Notification.Builder(this,"id"); builder.setSmallIcon(R.drawable.ic_launcher_background) .setContentTitle("title") .setContentText("text") //以上三個為必需的屬性 .setAutoCancel(true); //Android 7.0 之後需要通過Notification.Builder設定contentView builder.setCustomContentView(remoteViews). //建立通知 Notification notification = builder.build(); //推送通知 mManager.notify(1,notification);

    ​ RemoteViews 和 View 不同,每個方法中幾乎都要求傳入一個 id 引數,比如 setTextViewText(int viewId, CharSequence text),需要傳入TextView 的 id。

    直觀原因 是因為 RemoteViews 沒有提供和 View 類似的 findViewById 這個方法,因此我們無法獲取到 RemoteView 中的子 View。(實際原因並非如此,後面詳細介紹)

  2. RemoteViews 在桌面小部件上的應用

    利用 AppWidgetProvider,本質是廣播。

    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/imageView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/icon" />
      </LinearLayout>
    2. 定義小部件配置資訊

      在 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="84dp"
          android:minWidth="84dp"
          //自動更新週期,毫秒單位
          android:updatePeriodMillis="864000"/>
      
    3. 定義小部件的實現類

      繼承 AppWidgetProvider,功能為簡單的 點選後隨機切換圖片。

      public class MyAppWidgetProvider 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);
        }
      
        // 隨機更新圖片
        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, MyAppWidgetProvider.class), remoteViews);
        }
      }
    4. 在 AndroidManifest.xml 中宣告小部件

      原因:本質是廣播元件,因此需要註冊

      <receiver android:name=".MyAppWidgetProvider">
        <meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/appwidget_provider_info">
            </meta-data>
        <intent-filter>
          //識別小部件的單擊行為
            <action android:name="com.whdalive.action.click" />
             //作為小部件的標識,必須存在
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>
      </receiver>
    5. 廣播分發

      當廣播到來之後,AppWidgetProvider 會自動根據廣播的 Action 通過 onReceive 來自動分發廣播,相關方法如下

      1. onEnable: 當該視窗小部件第一次新增到桌面時呼叫的方法,可新增多次但只在第一次呼叫。
      2. onUpdate: 小部件被新增時或者每次小部件更新時都會呼叫一次該方法,小部件的更新時機是有updatePeriodMillis來指定,每個週期小部件就會自動更新一次。
      3. onDeleted: 每刪除一次桌面小部件就呼叫一次。
      4. onDisabled: 當最後一個該型別的小部件被刪除時呼叫該方法。
      5. onRestored:當接收到 ACTION_APPWIDGET_RESTORED 廣播,從備份恢復小部件時呼叫
      6. onAppWidgetOptionsChanged:當接收到 ACTION_APPWIDGET_OPTIONS_CHANGED 廣播,小部件的尺寸位置發生變化時呼叫。
      7. onReceive: 這是廣播的內建方法,用於分發具體事件給其他方法。
  3. PendingIntent 概述

    1. 基本介紹

      1. PendingIntent 表示一種處於 Pending(待定、等待、即將發生)狀態的意圖;
      2. 典型應用場景是給 RemoteViews 新增點選事件,(RemoteViews 執行在遠端程序)
      3. 通過 send 和 cancel 方法來發送和取消特定的待定 Intent。
    2. 分類

      1. 啟動 Activity -> getActivity(Context context, int requestCode, Intent intent, int flags)

      2. 啟動 Service -> getService(Context context, int requestCode, Intent intent, int flags)

      3. 傳送廣播 -> getBroadcast(Context context, int requestCode, Intent intent, int flags)

        引數說明:

        1. requestCode 表示 PendingIntent 傳送方的請求碼,多數情況下設定 0 即可,另外 requestCode 會影響到 flags 的效果。
        2. flags 引數:
        3. FLAG_ONE_SHOP 當前的PendingIntent只能被使用一次,然後他就會自動cancel,如果後續還有相同的PendingIntent,那麼它們的send方法就會呼叫失敗。
        4. FLAG_NO_CREATE 當前描述的PendingIntent不會主動建立,如果當前PendingIntent之前存在,那麼getActivity、getService和getBroadcast方法會直接返回Null,即獲取PendingIntent失敗,無法單獨使用,平時很少用到。
        5. FLAG_CANCEL_CURRENT 當前描述的PendingIntent如果已經存在,那麼它們都會被cancel,然後系統會建立一個新的PendingIntent。對於通知欄訊息來說,那些被cancel的訊息單擊後無法開啟。
        6. FLAG_UPDATE_CURRENT 當前描述的PendingIntent如果已經存在,那麼它們都會被更新,即它們的Intent中的Extras會被替換為最新的。
    3. 匹配規則

      1. 如果兩個 PendingIntent 內部的 Intent 相同且 requestCode 也相同,那麼二者相同
      2. Intent 相同的匹配規則:Intent 的 ComponentName 和 intent-filter 都相同。Extras 不參與 Intent的匹配過程。

RemoteView 的內部機制

  1. 構造方法 public RemoteViews(String packageName, int layoutId)

    引數說明:

    1. packageName:當前應用的包名
    2. layoutId:待載入的佈局檔案
  2. 限制 -> 支援的 View 型別有限

    1. LayoutFrameLayoutLineanLayoutRelativeLayoutGridLayout
    2. ViewAnalogClockButtonChronometerImageButtonImageViewProgressBarTextViewViewFlipperListViewGridViewStackViewAdapterViewFlipperViewStub
  3. 特殊之處

    1. RemoteView 沒有提供 findViewById 方法,因此無法直接訪問裡面的 View 元素,而必須通過 RemoteViews 所提供的一些列 set 方法來完成,這時因為 RemoteView 在遠端程序中顯示
    2. 一系列 set 方法 是通過反射來完成的。
  4. 工作流程

    1. 前置:通知欄和桌面小部件分別由 NotificationManager 和 AppWidgetManager 管理,而 NotificationManager 和 AppWidgetManager 通過 Binder 分別和 SystemServer 程序中的 NotificationManagerService(NMS) 以及 AppWidgetService(AWS) 進行通訊。佈局檔案實際是在 NMS 和 AWS 中被載入的,而執行在 SystemServer 中,這就和我們的程序構成了 跨程序通訊 的場景。

    2. 具體流程

      1. 首先 RemoteViews 通過 Binder 傳遞到 System Server 程序(RemoteViews 實現了 Parcelable 介面)。系統會根據 RemoteViews 中的包名等資訊去得到該應用的資源。

      2. 然後通過 LayoutInflater 去載入 RemoteViews 中的佈局檔案。(對於 SystemServer 程序來講,載入的只是一個普通的 view,只不過對於我們的程序來講是 遠端的)

      3. 接著系統對 View 執行一系列介面更新任務,這些任務通過 set 方法來提交。這些更新不是立刻執行,而是在 RemoteViews 中記錄所有更新操作,等到 RemoteViews 被載入以後才能執行。

        到此時,RemoteViews 就可以在 SystemServer 程序中顯示了。

      4. 當需要更新 RemoteViews 時,呼叫一些列 set 方法並通過 NotificationManager 和 AppWidgetManager 來提交更新任務,具體操作也是在 SystemServer 程序中完成。

    3. 進一步說明 – 跨程序

      1. 系統不直接通過 Binder 支援所有的 View 和 View 操作,否則 View 的方法龐大,同時 IPC 操作會影響效率
      2. 系統提供了一個 Action 概念, Action 實現了 Parcelable 介面,代表一個 View 操作。
      3. 系統首先將 View 操作封裝到 Action 物件並將這些物件跨程序傳輸到遠端程序,接著在遠端程序中執行 Action 物件中的具體操作。遠端程序通過 RemoteViews 的 apply 方法來進行 View 的更新操作,Remoteview 的 apply 方法內部會遍歷所有的 Action 物件並呼叫它們的 apply 方法,進而執行具體的 View 的更新操作。
      4. 此方法避免了 定義大量的 Binder 介面,其次通過遠端程序中批量執行修改擦歐總避免了大量 IPC 操作。
    4. 原始碼說明:

      1. 見原書吧。。
    5. 補充說明

      1. apply 和 reApply 的區別:前者會載入佈局並更新介面,後者只會更新介面
      2. 關於點選事件。RemoteViews 中只支援發起 PendingIntent 不支援 onClickListener 那種模式。另外, setOnClickPendingIntent 用於給普通 View 設定點選事件,不能給集合(ListView / StackvView)中的 View 設定點選事件。如果要給 ListView / StackvView 中的 itemview 設定單擊事件,必須將 setPendingIntentTemplate 和 setOnClickFillInIntent 組合使用才可以。

RemoteViews 的意義

  1. 從字面上就能猜到:RemoteViews 目的就是為了方便的更新遠端 views ,即跨程序更新 UI

    1. 當一個應用需要能夠更新另一個應用中的某個介面,這時候如果通過 AIDL實現,那麼可能會隨著介面更新操作的複雜導致效率變低。這種場景就很適合使用 RemoteViews。

    2. RemoteViews 缺點在於 它只支援一些常見的 View,不支援自定義 View。

    3. 佈局檔案的載入問題

      1. 同一個應用的多程序情形

        View view = remoteViews.apply(this,mRemoteViewsContent);
        mRemoteViewsContent.addView(view);
      2. 不同應用時

        主要是由於兩個應用的資源 ID 不一定一致,因此通過資源名稱來載入佈局檔案

        int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName());
        view view = getLayoutInflater().inflate(layoutId,mRemoteViewsContent,flase);
        remoteViews.reapply(this,view);
        mRemoteViewsContent.addView(view);