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.









