[學習筆記]Android開發藝術探索:理解RemoteViews

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

RemoteViews的應用

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

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

RemoveViews在桌面小部件上的應用

定義好小部件界面
在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>

定義小部件的配置信息
在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"/>

定義小部件的實現類
這個類需要繼承AppWidgetProvider;我們這裏實現一個簡單的widget,點擊它後,3張圖片隨機切換顯示。

public class MAppWidgetProvider extends AppWidgetProvider {
    public static final String TAG = "MAppWidgetProvider";
    public static final String CLICK_ACTION = "com.zza.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.test1);
        } else if (index == 2) {
            remoteViews.setImageViewResource(R.id.iv, R.mipmap.test2);
        } else {
            remoteViews.setImageViewResource(R.id.iv, R.mipmap.test3);
        }
        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, MAppWidgetProvider.class), remoteViews);
    }
}

在AndroidManifest.xml中聲明小部件:
因爲桌面小部件的本質是一個廣播組件,因此必須要註冊。

<receiver android:name=".RemoveViews.MAppWidgetProvider">
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/appwidget_provider_info">
        </meta-data>
    <intent-filter>
        <action android:name="com.zza.action.click" />
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
</receiver>

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

廣播到來的時候,AppWidgetProvider會自動根據廣播的Action通過onReceive方法來分發廣播,也就是調用:

  • onEnable: 當該窗口小部件第一次添加到桌面時調用的方法,可添加多次但只在第一次調用。
  • onUpdate: 小部件被添加時或者每次小部件更新時都會調用一次該方法,小部件的更新時機updatePeriodMillis來指定,每個週期小部件就會自動更新一次。
  • onDeleted: 每刪除一次桌面小部件就調用一次。
  • onDisabled: 當最後一個該類型的小部件被刪除時調用該方法。
  • onReceive: 這是廣播的內置方法,用於分發具體事件給其他方法。

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的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處於匹配階段,分情況:

    採用FLAG_ONE_SHOT標記位,那麼後續通知中的PendingIntent會和第一條通知保持一致,包括其中的Extras,單擊任何一條通知後,其他通知均無法再打開;當所有通知被清除後。

    採用FLAG_CANCEL_CURRENT標記位,只有最新的通知可以打開,之前彈出的所有通知均無法打開。

    採用FLAG_UPDATE_CURRENT標記位,那麼之前彈出的PendingIntent會被更新,最終它們和最新的一條保存完全一致,包括其中的Extras,並且這些通知都是可以打開的。

##RemoteViews的內部機制

RemoteViews的構造方法:public RemoteViews(String packageName,int layoutId),第一個參數表示當前應用的包名,第二個參數表示待加載的佈局文件。

RemoveViews並不能支持所有View類型,支持以下:

  • Layout:FrameLayout、LinearLayout、RelativeLayout、GridLayout。
  • View:Button、ImageButton、ImageView、ProgressBar、TextView、ListView、GridView、ViewStub等。

RemoteView沒有findViewById方法,因此無法訪問裏面的View元素,而必須通過RemoteViews所提供的一系列set方法來完成,這是通過反射調用的。

通知欄和小組件分別由NotificationManager(NM)和AppWidgetManager(AWM)管理,而NM和AWM通過Binder分別和SystemService進程中的NotificationManagerService以及AppWidgetService中加載的,而它們運行在系統的SystemService中,這就和我們進程構成了跨進程通訊。

工作流程:首先RemoteViews會通過Binder傳遞到SystemService進程,因爲RemoteViews實現了Parcelable接口,因此它可以跨進程傳輸,系統會根據RemoteViews的包名等信息拿到該應用的資源;然後通過LayoutInflater去加載RemoteViews中的佈局文件。接着系統會對View進行一系列界面更新任務,這些任務就是之前我們通過set來提交的。set方法對View的更新並不會立即執行,會記錄下來,等到RemoteViews被加載以後纔會執行。

爲了提高效率,系統沒有直接通過Binder去支持所有的View和View操作。而是提供一個Action概念,Action同樣實現Parcelable接口。系統首先將View操作封裝到Action對象並將這些對象跨進程傳輸到SystemService進程,接着SystemService進程執行Action對象的具體操作。遠程進程通過RemoteViews的apply方法來進行View的更新操作,RemoteViews的apply方法會去遍歷所有的Action對象並調用他們的apply方法。這樣避免了定義大量的Binder接口,也避免了大量IPC操作。

apply和reApply的區別在於:apply會加載佈局並更新界面,而reApply則只會更新界面。

關於單擊事件,RemoteViews中只支持發起PendingIntent,不支持onClickListener那種模式。setOnClickPendingIntent用於給普通的View設置單擊事件,不能給集合(ListView/StackView)中的View設置單擊事件(開銷大,系統禁止了這種方式)。如果要給ListView/StackView中的item設置單擊事件,必須將setPendingIntentTemplate和setOnClickFillInIntent組合使用纔可以。

RemoteViews的意義

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

  • 當一個應用需要更新另一個應用的某個界面,我們可以選擇用AIDL來實現,但如果更新比較頻繁,效率會有問題,同時AIDL接口就可能變得很複雜。如果採用RemoteViews就沒有這個問題,但RemoteViews僅支持一些常用的View,如果界面的View都是RemoteViews所支持的,那麼就可以考慮採用RemoteViews。
  • 利用RemoteViews加載其他App的佈局文件與資源。
    final String pkg = "com.zza.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, mRemoteViewContent);//mRemoteViewContent是View所在的父容器
        mRemoteViewContent.addView(view);
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章