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 来更新内容。而通知或是其他用法,没有相应的机制,所以也就无法正常使用。

相关推荐
C4rpeDime2 小时前
自建MD5解密平台-续
android
鲤籽鲲3 小时前
C# Random 随机数 全面解析
android·java·c#
m0_548514777 小时前
2024.12.10——攻防世界Web_php_include
android·前端·php
凤邪摩羯8 小时前
Android-性能优化-03-启动优化-启动耗时
android
凤邪摩羯8 小时前
Android-性能优化-02-内存优化-LeakCanary原理解析
android
喀什酱豆腐8 小时前
Handle
android
m0_7482329210 小时前
Android Https和WebView
android·网络协议·https
m0_7482517210 小时前
Android webview 打开本地H5项目(Cocos游戏以及Unity游戏)
android·游戏·unity
m0_7482546612 小时前
go官方日志库带色彩格式化
android·开发语言·golang
zhangphil12 小时前
Android使用PorterDuffXfermode模式PorterDuff.Mode.SRC_OUT橡皮擦实现“刮刮乐”效果,Kotlin(2)
android·kotlin