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, verticalnone
android:updatePeriodMillis 小组件更新时间频率,官方建议尽量降低更新频率,最好每小时不超过一次。
android:widgetCategory 设置小组件可以添加的地方,可以设置home_screensearchboxkeyguard。但是从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

相关推荐
拭心10 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王12 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡12 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道13 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库14 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道14 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe14 小时前
Android Hook - 动态加载so库
android
居居飒15 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He18 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗18 小时前
Android笔试面试题AI答之Android基础(1)
android