Android知識進階樹——RemoteViews使用和原理詳解

Alex@W發表於2019-01-25

1、初識RemoteViews

在我們平時的開發中,使用RemoteViews的機會並不是很對,可能多數還是在自定義通知介面時,但RemoteViews憑藉可以跨程式更新的特點,可以幫助我們實現不同的產品效果,Android中官方的使用就是通知和桌面小部件,今天就一起來看看它是如和使用和如何跨程式傳輸的;

簡介

1.1、控制限制
  • 對於小工具可用的唯一手勢
  1. 觸控
  2. 垂直滑動
1.2、 支援佈局

RemoteViews雖然可以很容易的實現跨程式的控制檢視,但並非所有的View都支援跨程式使用,根據GooGle官方文件指出只支援以下ViewGroup和View,不知持他們的子類和自定義View,所以在寫RemoteViews的佈局檔案時應注意選擇

  • 支援的佈局
  1. FrameLayout
  2. LinearLayout
  3. RelativeLayout
  4. GridLayout
  • 支援的View
  1. 一般View:Button、ImageButton、TextView、ImageView、ProgressBar
  2. 集合:ListView、GridView、StackView、AdapterViewFlipper
  3. 其餘View:AnalogClock、Chronometer、ViewFlipper

2、自定義通知介面

2.1、Notification

通知中的使用比較簡單也比較固定,建立RemoteViews匯入佈局並設定點選事件,然後將檢視設定為通知的contentView:

RemoteViews notificationLayout = new RemoteViews(getPackageName(), R.layout.notification);
notificationLayout.setOnClickPrndingIntent(…,…)//設定佈局中的點選事件(單個View的PendingIntent)
notification.contentView = notificationLayout
notification.contentIntent = … // 設定整個通知的PendingIntent
複製程式碼

3、AppWidget

另一個使用場景就是桌面小部件,桌面小部件確實豐富了產品的使用,更方便了使用者的適應這點本人在開發中涉及到的很少,AppWidget的開發雖然比通知使用複雜一些但也是有章可循,只要遵循每一步的流程即可實現,下面一起實現一個桌面小部件:

3.1、AppWidgetProvider

AppWidgetProvider是BroadcastReceive的子類,主要用於接收小部件操作或修改時的廣播意圖,AppWidget會根據狀態的不同傳送以下廣播:

  • ACTION_APPWIDGET_UPDATE:在每個小部件更新時傳送廣播
  • ACTION_APPWIDGET_DELETED:在每次刪除小部件時傳送廣播
  • ACTION_APPWIDGET_ENABLED:第一次新增小部件時傳送廣播
  • ACTION_APPWIDGET_DISABLED:刪除最後一個小部件時傳送廣播
  • ACTION_APPWIDGET_OPTIONS_CHANGED:當AppWidget內容修改時傳送廣播

AppWidgetProvider除了直接監聽廣播外,其內部簡化了廣播的使用,提供了不同狀態的回撥方法,在開發中也主要使用這些方法即可,具體如下:

  1. onUpdate():桌面小部件的更新方法,當使用者新增App Widget時會回撥一次,然後會按照updatePeriodMillis間隔迴圈呼叫
  2. onAppWidgetOptionsChanged():首次建立佈局視窗時或視窗大小調整時回撥
  3. onDelete():每次刪除桌面小部件時都會回撥此方法
  4. onEnabled(Context):僅在第一次新增AppWidget例項時回撥此方法(可執行初始化操作,如:開啟資料庫)
  5. onDisabled(Context):當刪除最後一個小部件時回撥(可以執行清理操作:如刪除資料庫)

既然AppWidgetProvider是廣播的子類,所以它的使用也必須在清單檔案中完成註冊:

<receiver android:name="MyAppWidgetProvider">
        <intent-filter>
        //配置AppWidget的意圖過濾
       <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
       </intent-filter>
        // 新增設定appwidget_info的xml檔案
       <meta-data android:name="android.appwidget.provider"
               android:resource="@xml/example_appwidget_info" />
</receiver>
複製程式碼
3.2、ConfigActivity

ConfigActivity顧名思義,用來設定桌面小部件,它在第一次新增小部件時會直接接入配置介面,可以在其中提供RemoteViews的相關配置,在配置完成後退出活動即可自動更新檢視,具體實現方式分兩步:

  • 建立Activity並在清單檔案中配置隱式啟動
<activity android:name=".OtherActivity">
    <intent-filter>
        //必須設定APPWIDGET_CONFIGURE意圖用於隱式啟動活動
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter>
</activity>
複製程式碼
  • 在AppWidgetProviderInfo XML檔案中宣告配置活動
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   android:configure="com.alex.kotlin.remoteview.OtherActivity">
</appwidget-provider>
複製程式碼
  • 使用細節
  1. 在配置結束後在返回的setResult()中,必須返回本次修改的AppWidget的ID
3.3、AppWidgetProviderInfo

AppWidgetProviderInfo主要用於設定AppWidget的基本資料,如:佈局、尺寸、更新頻率等,所有資訊設定在xml檔案中,並在清單檔案中配置xml檔案:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="86400000"
    android:previewImage="@drawable/preview"
    android:initialLayout="@layout/example_appwidget"
    android:configure="com.example.android.ExampleAppWidgetConfigure"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>
複製程式碼

xml標籤屬性:

  1. minWidth 和 minHeight :預設情況下應用程式小部件的最小空間
  2. updatePeriodMillis:定義App Widget框架呼叫update()方法的頻率
  3. android:initialLayout:指向定義應用程式小部件佈局的佈局資源
  4. android:previewImage:指定應用程式小部件新增時的預覽圖片
  5. android:widgetCategory:配置應用程式視窗小部件是否可以顯示在主螢幕(Home Sub螢幕)、鎖定螢幕(KEGHARID)上
  6. resizeMode :指定可調整小部件的調整規則(水平或豎直)
3.4、桌面小部件實戰
  • 建立AppWidgetProvider的繼承類重寫update()
class WidgetProvider : AppWidgetProvider() {
    override fun onUpdate(context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray?) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        val remoteView = RemoteViews(context?.packageName,R.layout.remoteview)
        val pendingIntentClick = PendingIntent.getActivity(context,0,Intent(context,MainActivity::class.java),0)
        remoteView.setBitmap(R.id.imageView,"setImageBitmap", BitmapFactory.decodeResource(context?.resources,R.drawable.a_round))
        remoteView.setOnClickPendingIntent(R.id.button,pendingIntentClick)
        for (id in appWidgetIds!!){
            appWidgetManager?.updateAppWidget(id,remoteView)
        }
複製程式碼

上面程式中建立了AppWidgetProvider的子類,在onUpdate()中創了RemoteView並設定資料,最後使用AppWidgetManager更新AppWidget

  • 建立xml檔案配置AppwidgetProviderInfo資料
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="100dp"
android:minHeight="100dp"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/remoteview"
android:previewImage="@mipmap/ic_launcher_round"
android:configure="com.alex.kotlin.remoteview.OtherActivity"
android:widgetCategory="home_screen">
</appwidget-provider>
複製程式碼
  • 在清單檔案中註冊WidgetProvider
<receiver android:name=".test.WidgetProvider">
    <meta-data android:name="android.appwidget.provider"
        android:resource="@xml/widget_provider_info"/>
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
        <action android:name="com.example.administrator.WidgetProvider.action.click"/>
    </intent-filter>
</receiver>
複製程式碼
  • 執行程式後新增桌面小部件效果如下
    在這裡插入圖片描述
  • 新增ConfigActivity後(新增方式見上面)執行效果:
  1. 在預覽介面為圓形Android 圖示
  2. 新增Widget後介面顯示圓形Icon和Button
  3. 新增配置介面,在配置介面中修改為 方形Icon 和Button
    在這裡插入圖片描述
3.5、列表小部件

在桌面小部件使用中,除了上面的使用還有一種就是列表小部件,即在桌面中新增顯示資料的列表如:ListView;此處不能使用RecyclerView,而且在此處的ListView使用方式也有所不同,在建立列表小部件之前,先介紹兩個類:

  1. RemoteViewsService:提供建立RemoteViewsFactory的例項
  2. RemoteViewsFactory:它的作用就和ListView使用的Adapter作用相同,都是根據資料設定ListView的Item

下面一起實現一個列表的AppWidget,主要實現步驟如下:

  • 實現並註冊RemoteViewsService服務,重寫方法用於建立RemoteViewsFactory的例項
class RemoteServiceImpl : RemoteViewsService() {
    override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
       return WidgetFactory(applicationContext)
    }
}

複製程式碼
  • 在清單檔案中註冊服務
<service android:name=".appwidget.AppWidgetService"
//設定RemoteViews的許可權
android:permission="android.permission.BIND_REMOTEVIEWS" />
複製程式碼
  • 在AppWidgetProvider中的update()中初始化RemoteView和列表
const val CLICK_ACTION: String = "com.example.administrator.WidgetProvider.action.click"
val intent = Intent(context, RemoteServiceImpl::class.java)  //設定繫結List資料的Service
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds!![0])
intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))
remoteView.setRemoteAdapter(R.id.listView, intent)  //為RemoteView的List設定適配服務
 
val tempIntent = Intent(CLICK_ACTION)  //建立點選的臨時Intent
tempIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
remoteView.setPendingIntentTemplate( //設定ListView中Item臨時佔位Intent
    R.id.recyclerView,PendingIntent.getBroadcast(context, 0, tempIntent, PendingIntent.FLAG_CANCEL_CURRENT))
appWidgetManager?.updateAppWidget(appWidgetIds!![0],remoteView)
複製程式碼

針對上面的程式有幾點說明:

  1. 建立Intent用於啟動服務RemoteServiceImpl,並使用setRemoteAdapter將其設定給ListView
  2. 點選事件由PendingIntent傳遞的,而對於列表的點選事件為避免為每個item建立PendingIntent,此處使用setPendingIntentTemplate()為真個ListView設定佔位的PendingIntent
  • 實現RemoteViewsFactory類重寫方法,為list的每個item設定資料
  public RemoteViews getViewAt(int i) {      // 設定每個item的資料 
  val remoteViews = RemoteViews(context.packageName,R.layout.remoteview)
  remoteViews.setTextViewText(R.id.button,listArray[position])
  val intent = Intent(WidgetProvider.CLICK_ACTION)
  intent.putExtra("Extra",listArray[position])
  remoteViews.setOnClickFillInIntent(R.layout.remoteview,intent). //設定佔位填充的Intent
  return remoteViews
 }
複製程式碼
  1. RemoteViewsFactory中的方法和Adapter基本一樣使用也很簡單
  2. 在RemoteViewsFactory()中為每個Item設定資料時,使用setOnClickFillInIntent()填充每個Item的點選事件,此處設定的Intent會和前面設定的臨時PendingIntent共同完成點選操作
  • 執行程式新增後效果:

在這裡插入圖片描述

  • 響應列表的點選事件 在設定ListView的點選事件時使用PendingIntent.getBroadcast(),所以Item的點選事件是以廣播形式傳送的,要響應點選操作只需在AppWidgetProvider的onReceiver()中接收廣播並更新AppWidget介面;
override fun onReceive(context: Context?, intent: Intent?) {
        super.onReceive(context, intent)
        when (intent?.action) {
            CLICK_ACTION -> {
             val positionDrawable = intent.getIntExtra("Extra", 0)
                val remoteView = RemoteViews(context?.packageName, R.layout.remoteview)
                remoteView.setImageViewBitmap(
                    R.id.imgBig,
                    BitmapFactory.decodeResource(context?.resources, WidgetFactory.getDrawable(positionDrawable))
                )
                val manager = AppWidgetManager.getInstance(context)
                val componentName = ComponentName(context, WidgetProvider::class.java)
                manager.updateAppWidget(componentName,remoteView)
            }
        }
    }
複製程式碼

上面程式碼實現的是在點選ListView的Item時,將RemoteViews中的大圖片換成點選Item對應的圖片,效果如下:

在這裡插入圖片描述

4、RemoteViews的工作原理

RemoteViews主要用途是通知 和 桌面小部件,這兩者分別由NotificationManger 和 Appwidgetmanger 管理,NotificationManger 和 AppwidgetManger 通過Binder 與SystemServer中的NotificationMangerServer 和 AppwidgetServer 實現程式通訊, 那它是如何跨程式控制佈局的呢?我們設定的佈局又是何時被載入的呢?帶著這個問題我們一起分析下其內部的工作原理:

  • setTextViewText(int viewId, CharSequence text)
public void setTextViewText(int viewId, CharSequence text) {
    setCharSequence(viewId, "setText", text);  // 呼叫setCharSequence,傳入方法名
}
public void setCharSequence(int viewId, String methodName, CharSequence value) {
    addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));    // 新增一個反射的Action
}
private void addAction(Action a) {
...
if (mActions == null) {
        mActions = new ArrayList<Action>();   
    }
    mActions.add(a);        // 將Action 儲存在集合中
    a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}
複製程式碼

本次原理分析以setTextViewText()為例,上面程式執行以下操作:

  1. 呼叫setCharSequence()並傳入方法名和引數值
  2. 建立ReflectionAction例項,儲存操作View 的ID、方法名、引數值
  3. 將ReflectionAction例項儲存在mActions中

AppWidgetManager提交更新之後RemoteViews便會由Binder跨程式傳輸到SystemServer程式中 ,之後在這個程式 RemoteViews會執行它的apply方法或者reapply方法

  • apply()
  1. 作用:載入佈局到ViewGroup中
  2. 與apply()方法作用類似的還有reApply(),二者區別在於:apply載入佈局並更新佈局、reApply只更新介面
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
    RemoteViews rvToApply = getRemoteViewsToApply(context);  // 獲取之前建立時儲存的RemoteViews
    View result = inflateView(context, rvToApply, parent);  // 呼叫inflateView()匯入佈局
    loadTransitionOverride(context, handler);
    rvToApply.performApply(result, parent, handler);  // 呼叫 performApply 執行apply()
    return result;
}
複製程式碼

執行操作:

  1. 獲取建立的RemoteViews例項
  2. 通過呼叫inflateView()方法載入佈局到佈局容器parent中
  3. 呼叫RemoteViews的performApply()執行儲存的Action
  • inflateView()
private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
...
LayoutInflater inflater = (LayoutInflater)
        context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflater.inflate(rv.getLayoutId(), parent, false);  // 匯入佈局
}
複製程式碼

inflateView()中只是獲取LayoutInflater例項,然後根據儲存的layout檔案,將檢視匯入佈局到parent中

  • 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);   // 獲取之前儲存的 反射的Action
            a.apply(v, parent, handler);  // 呼叫Action的Apply()方法
        }
    }
}
複製程式碼

performApply中就幹了一件事,取出之前儲存Action的集合mActions,迴圈執行其中的每個Action執行其apply(),從上面我們直到此處儲存的是ReflectionAction例項,所以一起看看ReflectionAction中apply()方法;

  • ReflectionAction中apply()
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
    final View view = root.findViewById(viewId); //獲取Action儲存View的Id
    
    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);
    }
}
複製程式碼

RemoteViews的工作過程總結如下:

  1. RemoteViews在呼叫set方法後並不會直接更新佈局,此時會建立反射Action儲存在ArrayList中
  2. RemoteView在跨程式設定後,通過呼叫apply()和reapply()載入和更新佈局
  3. 載入佈局完成後,從ArrayList中遍歷所有的Action,執行其apply()
  4. 在apply()方法中,根據儲存的方法名和引數,反射執行方法修改介面

相關文章