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

一、初識RemoteViews

在咱們平時的開發中,使用RemoteViews的機會並非很對,可能多數仍是在自定義通知界面時,但RemoteViews憑藉能夠跨進程更新的特色,能夠幫助咱們實現不一樣的產品效果,Android中官方的使用就是通知和桌面小部件,今天就一塊兒來看看它是如和使用和如何跨進程傳輸的;java

簡介android

1.一、控制限制
  • 對於小工具可用的惟一手勢
  1. 觸摸
  2. 垂直滑動
1.二、 支持佈局

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.一、Notification

通知中的使用比較簡單也比較固定,建立RemoteViews導入佈局並設置點擊事件,而後將視圖設置爲通知的contentView:bash

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

三、AppWidget

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

3.一、AppWidgetProvider

AppWidgetProvider是BroadcastReceive的子類,主要用於接收小部件操做或修改時的廣播意圖,AppWidget會根據狀態的不一樣發送如下廣播:框架

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

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

  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.二、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.三、AppWidgetProviderInfo

AppWidgetProviderInfo主要用於設置AppWidget的基本數據,如:佈局、尺寸、更新頻率等,全部信息設置在xml文件中,並在清單文件中配置xml文件:ui

<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.四、桌面小部件實戰
  • 建立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.五、列表小部件

在桌面小部件使用中,除了上面的使用還有一種就是列表小部件,即在桌面中添加顯示數據的列表如: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對應的圖片,效果以下:

在這裏插入圖片描述

四、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()方法中,根據保存的方法名和參數,反射執行方法修改界面
相關文章
相關標籤/搜索