RemoteViews 的限制很多,仅支持一些基础的 View 可以在 RemoteViews API 中使用。列表是一个比较特殊的需求,在 Android 34 才更新了一些 API 提供了更好的支持。 首先需要明确的是,ListView 只能在 App Widget 中使用,无法在通知或是其他形式使用。在设置 Adapter 的 API 中有明确说明:
java
/**
* Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}.
*
* Can only be used for App Widgets.
*
* @param viewId The id of the {@link AdapterView}
* @param intent The intent of the service which will be
* providing data to the RemoteViewsAdapter
*/
public void setRemoteAdapter(@IdRes int viewId, Intent intent) {
addAction(new SetRemoteViewsAdapterIntent(viewId, intent));
}
为什么仅限 App Widgets 呢,这个问题最后解答,下面先来看看在 App Widget 中使用 ListView 的用法。
首先,需要创建一个 AppWidget,如果你使用 Android 的模版代码,会是这个样子:
kotlin
class NewAppWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
}
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val widgetText = context.getString(R.string.appwidget_text)
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.new_app_widget)
views.setTextViewText(R.id.appwidget_text, widgetText)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
在 updateAppWidget 就可以实现响应的 RemoteViews UI 的逻辑。基本的 ListView 实现逻辑是以下步骤:
- 在 xml 中添加 ListView 标签;
- 在 updateAppWidget 中使用 setRemoteAdapter 方法来给 ListView 设置 Adapter 和数据。
RemoteViews 提供不同的了 setRemoteAdapter,包括:
kotlin
public void setRemoteAdapter(@IdRes int viewId, Intent intent)
public void setRemoteAdapter(@IdRes int viewId, @NonNull RemoteCollectionItems items)
还有两个已弃用的版本,这里不列举。
setRemoteAdapter(int, Intent)
使用这个版本的 API,需要配合 RemoteViewsService 和 RemoteViewsFactory 来实现数据提供逻辑。 首先要创建一个 Service 继承自 RemoteViewsService:
kotlin
class RemoteDataService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
// ...
}
}
需要注意的是,这个 Service 需要配置一个特殊的权限:
xml
<service
android:name=".RemoteDataService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_REMOTEVIEWS"/>
因为 onGetViewFactory 方法需要返回一个 RemoteViewsFactory,所以需要定义一个 RemoteViewsFactory 的实现类:
kotlin
class RemoteFactory(private val context: Context, private val array: ArrayList<String>): RemoteViewsService.RemoteViewsFactory {
override fun onCreate() {}
override fun onDataSetChanged() {}
override fun onDestroy() {}
override fun getCount(): Int {
return array.size
}
override fun getViewAt(position: Int): RemoteViews {
val view = RemoteViews(context.packageName, R.layout.appwidget_list_item)
view.setTextViewText(R.id.item_text, array[position])
return view
}
override fun getLoadingView(): RemoteViews {
val view = RemoteViews(context.packageName, R.layout.appwidget_list_item)
view.setTextViewText(R.id.item_text, "loading...")
return view
}
override fun getViewTypeCount(): Int {
return 1
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun hasStableIds(): Boolean {
return true
}
}
这里简单实现了一下内部逻辑,传递了 context 和一个 String 数组作为构造参数,从外部将数据传递进去。其中一些方法比较重要:
- getCount:返回列表数量;
- getViewAt:返回对应 position 位置的 RemoteViews,也就是在这里进行数据和 UI 的绑定,这里需要创建 item 的 xml 来定义 Item 的布局;
- getLoadingView:支持定义 loading 状态下的 RemoteViews;
- getViewTypeCount:这里切记不要设置为 0(会一直展示 getLoadingView 返回的样式),根据实际需求确定有几种类型的 item;
- getItemId 和 hasStableIds 可以自行定义 item 位置相关的逻辑。
在实现了这些内容后,需要在 AppWidget 中对 ListView 的 Adapter 进行绑定:
kotlin
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val widgetText = context.getString(R.string.appwidget_text)
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.new_app_widget)
views.setTextViewText(R.id.appwidget_text, widgetText)
// core step 1
val intent = Intent(context, RemoteDataService::class.java)
// core step 2
views.setRemoteAdapter(R.id.appwidget_list, intent)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
其实本质上就是调用setRemoteAdapter,需要构建所需要的参数 Intent。 最后通过 appWidgetManager.updateAppWidget(appWidgetId, views)
告知小部件管理器更新小部件。
setRemoteAdapter(int, RemoteCollectionItems)
第二种方式是使用 RemoteCollectionItems,这是一种在 Android 34 新增的 API,低版本需要集成依赖:
kotlin
implementation("androidx.core:core-remoteviews:1.0.0")
并且确保 compileSdk = 34
。 与第一种方式不同的是,不需要实现一个 RemoteViewsService,直接在 updateAppWidget 中实现逻辑即可:
kotlin
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val widgetText = context.getString(R.string.appwidget_text)
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.new_app_widget)
views.setTextViewText(R.id.appwidget_text, widgetText)
// 假的数据
val array = arrayListOf<String>()
for (i in 0 until 10) {
array.add("index - $i")
}
// 根据版本使用不同的 API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val builder = RemoteViews.RemoteCollectionItems.Builder()
array.forEachIndexed { index, s ->
val view = RemoteViews(context.packageName, R.layout.appwidget_list_item)
view.setTextViewText(R.id.item_text, array[index])
builder.addItem(index.toLong(), view)
}
views.setRemoteAdapter(R.id.appwidget_list, builder.build())
} else {
val builder = RemoteViewsCompat.RemoteCollectionItems.Builder()
array.forEachIndexed { index, s ->
val view = RemoteViews(context.packageName, R.layout.appwidget_list_item)
view.setTextViewText(R.id.item_text, array[index])
builder.addItem(index.toLong(), view)
}
RemoteViewsCompat.setRemoteAdapter(context, views, appWidgetId, R.id.appwidget_list, builder.build())
}
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
setRemoteAdapter 在 Android 34 版本可以直接使用 RemoteViews 调用;而低于 34 需要使用 RemoteViewsCompat.setRemoteAdapter
来进行调用。
列表中每一个 Item 的需要通过 RemoteCollectionItems 来添加,首先创建一个 RemoteCollectionItems. Builder 对象,每一个 item 都创建一个 RemoteViews,然后通过 RemoteCollectionItems. Builder 对象的 addItem 进行添加,最后,通过 builder.build()
构造 RemoteCollectionItems 对象。
最后,appWidgetManager.updateAppWidget(appWidgetId, views)
告知小部件管理器更新小部件。
最终效果图:
源码: Github - JChunyu - android-code-demo - appwidget
总结
在上面的例子中,在调用 setRemoteAdapter 方法后,都多一个步骤:appWidgetManager.updateAppWidget(appWidgetId, views)
告知小部件管理器更新小部件。 这也就是为什么 ListView 只能用在 App Widget 中的原因,它可以通过 appWidgetManager 来更新内容。而通知或是其他用法,没有相应的机制,所以也就无法正常使用。