Android 桌面小组件(一)— 添加桌面小组件

App中可能存在一些高频使用的功能,例如扫一扫、开启收付款码等。如果能够在不先打开App的情况下快速使用这些功能,将显著提升用户体验。

除了在之前的文章Android 快捷方式和Android 下拉栏中的快捷设置中介绍过的两种快捷方式之外,还可以通过Android系统的App widgets实现在桌面添加小组件。本文将简单介绍如何使用App widgets相关API实现在桌面添加小组件。

添加桌面小组件

想要实现在桌面添加App的小组件,需要先完成下列三个前置步骤:

  • 使用XML定义小组件的布局样式。
  • 添加AppWidgetProviderInfo配置文件,声明小组件的必要配置,例如使用的布局、能否够改变大小、更新频率等等。
  • 实现AppWidgetProvider类,并在AndroidManifest中注册。可以通过AppWidgetProvider类接收小组件生命周期相关的广播,进行相应的处理。

接下来通过实现一个统计饮水量的小组件进行演示。

创建布局

简单实现一个统计饮水量的布局,布局中包含显示标题的TextView、显示饮水量的TextView以及增减饮水量的Button。

  • layout_example_desktop_widget.xml
ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="280dp"
    android:layout_height="90dp"
    android:background="@color/white">

    <TextView
        android:id="@+id/tv_example_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="5dp"
        android:includeFontPadding="false"
        android:text="饮水量统计"
        android:textSize="14sp" />

    <TextView
        android:id="@+id/tv_example_content"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_below="@id/tv_example_title"
        android:layout_centerHorizontal="true"
        android:layout_marginStart="10dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="10dp"
        android:gravity="center_vertical"
        android:hint="当前饮水量:0"
        android:includeFontPadding="false"
        android:textSize="12sp" />

    <Button
        android:id="@+id/btn_plus"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignTop="@id/tv_example_content"
        android:layout_toStartOf="@id/tv_example_content"
        android:includeFontPadding="false"
        android:text="加一"
        android:textSize="12sp" />

    <Button
        android:id="@+id/btn_reduce"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignTop="@id/btn_plus"
        android:layout_toEndOf="@id/tv_example_content"
        android:text="减一" />

</RelativeLayout>

你也许会好奇为什么这里根布局使用的是RelativeLayout呢?其实一开始使用的是ConstraintLayout,但是小组件所使用的RemoteViews仅支持部分控件,而ConstraintLayout恰好是不支持的,所以只能改用RelativeLayout。

设置RemoteViews时,如果布局中包含不支持的控件,编译器会警告,如图:

添加AppWidgetProviderInfo配置文件

在res/xml中创建example_desktop_widget_provider_info.xml,声明小组件必要的配置。

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:description="@string/label_desktop_widget_example_description"
    android:initialLayout="@layout/layout_example_desktop_widget"
    android:minWidth="280dp"
    android:minHeight="90dp"
    android:resizeMode="none"
    android:updatePeriodMillis="86400000"
    android:widgetCategory="home_screen" />

简单介绍一下example_desktop_widget_provider_info.xml中用到的配置:

配置项 用途
android:description 小组件的描述,用户在添加小组件时可以看到。
android:initialLayout 小组件的初始布局。
android:minWidth 小组件最小的宽度。
android:minHeight 小组件最小的高度。
android:resizeMode 改变小组件尺寸的模式,可以设置horizontal, vertical和 none。
android:updatePeriodMillis 小组件更新时间频率,官方建议尽量降低更新频率,最好每小时不超过一次。
android:widgetCategory 设置小组件可以添加的地方,可以设置home_screen、searchbox和keyguard。但是从Android 5.0开始,仅有home_screen可以生效。

这些配置对于实现一个简单的小组件来说已经足够,更多其他配置可以在官网查看。

实现AppWidgetProvider类

自定义一个ExampleDesktopWidgetProvider继承AppWidgetProvider,重写需要的方法,如下:

kotlin 复制代码
class ExampleDesktopWidgetProvider : AppWidgetProvider() {

    private val ACTION_INCREASE = "ACTION_INCREASE"

    private val ACTION_DECREASE = "ACTION_DECREASE"

    private val requestCode = this.hashCode()

    private var appWidgetManager: AppWidgetManager? = null

    override fun onReceive(context: Context?, intent: Intent?) {
        super.onReceive(context, intent)
        // 接收到广播之后回调此方法。
        context?.also { usableContext ->
            if (!SpUtils.isInit()) {
                SpUtils.init(usableContext)
            }
            var changed = false
            // 实测全局变量在每次接受广播时都会重置。
            // 简单通过SharedPreference从本地存取,确保可以使用最新的数据。
            var currentTotalWaterCount = SpUtils.getInt("currentTotalWaterCount", 0)
            when (intent?.action) {
                ACTION_INCREASE -> {
                    changed = true
                    currentTotalWaterCount++
                }

                ACTION_DECREASE -> {
                    if (currentTotalWaterCount > 0) {
                        changed = true
                        currentTotalWaterCount--
                    }
                }
            }
            if (changed) {
                SpUtils.put("currentTotalWaterCount", currentTotalWaterCount)
                getWidgetManager(usableContext).run {
                    updateWidget(usableContext, this, getAppWidgetIds(ComponentName(usableContext, ExampleDesktopWidgetProvider::class.java)))
                }
            }
        }
    }

    override fun onEnabled(context: Context?) {
        super.onEnabled(context)
        // 首次创建小组件时回调此方法,适合在此执行初始化代码。
    }

    override fun onAppWidgetOptionsChanged(context: Context?, appWidgetManager: AppWidgetManager?, appWidgetId: Int, newOptions: Bundle?) {
        super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
        // 首次添加桌面组件以及调整组件大小时回调,可以根据用户调整的大小来控制显示的内容。
    }

    override fun onUpdate(context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray?) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        // 以AppWidgetProviderInfo中设置的updatePeriodMillis为间隔,周期性的回调此方法。
        // 用户添加小组件时也会回调此方法。
        context?.let { usableContext ->
            appWidgetManager?.let { usableAppWidgetManager ->
                updateWidget(usableContext, usableAppWidgetManager, appWidgetIds)
            }
        }
    }

    override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
        super.onDeleted(context, appWidgetIds)
        // 当小组件被移除时回调此方法。
    }

    override fun onDisabled(context: Context?) {
        super.onDisabled(context)
        // 当小组件的所有实例都被移除时回调此方法,适合执行释放缓存代码。
    }

    private fun getWidgetManager(context: Context?): AppWidgetManager {
        return appWidgetManager ?: AppWidgetManager.getInstance(context).also {
            appWidgetManager = it
        }
    }

    private fun updateWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray?) {
        val currentTotalWaterCount = SpUtils.getInt("currentTotalWaterCount", 0)
        appWidgetManager.updateAppWidget(appWidgetIds, RemoteViews(context.packageName, R.layout.layout_example_desktop_widget).apply {
            setTextViewText(R.id.tv_example_content, "当前饮水量(杯):$currentTotalWaterCount")
            setOnClickPendingIntent(R.id.btn_plus, PendingIntent.getBroadcast(context, requestCode, Intent(context, ExampleDesktopWidgetProvider::class.java).apply { action = ACTION_INCREASE }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
            setOnClickPendingIntent(R.id.btn_reduce, PendingIntent.getBroadcast(context, requestCode, Intent(context, ExampleDesktopWidgetProvider::class.java).apply { action = ACTION_DECREASE }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
        })
    }
}

需要注意的是,类中的全局变量在每次接收到广播时都是初始值,如果需要延续性的使用数据,可以通过持久化数据实现。

效果演示与完整示例代码

最终演示效果如下:

所有演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee

相关推荐
传奇开心果编程1 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓3 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1234 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium
Dovis(誓平步青云)4 小时前
导览音频切换太快,旧讲解不能覆盖新展品
android·前端·javascript·ecmascript·音视频·宠物
五彩小白21 小时前
自回归和上下文
android
事圆则缓1 天前
Android 多渠道打包实战:Product Flavors、签名、AAB 与发包校验
android