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中添加。