Android Collection Widgets 是用來在 home screen 上顯示多筆相同型態的資料,例如圖片集、郵件列表等。所以,一般來說 collection widgets 可以上下滑動來顯示更多的資料。本文章將介紹如何建立一個 collection widgets。
Table of Contents
建立一個 Collection Widget
首先,我們要建立一個 app widget。如果還不熟悉怎麼建立一個 app widget 的話,請先參考以下文章。我們將延續以下文章中的 PersonInfoAppWidget 範例,將它修改成 collection widget。
修改 person_info_app_widget.xml 成如下。我們把它改成顯示一個 ListView。當 ListView 裡面沒有任何資料時,改為顯示 No Data
的 TextView。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" style="@style/Widget.CollectionAppWidgetExample.AppWidget.Container" android:layout_width="match_parent" android:layout_height="match_parent" android:theme="@style/Theme.CollectionAppWidgetExample.AppWidgetContainer"> <ListView android:id="@+id/person_info_list_view" style="@style/Widget.CollectionAppWidgetExample.AppWidget.InnerView" android:layout_width="match_parent" android:layout_height="match_parent" android:divider="@null" android:listSelector="@android:color/transparent" android:scrollbarAlwaysDrawHorizontalTrack="false" android:scrollbars="none" /> <TextView android:id="@+id/empty_view" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="No data" android:textSize="24sp" android:textStyle="bold|italic" /> </FrameLayout>
新增 person_info_item.xml。它將用於顯示在 ListView 裡的每一筆資料。
<?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="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/name_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/job_text_view" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textAlignment="textEnd" /> </LinearLayout>
RemoteViewsService 和 RemoteViewsService.RemoteViewsFactory
我們需要利用 RemoteViewsService 來提供要顯示的資料,以及每一筆資料的 view。在 RemoteViewsService 中,你可以從資料庫中取得資料,也可以 content provider 中取得資料。
就如同一般使用 ListView 時,我們需要一個 Adapter 來提供每筆資料的 view。我們要覆寫 RemoteViewsService.onGetViewFactory,並回傳 RemoteViewsService.RemoteViewsFactory。它是一個 Adapter 的 wrapper。因此,使用方式與 Adapter 幾乎相同。
class PersonInfoAppWidgetService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { return PersonInfoRemoteViewFactory(this) } } class PersonInfoRemoteViewFactory( private val context: Context, ) : RemoteViewsService.RemoteViewsFactory { private lateinit var people: List<Person> override fun onCreate() { loadData() } override fun onDataSetChanged() { loadData() } private fun loadData() { people = listOf( Person("Wayne", "Mobile Software Developer & Blogger"), Person("David", "Android Developer"), Person("Peter", "Embedded System Developer"), ) } override fun onDestroy() { } override fun getCount(): Int { print("count=${people.size}") return people.size } override fun getViewAt(position: Int): RemoteViews { return RemoteViews(context.packageName, R.layout.person_info_item).apply { val person = people[position] setTextViewText(R.id.name_text_view, person.name) setTextViewText(R.id.job_text_view, person.job) } } override fun getLoadingView(): RemoteViews? = null override fun getViewTypeCount(): Int = 1 override fun getItemId(position: Int): Long = people[position].name.hashCode().toLong() override fun hasStableIds(): Boolean = true }
在 AppWidgetProvider.onUpdate() 中,我們要呼叫 RemoteViews.setRemoteAdapter() 來設定 adapter。RemoteViews.setEmptyView() 設定當沒有資料時,要隱藏第一個參數的 view,並且顯示第二個參數的 view。
class PersonInfoAppWidget : AppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, ) { for (appWidgetId in appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId) } } } internal fun updateAppWidget( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, ) { val intent = Intent(context, PersonInfoAppWidgetService::class.java).apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) } val views = RemoteViews(context.packageName, R.layout.person_info_app_widget).apply { setRemoteAdapter(R.id.person_info_list_view, intent) setEmptyView(R.id.person_info_list_view, R.id.empty_view) } appWidgetManager.updateAppWidget(appWidgetId, views) }
最後,因為 PersonInfoAppWidgetService 是一個 Service。所以,我們必須在 AndroidManifest.xml 裡宣告它。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application> <service android:name=".PersonInfoAppWidgetService" android:permission="android.permission.BIND_REMOTEVIEWS" /> </application> </manifest>
AppWidgetManager
至目前為止,PersonInfoAppWidget 大致完成了。系統會依據 <appwidget-provider/> 的 updatePeriodMillis 的值,定期地呼叫 PersonInfoAppWidget.onUpdate() 來更新 view。然而,當 app 修改了資料時,PersonInfoAppWidget 就無法即時顯示更動後的資料。
更動資料後,app 可以呼叫 AppWidgetManager.notifyAppWidgetViewDataChanged() 來要求系統呼叫 PersonInfoAppWidget.onUpdate()。
如果 app 不知道要更新的 app widget 的 appWidgetId 的話,我們可以利用 AppWidgetManager.getappWidgetIds() 取得所有 PersonInfoAppWidget instance 的 appWidgetIds。
AppWidgetManager.getInstance(application).let { appWidgetManager -> val appWidgetIds = appWidgetManager.getAppWidgetIds( ComponentName(application, PersonInfoAppWidget::class.java) ) appWidgetManager .notifyAppWidgetViewDataChanged(appWidgetIds, R.id.person_info_list_view) }
結語
Collection widget 看似有點複雜,但是實作起來其實就好像實作一個 Adapter。和實作 ListView 或 RecycleView 很相似。
參考
- Use widget collections, Google developers.