Android 開發(fā)藝術(shù)探索筆記之五 -- 理解 RemoteViews

學(xué)習(xí)內(nèi)容:

  • RemoteViews 在通知欄和桌面小部件上的應(yīng)用
  • RemoteViews 的內(nèi)部機(jī)制
  • RemoteViews 的意義

RemoteView 的應(yīng)用

實(shí)際開發(fā)中,RemoteViews 主要用在通知欄和桌面小部件的開發(fā)過程中。通知欄主要通過 NotificationManager 的 notify 方法實(shí)現(xiàn),桌面小部件則是通過 AppWidgetProvider 來實(shí)現(xiàn),其本質(zhì)也是一個(gè)廣播。

通知欄和桌面小部件更新界面時(shí),RemoteView 無法像 View 一樣在 Activity 中直接更新,因?yàn)榻缑孢\(yùn)行在系統(tǒng)的 SystemServer 進(jìn)程,需要跨進(jìn)程更新。

下面簡(jiǎn)單介紹 RemoteView 的應(yīng)用

  1. RemoteView 在通知欄上的應(yīng)用(主要為 自定義布局

    (適配 Android 8.0)

    //創(chuàng)建NotificationManager實(shí)例
    NotificationManager mManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    
    //創(chuàng)建NotificationChannel實(shí)例
    //參數(shù)說明:
    //id:NotificationChannel的唯一標(biāo)識(shí)
    //name:NotificationChannel的名稱,在Settings可看到
    //importance:對(duì)channel設(shè)置重要性,更改見后續(xù)表格
    NotificationChannel mChannel = new NotificationChannel("id","name",NotificationManager.IMPORTANCE_DEFAULT);
    mManager.createNotificationChannel(mChannel);
    
    //創(chuàng)建PendingIntent
    Intent intent = new Intent(this,SecondActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT);
    
    //創(chuàng)建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);
    
    //創(chuàng)建builder,并設(shè)置一系列屬性
    Notification.Builder builder = new Notification.Builder(this,"id");
    builder.setSmallIcon(R.drawable.ic_launcher_background)
            .setContentTitle("title")
            .setContentText("text")
            //以上三個(gè)為必需的屬性
            .setAutoCancel(true);
    
    //Android 7.0 之后需要通過Notification.Builder設(shè)置contentView
    builder.setCustomContentView(remoteViews).
        
    //創(chuàng)建通知
    Notification notification = builder.build();
    //推送通知
    mManager.notify(1,notification);
    

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

    ? 直觀原因 是因?yàn)?RemoteViews 沒有提供和 View 類似的 findViewById 這個(gè)方法,因此我們無法獲取到 RemoteView 中的子 View。(實(shí)際原因并非如此,后面詳細(xì)介紹)

  2. RemoteViews 在桌面小部件上的應(yīng)用

    利用 AppWidgetProvider,本質(zhì)是廣播。

    1. 定義小部件界面

      在 res/layout/ 新建一個(gè) xml 文件,命名為 widget.xml,名稱和內(nèi)容可自定義,視小部件具體需求而定。

      <?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"
            //自動(dòng)更新周期,毫秒單位
            android:updatePeriodMillis="864000"/>
      
      
    3. 定義小部件的實(shí)現(xiàn)類

      繼承 AppWidgetProvider,功能為簡(jiǎn)單的 點(diǎn)擊后隨機(jī)切換圖片。

      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);
          }
      
          // 隨機(jī)更新圖片
          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 中聲明小部件

      原因:本質(zhì)是廣播組件,因此需要注冊(cè)

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

      當(dāng)廣播到來之后,AppWidgetProvider 會(huì)自動(dòng)根據(jù)廣播的 Action 通過 onReceive 來自動(dòng)分發(fā)廣播,相關(guān)方法如下

      1. onEnable: 當(dāng)該窗口小部件第一次添加到桌面時(shí)調(diào)用的方法,可添加多次但只在第一次調(diào)用。
      2. onUpdate: 小部件被添加時(shí)或者每次小部件更新時(shí)都會(huì)調(diào)用一次該方法,小部件的更新時(shí)機(jī)是有updatePeriodMillis來指定,每個(gè)周期小部件就會(huì)自動(dòng)更新一次。
      3. onDeleted: 每刪除一次桌面小部件就調(diào)用一次。
      4. onDisabled: 當(dāng)最后一個(gè)該類型的小部件被刪除時(shí)調(diào)用該方法。
      5. onRestored:當(dāng)接收到 ACTION_APPWIDGET_RESTORED 廣播,從備份恢復(fù)小部件時(shí)調(diào)用
      6. onAppWidgetOptionsChanged:當(dāng)接收到 ACTION_APPWIDGET_OPTIONS_CHANGED 廣播,小部件的尺寸位置發(fā)生變化時(shí)調(diào)用。
      7. onReceive: 這是廣播的內(nèi)置方法,用于分發(fā)具體事件給其他方法。
  3. PendingIntent 概述

    1. 基本介紹

      1. PendingIntent 表示一種處于 Pending(待定、等待、即將發(fā)生)狀態(tài)的意圖;
      2. 典型應(yīng)用場(chǎng)景是給 RemoteViews 添加點(diǎn)擊事件,(RemoteViews 運(yùn)行在遠(yuǎn)程進(jìn)程)
      3. 通過 send 和 cancel 方法來發(fā)送和取消特定的待定 Intent。
    2. 分類

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

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

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

     參數(shù)說明:

     1. requestCode 表示 PendingIntent 發(fā)送方的請(qǐng)求碼,多數(shù)情況下設(shè)置 0 即可,另外 requestCode 會(huì)影響到 flags 的效果。
     2. flags 參數(shù):
        1. **FLAG_ONE_SHOP** 當(dāng)前的PendingIntent只能被使用一次,然后他就會(huì)自動(dòng)cancel,如果后續(xù)還有相同的PendingIntent,那么它們的send方法就會(huì)調(diào)用失敗。
        2. **FLAG_NO_CREATE** 當(dāng)前描述的PendingIntent不會(huì)主動(dòng)創(chuàng)建,如果當(dāng)前PendingIntent之前存在,那么getActivity、getService和getBroadcast方法會(huì)直接返回Null,即獲取PendingIntent失敗,無法單獨(dú)使用,平時(shí)很少用到。
        3. **FLAG_CANCEL_CURRENT** 當(dāng)前描述的PendingIntent如果已經(jīng)存在,那么它們都會(huì)被cancel,然后系統(tǒng)會(huì)創(chuàng)建一個(gè)新的PendingIntent。對(duì)于通知欄消息來說,那些被cancel的消息單擊后無法打開。
        4. **FLAG_UPDATE_CURRENT** 當(dāng)前描述的PendingIntent如果已經(jīng)存在,那么它們都會(huì)被更新,即它們的Intent中的Extras會(huì)被替換為最新的。
  1. 匹配規(guī)則

    1. 如果兩個(gè) PendingIntent 內(nèi)部的 Intent 相同且 requestCode 也相同,那么二者相同
    2. Intent 相同的匹配規(guī)則:Intent 的 ComponentName 和 intent-filter 都相同。Extras 不參與 Intent的匹配過程。

RemoteView 的內(nèi)部機(jī)制

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

    參數(shù)說明:

    1. packageName:當(dāng)前應(yīng)用的包名
    2. layoutId:待加載的布局文件
  2. 限制 -> 支持的 View 類型有限

    1. LayoutFrameLayout,LineanLayout,RelativeLayoutGridLayout
    2. ViewAnalogClockButton,ChronometerImageButton,ImageViewProgressBar,TextView,ViewFlipperListViewGridView,StackViewAdapterViewFlipper,ViewStub
  3. 特殊之處

    1. RemoteView 沒有提供 findViewById 方法,因此無法直接訪問里面的 View 元素,而必須通過 RemoteViews 所提供的一些列 set 方法來完成,這時(shí)因?yàn)?RemoteView 在遠(yuǎn)程進(jìn)程中顯示
    2. 一系列 set 方法 是通過反射來完成的。
  4. 工作流程

    1. 前置:通知欄和桌面小部件分別由 NotificationManager 和 AppWidgetManager 管理,而 NotificationManager 和 AppWidgetManager 通過 Binder 分別和 SystemServer 進(jìn)程中的 NotificationManagerService(NMS) 以及 AppWidgetService(AWS) 進(jìn)行通信。布局文件實(shí)際是在 NMS 和 AWS 中被加載的,而運(yùn)行在 SystemServer 中,這就和我們的進(jìn)程構(gòu)成了 跨進(jìn)程通信 的場(chǎng)景。

    2. 具體流程

      1. 首先 RemoteViews 通過 Binder 傳遞到 System Server 進(jìn)程(RemoteViews 實(shí)現(xiàn)了 Parcelable 接口)。系統(tǒng)會(huì)根據(jù) RemoteViews 中的包名等信息去得到該應(yīng)用的資源。

      2. 然后通過 LayoutInflater 去加載 RemoteViews 中的布局文件。(對(duì)于 SystemServer 進(jìn)程來講,加載的只是一個(gè)普通的 view,只不過對(duì)于我們的進(jìn)程來講是 遠(yuǎn)程的)

      3. 接著系統(tǒng)對(duì) View 執(zhí)行一系列界面更新任務(wù),這些任務(wù)通過 set 方法來提交。這些更新不是立刻執(zhí)行,而是在 RemoteViews 中記錄所有更新操作,等到 RemoteViews 被加載以后才能執(zhí)行。

        到此時(shí),RemoteViews 就可以在 SystemServer 進(jìn)程中顯示了。

      4. 當(dāng)需要更新 RemoteViews 時(shí),調(diào)用一些列 set 方法并通過 NotificationManager 和 AppWidgetManager 來提交更新任務(wù),具體操作也是在 SystemServer 進(jìn)程中完成。

    3. 進(jìn)一步說明 -- 跨進(jìn)程

      1. 系統(tǒng)不直接通過 Binder 支持所有的 View 和 View 操作,否則 View 的方法龐大,同時(shí) IPC 操作會(huì)影響效率
      2. 系統(tǒng)提供了一個(gè) Action 概念, Action 實(shí)現(xiàn)了 Parcelable 接口,代表一個(gè) View 操作。
      3. 系統(tǒng)首先將 View 操作封裝到 Action 對(duì)象并將這些對(duì)象跨進(jìn)程傳輸?shù)竭h(yuǎn)程進(jìn)程,接著在遠(yuǎn)程進(jìn)程中執(zhí)行 Action 對(duì)象中的具體操作。遠(yuǎn)程進(jìn)程通過 RemoteViews 的 apply 方法來進(jìn)行 View 的更新操作,Remoteview 的 apply 方法內(nèi)部會(huì)遍歷所有的 Action 對(duì)象并調(diào)用它們的 apply 方法,進(jìn)而執(zhí)行具體的 View 的更新操作。
      4. 此方法避免了 定義大量的 Binder 接口,其次通過遠(yuǎn)程進(jìn)程中批量執(zhí)行修改擦歐總避免了大量 IPC 操作。
    4. 源碼說明:

      1. 見原書吧。。
    5. 補(bǔ)充說明

      1. apply 和 reApply 的區(qū)別:前者會(huì)加載布局并更新界面,后者只會(huì)更新界面
      2. 關(guān)于點(diǎn)擊事件。RemoteViews 中只支持發(fā)起 PendingIntent 不支持 onClickListener 那種模式。另外, setOnClickPendingIntent 用于給普通 View 設(shè)置點(diǎn)擊事件,不能給集合(ListView / StackvView)中的 View 設(shè)置點(diǎn)擊事件。如果要給 ListView / StackvView 中的 itemview 設(shè)置單擊事件,必須將 setPendingIntentTemplate 和 setOnClickFillInIntent 組合使用才可以。

RemoteViews 的意義

  1. 從字面上就能猜到:RemoteViews 目的就是為了方便的更新遠(yuǎn)程 views ,即跨進(jìn)程更新 UI

    1. 當(dāng)一個(gè)應(yīng)用需要能夠更新另一個(gè)應(yīng)用中的某個(gè)界面,這時(shí)候如果通過 AIDL實(shí)現(xiàn),那么可能會(huì)隨著界面更新操作的復(fù)雜導(dǎo)致效率變低。這種場(chǎng)景就很適合使用 RemoteViews。

    2. RemoteViews 缺點(diǎn)在于 它只支持一些常見的 View,不支持自定義 View。

    3. 布局文件的加載問題

      1. 同一個(gè)應(yīng)用的多進(jìn)程情形

        View view = remoteViews.apply(this,mRemoteViewsContent);
        mRemoteViewsContent.addView(view);
        
      2. 不同應(yīng)用時(shí)

        主要是由于兩個(gè)應(yīng)用的資源 ID 不一定一致,因此通過資源名稱來加載布局文件

        int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName());
        view view = getLayoutInflater().inflate(layoutId,mRemoteViewsContent,flase);
        remoteViews.reapply(this,view);
        mRemoteViewsContent.addView(view);
        
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 今天聽曾仕強(qiáng)教授講解《論語》中“知之為知之,不知為不知,是知也”時(shí),講了個(gè)故事,首先解釋下“知之為知之,不知為不知...
    o鹿鳴閱讀 374評(píng)論 1 2
  • 《吉檀迦利》在剛開始學(xué)的時(shí)候,懵懵懂懂,應(yīng)付著學(xué)下去,不懂得它的真正含義,許多比喻都看不出來本體。因?yàn)樵趧傞_...
    崔禹喆閱讀 319評(píng)論 1 1
  • 踏足那會(huì)兒,正是隆冬 深夜的夢(mèng),全都是不安的躁動(dòng) 寒風(fēng)還肆虐的時(shí)候,你就坐上火車 趕著離開了,這么久 這么遠(yuǎn)的路,...
    洛城未晞閱讀 221評(píng)論 0 1
  • 以前真的不能體會(huì)農(nóng)民對(duì)土地的愛,因?yàn)樘锏亟o我的記憶不深,并隨著年齡的成長(zhǎng)離我越來越遠(yuǎn)。一片只是能種點(diǎn)東西的土,...
    54小愈閱讀 293評(píng)論 0 0

友情鏈接更多精彩內(nèi)容