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

相关推荐
2501_937145411 天前
IPTV电视源码系统2026优化版:技术升级,全场景流畅适配
android·电视盒子·源代码管理
Ehtan_Zheng1 天前
让你的代码更整洁:10 个必知的 Kotlin 扩展函数
android
城东米粉儿1 天前
Android VSync 笔记
android
城东米粉儿1 天前
Android SurfaceFlinger 笔记
android
似霰1 天前
Android 日志系统5——logd 写日志过程分析二
android·log
hewence11 天前
Kotlin CoroutineContext 详解
android·开发语言·kotlin
Albert Edison1 天前
【Python】文件
android·服务器·python
大模型玩家七七1 天前
效果评估:如何判断一个祝福 AI 是否“走心”
android·java·开发语言·网络·人工智能·batch
Aurora4191 天前
Android事件分发逻辑--针对事件分发相关函数的讲解
android