好记性不如烂笔头之 App widgets(二)

好记性不如烂笔头之 App widgets(一)_禽兽先生不禽兽的博客-CSDN博客

之前记录了 AppWidgets 的基本用法,当我的小组件中需要展示列表的时候,发现它的方式也跟普通的列表控件不一样,而且在使用 AppWidgetProvider 时还有更需要注意的地方,在此特意也记录一下。

一、定义 ListDemoWidget

跟之前小组件一样,我们需要一个 AppWidgetProvider 的实现类:

package com.qinshou.appwidgetsdemo

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.widget.RemoteViews
import org.json.JSONArray
import org.json.JSONObject

class ListDemoWidget : AppWidgetProvider() {
    companion object {
        private const val TAG = "ListDemoWidget"
    }

    override fun onReceive(context: Context, intent: Intent) {
        super.onReceive(context, intent)
        Log.i(TAG, "onReceive : " + "action--->" + intent.action)
    }

    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        Log.i(TAG, "onUpdate")
        // 保存一个 json 串,模拟数据
        val jsonArray = JSONArray()
        for (i in 0 until 20) {
            val jsonObject = JSONObject()
            jsonObject.put("done", false)
            jsonObject.put("content", "待办事项${i}")
            jsonArray.put(jsonObject)
        }
        val sharedPreferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
        val editor = sharedPreferences.edit()
        editor.putString("todo", jsonArray.toString())
        editor.apply()
        // 为小组件添加列表控件
        for (appWidgetId in appWidgetIds) {
            val remoteViews = RemoteViews(context.packageName, R.layout.widget_list_demo)
            val intent = Intent(context, ListDemoService::class.java).apply {
                putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
            }
            remoteViews.setRemoteAdapter(R.id.lv_demo, intent)
            appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
        }
    }
}

这里重点关注 remoteViews.setRemoteAdapter(R.id.lv_demo, intent) 这一行代码,该方法需要传递两个参数,第一个参数为列表控件的 id,第二个参数为一个 Intent 对象。

既然是列表视图,一定要有一个列表控件,看这个 id 能够猜出来使用的是 ListView。之前说过小组件布局中使用的控件一定是 @RemoteView 注解修饰的控件,所以很遗憾不能使用现在主流的 RecyclerView 了,所以我们选择了 ListView。

记得之前使用 ListView 我们会定义一个 Adapter,而且为了复用还需要定义一个 ViewHolder 类去提高 ListView 的效率,但是在小组件中使用 ListView 又有些不同,不是使用 Adapter 去填充数据,而是需要一个 RemoteViewsService 和 RemoteViewsFactory 去实现,所以第二个参数 intent 就是用来启动这个填充数据的 Service 的。

二、定义 ListDemoService

这个 Service 就是重点了,它继承自 RemoteViewsService,点进源码发现 RemoteViewsService 是一个抽象类,只需要我们实现 public abstract RemoteViewsService.RemoteViewsFactory onGetViewFactory(Intent var1) 这个方法,该方法需要返回一个 RemoteViewsService.RemoteViewsFactory 对象。接着看发现 RemoteViewsService.RemoteViewsFactory 是一个接口,定义了如下方法需要我们去实现:

  • void onCreate():RemoteViewsFactory 创建时回调,可以在该方法中初始化数据。
  • void onDataSetChanged():调用 AppWidgetManager 的 notifyAppWidgetViewDataChanged() 方法时回调,表示数据发生改变,可以在该方法中更新数据。
  • void onDestroy():RemoteViewsFactory 销毁时回调,可以在该方法中释放数据。
  • int getCount():返回数据个数。
  • RemoteViews getViewAt(int var1):返回每一个 item 的视图,重要。
  • RemoteViews getLoadingView():加载中布局,如果不需要则返回 null。
  • int getViewTypeCount():返回视图类型数量,通常不需要多布局的话返回 1。
  • long getItemId(int var1):返回每一个 item 的独立 id,通常返回对应 position 就行。
  • boolean hasStableIds():Item 是否是稳定 id,没太明白该方法的具体作用,通常返回 true。

因此我们可以这样理解,RemoteViewsService 是用于小组件与适配器通信的一个服务,RemoteViewsService.RemoteViewsFactory 则是用于填充数据,相当于 ListView 的 Adapter 的角色。

上代码:

package com.qinshou.appwidgetsdemo

import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import org.json.JSONArray

class ListDemoService : RemoteViewsService() {

    override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
        return ListDemoRemoteViewsFactory(applicationContext)
    }

    private class ListDemoRemoteViewsFactory(private val context: Context) : RemoteViewsFactory {
        private var datum: MutableList<Pair<Boolean?, String?>> = ArrayList()
        override fun onCreate() {
            datum.clear()
            val sharedPreferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
            val json = sharedPreferences.getString("todo", null) ?: return
            val jsonArray = JSONArray(json)
            for (i in 0 until jsonArray.length()) {
                val jsonObject = jsonArray.getJSONObject(i)
                val done = jsonObject.optBoolean("done", false)
                val content = jsonObject.optString("content", "")
                datum.add(Pair(done, content))
            }
        }

        override fun onDataSetChanged() {
            datum.clear()
            val sharedPreferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
            val json = sharedPreferences.getString("todo", null) ?: return
            val jsonArray = JSONArray(json)
            for (i in 0 until jsonArray.length()) {
                val jsonObject = jsonArray.getJSONObject(i)
                val done = jsonObject.optBoolean("done", false)
                val content = jsonObject.optString("content", "")
                datum.add(Pair(done, content))
            }
        }

        override fun onDestroy() {
            datum.clear()
        }

        override fun getCount(): Int {
            return datum.size
        }

        override fun getViewAt(position: Int): RemoteViews {
            val item = datum[position]
            // 创建在当前索引位置要显示的View
            val remoteViews = RemoteViews(context.packageName, R.layout.item_lv_demo)
            // 设置要显示的内容
            remoteViews.setImageViewResource(R.id.iv_done, if (item.first != null && item.first!!) android.R.drawable.checkbox_on_background
            else android.R.drawable.checkbox_off_background)
            remoteViews.setTextViewText(R.id.tv_content, item.second)
            return remoteViews
        }

        override fun getLoadingView(): RemoteViews? {
            return null
        }

        override fun getViewTypeCount(): Int {
            return 1
        }

        override fun getItemId(position: Int): Long {
            return position.toLong()
        }

        override fun hasStableIds(): Boolean {
            return true
        }
    }
}

三、小组件的布局

布局很简单没什么好说的。

widget_list_demo.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFE165"
    android:orientation="vertical"
    android:padding="15dp"
    tools:context=".ListDemoWidget">

    <ListView
        android:id="@+id/lv_demo"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never"
        android:scrollbars="none" />
</LinearLayout>

item 的布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/ll_root"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/iv_done"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@android:drawable/checkbox_on_background" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:maxLines="1"
        android:textColor="#FF000000"
        android:textSize="15sp"
        tools:text="测试文字" />
</LinearLayout>

四、小组件的配置

这个也没什么好说的,跟之前的定义基本差不多:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialKeyguardLayout="@layout/widget_demo"
    android:initialLayout="@layout/widget_demo"
    android:minWidth="250dp"
    android:minHeight="110dp"
    android:previewImage="@mipmap/ic_launcher"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="86400000"
    android:widgetCategory="home_screen">

</appwidget-provider>

五、小组件的声明

声明时需要注意一下,处理声明小组件的 receiver 之外,还需要声明 RemoteViewsService 的实现类,并且这个 service 还需要声明 "android.permission.BIND_REMOTEVIEWS" 权限,官方说明增加该权限是为了防止其他应用自由访问你的数据,这个权限一定别忘了加,否则数据显示不出来,还挺难排查的。

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>

    <application
        ...>
        ...
        <service
            android:name=".ListDemoService"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.BIND_REMOTEVIEWS" />
        ...
        <receiver
            android:name=".ListDemoWidget"
            android:exported="true"
            android:label="ListDemo小组件">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/widget_config_list_demo" />
        </receiver>
    </application>

</manifest>

至此,我们就可以添加一个使用列表的小组件了:

六、Item 响应点击事件

列表中的 item 设置点击事件跟之前设置 button 的点击事件也不一样,一共需要设置两个地方。

首先修改 ListDemoWidget 的 onUpdate() 方法:

override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
    super.onUpdate(context, appWidgetManager, appWidgetIds)
    Log.i(TAG, "onUpdate")
    // 保存一个 json 串,模拟数据
    val jsonArray = JSONArray()
    for (i in 0 until 20) {
        val jsonObject = JSONObject()
        jsonObject.put("done", false)
        jsonObject.put("content", "待办事项${i}")
        jsonArray.put(jsonObject)
    }
    val sharedPreferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
    val editor = sharedPreferences.edit()
    editor.putString("todo", jsonArray.toString())
    editor.apply()
    // 为小组件添加列表控件
    for (appWidgetId in appWidgetIds) {
        val remoteViews = RemoteViews(context.packageName, R.layout.widget_list_demo)
        val intent = Intent(context, ListDemoService::class.java).apply {
            putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
            data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
        }
        remoteViews.setRemoteAdapter(R.id.lv_demo, intent)
        // 为集合设置待定 intent
        val itemClickIntent = Intent(context, ListDemoWidget::class.java).apply {
            action = ACTION_NOTIFY_ITEM_DONE_CHANGED
            putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
            data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
        }
        val pendingIntentTemplate = PendingIntent.getBroadcast(context, 0, itemClickIntent, PendingIntent.FLAG_UPDATE_CURRENT)
        remoteViews.setPendingIntentTemplate(R.id.lv_demo, pendingIntentTemplate)
        appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
    }
}

然后修改 ListDemoRemoteViewsFactory 的 getViewAt() 方法:

override fun getViewAt(position: Int): RemoteViews {
    val item = datum[position]
    // 创建在当前索引位置要显示的View
    val remoteViews = RemoteViews(context.packageName, R.layout.item_lv_demo)
    // 设置要显示的内容
    remoteViews.setImageViewResource(R.id.iv_done, if (item.first != null && item.first!!) android.R.drawable.checkbox_on_background
    else android.R.drawable.checkbox_off_background)
    remoteViews.setTextViewText(R.id.tv_content, item.second)
    // 为 item 设置填充 intent,响应点击事件
    val intent = Intent().apply {
        putExtra(ListDemoWidget.POSITION, position)
    }
    remoteViews.setOnClickFillInIntent(R.id.ll_root, intent)
    return remoteViews
}

主要就是设置 intent 的地方,设置列表 Adapter 的时候是调用 setPendingIntentTemplate() 方法设置的是待定 Intent,设置 item 视图的时候是调用 setOnClickFillInIntent() 方法设置的是填充 Intent。

最后在 onReceive() 方法中接收广播,处理逻辑:

companion object {
    private const val TAG = "ListDemoWidget"
    const val POSITION = "position"
    const val ACTION_NOTIFY_ITEM_DONE_CHANGED = "notifyItemDoneChanged"
}

override fun onReceive(context: Context, intent: Intent) {
    super.onReceive(context, intent)
    Log.i(TAG, "onReceive : " + "action--->" + intent.action)
    when (intent.action) {
        ACTION_NOTIFY_ITEM_DONE_CHANGED -> {
            val position = intent.getIntExtra(POSITION, -1)
            if (position == -1) {
                return
            }
            val sharedPreferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
            val json = sharedPreferences.getString("todo", null) ?: return
            val jsonArray = JSONArray(json)
            val jsonObject = jsonArray.getJSONObject(position)
            jsonObject.put("done", !jsonObject.optBoolean("done", false))
            val editor = sharedPreferences.edit()
            editor.putString("todo", jsonArray.toString())
            editor.apply()

            val appWidgetManager = AppWidgetManager.getInstance(context)
            appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetManager.getAppWidgetIds(ComponentName(context, ListDemoWidget::class.java)), R.id.lv_demo)
        }
    }
}

至此,就实现点击 item,改变数据的 done 字段,复选框根据 done 字段显示不同的状态(复选框是通过 ImageView 实现的,因为 CheckBox 控件是在 API 31 以上才支持的)。效果如下(需要移除小组件,重新添加后才能看到效果):

七、总结

在小组件中使用列表还是挺麻烦的,小组件中操作控件已经比普通方式麻烦一些了,使用列表区别于 ListView 就更加麻烦,不过从我记录的这个示例来看,做一个类似 todo 的应用还是没有问题,用小组件操作 todo,比起打开应用进入界面操作方便简直不要太多。

禽兽先生/AppWidgetsDemo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值