建立一個 Android Collection App Widget

Photo by Max van den Oetelaar on Unsplash
Photo by Max van den Oetelaar on Unsplash
Android Collection Widgets 是用來在 home screen 上顯示多筆相同型態的資料,例如圖片集、郵件列表等。所以,一般來說 collection widgets 可以上下滑動來顯示更多的資料。

Android Collection Widgets 是用來在 home screen 上顯示多筆相同型態的資料,例如圖片集、郵件列表等。所以,一般來說 collection widgets 可以上下滑動來顯示更多的資料。本文章將介紹如何建立一個 collection widgets。

完整程式碼可以在 下載。

建立一個 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 很相似。

參考

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Hans-Jurgen Mager on Unsplash
Read More

Kotlin Coroutine 教學

Kotlin 的 coroutine 是用來取代 thread。它不會阻塞 thread,而且還可以被取消。Coroutine core 會幫你管理 thread 的數量,讓你不需要自行管理,這也可以避免不小心建立過多的 thread。
Read More