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

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

一、定义 ListDemoWidget

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

package com.qinshou.appwidgetsdemoimport 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.JSONObjectclass 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.appwidgetsdemoimport android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import org.json.JSONArrayclass 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) ?: returnval 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) ?: returnval 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]// 创建在当前索引位置要显示的Viewval 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_backgroundelse 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"><ListViewandroid: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"><ImageViewandroid:id="@+id/iv_done"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@android:drawable/checkbox_on_background" /><TextViewandroid: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...>...<serviceandroid:name=".ListDemoService"android:enabled="true"android:exported="true"android:permission="android.permission.BIND_REMOTEVIEWS" />...<receiverandroid:name=".ListDemoWidget"android:exported="true"android:label="ListDemo小组件"><intent-filter><action android:name="android.appwidget.action.APPWIDGET_UPDATE" /></intent-filter><meta-dataandroid: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)// 为集合设置待定 intentval itemClickIntent = Intent(context, ListDemoWidget::class.java).apply {action = ACTION_NOTIFY_ITEM_DONE_CHANGEDputExtra(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]// 创建在当前索引位置要显示的Viewval 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_backgroundelse 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) ?: returnval 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

好记性不如烂笔头之 App widgets(二)相关推荐

  1. 好记性不如烂笔头之 App widgets(一)

    在 iOS 14 更新的时候,新增了一个功能--桌面小组件.当时我一看,妈呀,都说 Android 一直在模仿 iOS,但是这个小组件不一直都是 Android 有的功能吗,终于轮到被 iOS 模仿一 ...

  2. 好记性不如烂笔头——Vite篇

    茶已备好,只待君来!感谢关注 前端点线面 (>‿<),本号定期推荐原创深度好文,帮助每一位在前端领域打拼的伙伴们走向前列,此外关注我获取最前沿知识点.海量学习资料.<前端百题斩> ...

  3. 好记性不如烂笔头——C++篇

    大家好,我是前端点线面,毕业于华中科技大学,非科班出身的一枚新时代农民工,现在是百度前端研发工程师,著有<前端百题斩>.数十篇学习思维导图(go.React.Redux.Vue.Vuex. ...

  4. 好记性不如烂笔头——Vuex篇

    俗话说的好"好记性不如烂笔头",今天秉承着后期复习方便的态度,整理了一份Vuex基础知识点,并以思维导图的方式呈现出来,方便跟老铁们一起查漏补缺. Vuex.png 一.基础 1. ...

  5. 好记性不如烂笔头——记录项目过程中的编译错误及解决方法(持续记录中)

    文章目录 前言 前言 记录做项目的时候编译问题,好记性不如烂笔头,下次碰到相同的问题也可以方便查阅 2023.2.10 问题1:编写makefile脚本后,执行make all,遇到如下问题 解决方法 ...

  6. 进来在看《How Tomcat Works》这本书,将读书笔记贴在这里,好记性不如烂笔头。

    进来在看<How Tomcat Works>这本书,将读书笔记贴在这里,好记性不如烂笔头.  最简单的一个服务器,一个很简单的结果.  HttpServer构建ServerSocket,每 ...

  7. 好记性不如烂笔头:会议纪要本

    开发的大多数经历都用在思考代码如何写上,当你在进行某个项目时,突然开会又说要弄其他的项目,或者又是新的需求,需求错综复杂,多个项目交织在一起的时候容易乱. 开会时需要一个会议纪要本,会上讲了哪些关于你 ...

  8. 好记性不如烂笔头-笔记记录方法公开

    见字如面,我是东北码农. 俗话说,好记性不如烂笔头,在工作学习中记笔记是很重要的.今天介绍一下自己在工作中如何记录笔记,以及给大家安利一下win10自带的OneNote. 1.记录什么 1.1.流水账 ...

  9. 好记性不如烂笔头-linux学习笔记4apache相关知识

    好记性不如烂笔头-linux学习笔记4apache相关知识 apache 启动有2种模式 1是prefork模式,每个进程对应一个线程,如果是比较稳定的平台,那么prefork模式是worker模式 ...

最新文章

  1. Java多线程设计模式(4)线程池模式
  2. oracle替换表merge,sql – Oracle – 如何使用merge根据其他表和列中的值更新列
  3. sysbench 项目测试过程与分析
  4. Angular特殊的选择器:host,:host-context,::ng-deep
  5. window下pip 用不了的一种解决办法
  6. 三剑客之sed常用操作
  7. 怎么配置iptv服务器信息,请配置iptv服务器信息
  8. python中multiple函数_关于多处理:在Python中将多个参数传递给pool.map()函数
  9. 图文+动画讲解排序算法总结!!
  10. java 自动类型转换_彻底理解Java中的基本数据类型转换(自动、强制、提升)
  11. 7-5 sdut-验证“哥德巴赫猜想” (10 分)(优化素数判断)
  12. 计算机图形学的未来前景,计算机图形学的发展前景
  13. MATLAB struct函数(结构体数组)
  14. mac 文字识别软件ocr_树洞OCR文字识别软件for mac-树洞OCR文字识别mac版v1.2.0 最新版-腾牛苹果网...
  15. 来自30岁女程序员的自我唤醒
  16. KafKa 开启 SASL 验证
  17. 如何在IDEA中创建Web项目
  18. .net的反射技术(2)深究及 性能比较
  19. html2canvas图片跨域问题
  20. 有IBM Guardium 看清谁动了我的数据

热门文章

  1. (附源码)计算机毕业设计SSM羽毛球场地管理系统
  2. codeforces 1177B
  3. qmail 相关问题
  4. 双通道连续波多普勒雷达测速模型 - Matlab仿真
  5. 51单片机基础之矩阵键盘4X4
  6. 大规模部署lxc容器遇到的若干问题
  7. 经典升级,长直播,马上开课 | 第 19 期高级转录组分析和R数据可视化火热报名中!!!...
  8. OpenCV--014: 图像插值
  9. 汇编语言--jmp指令
  10. 区块链游戏为何如此火?大概是因为投机者和“韭菜”太多