日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

自定义小部件Widget的探讨

發布時間:2024/3/13 编程问答 61 豆豆
生活随笔 收集整理的這篇文章主要介紹了 自定义小部件Widget的探讨 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

一、前言

二、Widget基本使用

2.1 AppWidgetProvider繼承類對象

2.2 AppWidgetProviderInfo資源配置文件

三、定制化需求

3.1 困境

3.2 自定義小部件方案

方案一、Drawable替代繪制方案

方案二、Framework的widget目錄添加自定義View文件方案


一、前言

????????在Android開發中,有時會遇到根據應用需求定制在launcher上顯示的小部件即Widget,讓用戶能夠不打開應用界面的前提下享受應用提供的一些功能服務,比如常見的音樂播放器,圖庫的圖片展示,時鐘及天氣等等,這篇文檔旨在介紹目前小部件的基本使用以及討論現有框架的局限性,針對這些局限性,我們有哪些辦法解決。

二、Widget基本使用

????????我們先用一個簡單用例介紹一下簡單的widget配置。

????????創建應用小部件,最起碼需要兩個步驟:

2.1 AppWidgetProvider繼承類對象

????????你需要寫一個類繼承自AppWidgetProvider類:???????

class MyAppWidget: AppWidgetProvider(){override fun update(...){} }

????????一般來說,重寫update這個方法即可,當添加widget或者其他操作觸發更新時就會收到更新廣播,接著就會調用這個方法,你在里面創建RemoteViews對象并進行配置,最后調用AppWidgetManager.updateAppWidget方法更新小部件UI,這個RemoteViews本質不是一個View,是一個專門用來跨進程傳輸的對象,保存小部件的布局信息,詳細的以后有機會在說。除此以外,這個類還有其他一些方法,比如刪除回調onDeleted, 大小調節回調onAppWidgetOPtionsChanged等等。

????????同時別忘了在清單文件AndroidManifest中聲明這個類。別看他是個provider,但他并不是ContentProvider,而是Broadcast廣播,所以需要進行聲明操作:

//AndroidManifest <receiver android:name=".MyAppWidget"android:exported="true"><intent-filter><action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> //監聽AppWidgetManager發出的更新廣播,只需要聲明這個即可</intent-filter><meta-dataandroid:name="android.appwidget.provider"android:resource="@xml/app_widget_my"/> //這個xml文件是AppWidgetProviderInfo配置文件,見下小節 </receiver>

????????系統服務中有一個開機啟動的叫AppWidgetService的服務 具體邏輯處理在AppWidgetServiceImpl中,這個服務就是用來管理小部件的服務,被AppWidgetManager所持有,他會監聽應用安裝的廣播:

private void registerBroadcastReceiver() {// Register for broadcasts about package install, etc., so we can// update the provider list. IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);...... }

????????所以當應用安裝后,AppWidgetServiceImpl會通過packageManager查詢到應用聲明的provider信息并創建AppWidgetProviderInfo對象。

需要指定兩個屬性:

  • android:name - 指定元數據名稱。使用android.appwidget.provider 將數據標識為AppWidgetProviderInfo描述符
  • android:resource - 指定 AppWidgetProviderInfo資源位置

2.2 AppWidgetProviderInfo資源配置文件

????????這個就是上述在清單文件中聲明的小部件資源文件,保存在項目的res/xml 文件夾中。用來描述應用小部件的元數據,如Widget的初始布局,更新頻率,預設大小,顯示位置等等。系統小部件服務就是通過讀取這個創建AppWidgetProviderInfo對象的。

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"android:minWidth="40dp" //最小寬度android:minHeight="40dp" //最小高度android:targetCellWidth="5" //預設寬度占用單元格數,這個單元格的寬高大小由launcher決定android:targetCellHeight="2" //預設高度占用單元格數android:updatePeriodMillis="86400000" // 更新頻率,規定至少30分鐘,所以實際使用場景下很尷尬,沒卵用android:previewImage="@drawable/preview" //在選擇小部件的時候看到的預覽圖片android:initialLayout="@layout/example_appwidget" //初始布局android:configure="com.example.android.ExampleAppWidgetConfigure" //添加小部件時啟動的配置Activity(可選)android:resizeMode="horizontal|vertical" //用戶手動可調整大小的方向android:widgetCategory="home_screen"> //小部件顯示的位置,主屏幕(home_screen)、鎖屏(keyguard)(Android5.0以上無效) </appwidget-provider>

????????配置好以上文件后,launcher應用作為小部件托管應用,從framework中拿到AppWidgetProviderInfo配置選項,提供在界面上嵌入小部件的服務,所以我們應用是不與launcher直接通信的,而是通過framework來通信交流。具體的源碼流程分析下次會梳理出來。我們本次的重點是下個環節。

三、定制化需求

????????以上只能滿足一些小部件開發的基本需求,而有時候產品設計需要我們定制一些更炫酷的UI以及交互,此時我們就遇到了困難。

3.1 困境

????????我們三方應用是通過將本地寫好的小部件xml布局封裝進RemoteViews里面根據AppWidgetId傳給AppWidgetManager,然后AppWidgetManager內部的AppWidgetService根據RemoteViews創建AppWidgetProviderInfo對象并保存在內部的ArrayList中,接著就會通知小部件托管應用(Launcher),托管應用就會再通過AppWidgetManager拿到最新的一系列AppWidgetProviderInfo進行刷新操作。

????????這一系列流程跨越了至少三個不同的進程,所以才會使用RemoteViews這個序列化對象保存布局資源LayoutId,等待后續需要加載時再進行inflate得到View。這就導致了我們應用的自定義View是無法使用的,因為system_server進程是找不到你應用內部定義的類的,所以使用自定義View會導致報錯,系統找不到該類信息。

Caused by: android.view.InflateException: Binary XML file line #7 in com.example.widgettest:layout/test_widget_layout: Error inflating class com.example.widgettest.MyImageViewCaused by: java.lang.ClassNotFoundException: com.example.widgettest.MyImageViewat java.lang.Class.classForName(Native Method)at java.lang.Class.forName(Class.java:454)at android.view.LayoutInflater.createView(LayoutInflater.java:819)at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1010)at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:965)at android.view.LayoutInflater.rInflate(LayoutInflater.java:1127)at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1088)at android.view.LayoutInflater.inflate(LayoutInflater.java:686)at android.view.LayoutInflater.inflate(LayoutInflater.java:538)at android.widget.RemoteViews.inflateView(RemoteViews.java:5652)at android.widget.RemoteViews.-$$Nest$minflateView(Unknown Source:0)at android.widget.RemoteViews$AsyncApplyTask.doInBackground(RemoteViews.java:5779)at android.widget.RemoteViews$AsyncApplyTask.doInBackground(RemoteViews.java:5738)at android.os.AsyncTask$3.call(AsyncTask.java:394)at java.util.concurrent.FutureTask.run(FutureTask.java:264)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)at java.lang.Thread.run(Thread.java:1012)

????????可能你會很好奇,上述下劃線中提到的inflate view操作,系統進程是怎么通過一個LayoutId就可以加載成一個View的,畢竟資源id只是在應用內部是唯一的,縱觀整個系統則不一定唯一,而id與資源文件的對應關系保存在應用的R文件中,外部進程是如何通過id確認加載哪一個布局文件的呢?其實是應用創建RemoteViews時會在內部保存我們應用進程的ApplicationInfo對象,然后launcher需要View時,會把launcher的context傳給系統進程,系統進程使用這個context和之前拿到的ApplicationInfo對象調用context.createApplicationContext方法拿到我們應用的上下文Context,就可以通過這個上下文使用資源Id訪問目標應用的資源了。

????????RemoteViews支持的View也很有限,根據Google開發文檔描述,

????????RemoteViews對象可以支持以下布局類:

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

????????以及以下View類(不包括后代類):

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

????????以及ViewStub

? ? ? ? 所以這個限制大大影響了我們在Widget上的發揮力,很多預想中的酷炫動效、交互等是無法實現的,比如我準備做的衛星天頂圖的小部件展示,以上的View是按照道理是無法承載衛星天頂圖的展示內容的,因為我們時刻監聽衛星軌跡的變化,并根據衛星方位角等的參數以及手機的旋轉矢量參數值進行繪制,這就是小部件開發中的最大困難點,原生小部件框架拓展性太差了。

????????所以面對這種不是顯示固定內容而是需要根據參數動態繪制內容的情況,我們需要另辟蹊徑(除非你能拋棄現有的小部件框架,自創可支持拓展的新框架,這個我們以后可以討論討論,有想法的同學可以分享分享)。

3.2 自定義小部件方案

方案一、Drawable替代繪制方案

????????考慮到我們在設計自定義View的時候,通常設計成類似于一個黑盒,外部只需要傳進去一些參數,View內部在draw方法里會根據參數進行繪制,這樣的話,我們可以投機取巧一下,把這個需要內部自定義繪制的View使用ImageView來替代,把這些自定義的繪制操作移到自定義Drawable里面,最后更新Widget時調用RemoteViews.setImageViewBitmap方法把自定義drawable轉成Bitmap對象傳過去以實現更新。

class TestDrawable: Drawable() {@ColorIntprivate var mColor = Color.BLUEoverride fun draw(p0: Canvas) {p0.drawColor(mColor)}fun updateColor(@ColorInt color: Int){mColor = color}...... } class TestAppWidget: AppWidgetProvider() {override fun onUpdate(context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray?) {......for (appWidgetId in appWidgetIds){val remoteView = RemoteViews(context.packageName, R.layout.test_widget_layout)remoteView.setImageViewBitmap(R.id.image_view, TestDrawable().toBitmap(200,200)) //圖片默認是藍色val intent = PendingIntent.getBroadcast(context, 0, TestWidgetManager.getInternalUpdateIntent(context), PendingIntent.FLAG_MUTABLE)remoteView.setOnClickPendingIntent(R.id.image_view, intent) //設置點擊事件的intent,這里是一個自定義廣播PendingIntentappWidgetManager?.updateAppWidget(appWidgetId, remoteView)}} override fun onReceive(context: Context?, intent: Intent?) {......//收到自定義的點擊事件廣播,我們可以主動去更新小部件,把新drawable傳過去if (intent?.action == TEST_UPDATE_APP_WIDGET){val appWidgetManager = AppWidgetManager.getInstance(context)val ids = appWidgetManager.getAppWidgetIds(ComponentName("com.example.widgettest","com.example.widgettest.TestAppWidget"))for (id in ids){val remoteView = RemoteViews(context.packageName, R.layout.test_widget_layout)val drawable = TestDrawable().apply { updateColor(Color.RED) } //將圖片變成紅色remoteView.setImageViewBitmap(R.id.image_view, drawable.toBitmap(200,200))appWidgetManager.partiallyUpdateAppWidget(id, remoteView) //局部更新}} } //test_widget_layout.xml <FrameLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"xmlns:android="http://schemas.android.com/apk/res/android"><ImageViewandroid:id="@+id/image_view"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/white"android:scaleType="centerCrop"android:clickable="true"/> </FrameLayout>

????????我們給小部件的ImageView設置了一個點擊事件的廣播通知,當用戶點擊小部件的ImageView時,會從默認的藍色變成紅色,實際效果如下圖:

????????可以發現,這個思路是可以的,我們可以根據產品需求在drawable里面進行更復雜的繪制操作,通過drawable的不斷覆蓋來實現交互與UI的交叉,甚至你可以在drawable里寫個動畫也可以。

????????這個方法不建議需要頻繁更新的情況下使用,本身小部件通信就涉及到跨進程通信,再加上需要創建Bitmap對象造成內存消耗,頻繁更新會影響到電池電量。

方案二、Framework的widget目錄添加自定義View文件方案

????????要想真正實現更多UI、交互可能性,我們目前只能通過在framework/base/core/java/android/widget目錄下把我們自定義View文件放進去,這樣系統服務在inflate我們的小部件布局文件時就不會發生找不到我們自定義View的錯誤。

????????不過要注意,我們自定義View需要遵循做到以下原則:

  • "高內聚,低耦合",和應用其他類要盡量解偶,不能解偶的自定義類,只能也放到framework目錄下,比如自定義adapter,工具類等等
  • 自定義View類必須在類上加上@RemoteView 注解,如下所示:
@RemoteView public class MyImageView extends ImageView { ...... }
  • 路徑一致。自己應用的自定義View的路徑需要和放到系統里面的自定義View路徑一致,也就是說我們需要在自己的應用項目java目錄下新增一樣的文件夾路徑:framework/base/core/java/android/widget ,然后把自定義View放進去即可。

????????在自定義View里面可以添加數據處理邏輯,視圖切換動畫等等。

????????現在系統能夠識別出我們的自定義View了,但我們在自己的應用里是無法直接把數據注入到自定義View的,畢竟更新時只有LayoutId,沒有View,我們無法通過findViewById拿到View對象再把數據注進去,那該怎么辦呢?RemoteViews也給我們提供了一系列方法:????????

//RemoteView.java public class RemoteViews implements Parcelable, Filter {public void setBoolean(int viewId, String methodName, boolean value) {...}public void setColor(int viewId, @NonNull String methodName, int colorResource) {...}public void setString(int viewId, String methodName, String value) {...} public void setBundle(int viewId, String methodName, Bundle value) {...}...... }

????????從參數上想必你大概可以猜出來是怎么注入進去的吧,沒錯,就是內部通過反射的方式把數據傳進去:

//RemoteView.BaseReflectionAction.apply(...) getMethod(view, this.methodName, param, false /* async */).invoke(view, value);

????????所以,你可以在需要主動或者被動觸發更新時,在創建RemoteViews對象時,調用上述方法即可,最后調用AppWidgetManager.updateAppWidget更新方法或者AppWidgetManager.partiallyUpdateAppWidget局部更新方法去刷新小部件的UI。

????????當然,如果你想要自定義一些集合展示并且自定義Adapter的話,要更復雜一些,因為原生框架內部已經封裝好了類似適配器的存在,已經寫死了,要修改的地方要多一些。這里就不展開講了,以前做過類似的圖庫小部件,可以實現自定義切換動畫,因為需求需要修改原生的adapter內部的一些緩存策略,所以改過,是一個比較痛苦的回憶,哈哈。

????????以上,就是實現小部件自定義繪制的兩個方案,一個是自定義Drawable,一個是自定義View,前者雖然不用修改源碼,但仍然會有許多局限性,后者雖然比較自由,能實現更酷炫的自定義UI和交互,但你必須擁有修改源碼的能力,比如手機系統研發人員。

總結

以上是生活随笔為你收集整理的自定义小部件Widget的探讨的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。