The usage of a ListView in RemoteViews

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 实现逻辑是以下步骤:

  1. 在 xml 中添加 ListView 标签;
  2. 在 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;
  • getItemIdhasStableIds 可以自行定义 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 来更新内容。而通知或是其他用法,没有相应的机制,所以也就无法正常使用。

相关推荐
sun0077006 小时前
android ndk编译valgrind
android
AI视觉网奇8 小时前
android studio 断点无效
android·ide·android studio
jiaxi的天空8 小时前
android studio gradle 访问不了
android·ide·android studio
No Silver Bullet9 小时前
android组包时会把从maven私服获取的包下载到本地吗
android
catchadmin9 小时前
PHP serialize 序列化完全指南
android·开发语言·php
tangweiguo0305198710 小时前
Kable使用指南:Android BLE开发的现代化解决方案
android·kotlin
00后程序员张13 小时前
iOS App 混淆与资源保护:iOS配置文件加密、ipa文件安全、代码与多媒体资源防护全流程指南
android·安全·ios·小程序·uni-app·cocoa·iphone
柳岸风14 小时前
Android Studio Meerkat | 2024.3.1 Gradle Tasks不展示
android·ide·android studio
编程乐学14 小时前
安卓原创--基于 Android 开发的菜单管理系统
android
whatever who cares16 小时前
android中ViewModel 和 onSaveInstanceState 的最佳使用方法
android