一.简单上手
1. 配置并显示widget
1.1 继承AppWidgetProvider
自定义MyWidgetProvider继承AppWidgetProvider,重写相关方法。
class MyWidgetProvider : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
//当更新widget的时候会触发,添加的时候也会触发
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
//删除widget的时候会触发
}
override fun onDisabled(context: Context?) {
super.onDisabled(context)
//最后一个widget被删除的时候触发
}
override fun onEnabled(context: Context?) {
super.onEnabled(context)
//第一个widget被添加的时候触发
}
override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
//当widget被第一次添加或者widget大小改变的时候触发
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
//处理方式和普通广播一样
}
}
AppWidgetProvider实质上就是一个广播,其中处理了相关的action并给出了回调方法。
1.2 配置appwidget-provider
找到res目录下的xml目录,若没有xml目录就新建一个,然后新建一个文件widget_info.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/widget_layout"
android:initialLayout="@layout/widget_layout"
android:minWidth="@dimen/dp_320"
android:minHeight="@dimen/dp_110"
android:previewImage="@mipmap/img_widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="5"
android:widgetCategory="home_screen">
</appwidget-provider>
这里介绍下常用属性
-
android:initialLayout
添加到桌面的widget布局 -
android:initialKeyguardLayout
添加到锁屏页面的widget布局 -
android:minWidth
最小宽度,通用计算方式: (N * 70)-30=宽度 -
android:minHeight
最小高度,通用计算方式: (N * 70)-30=高度,宽度和高度的格数按照google标准是这样设置的,但是有很多厂家对Launcher重新定义,所以比如你设置的是5 * 1,但是某些手机上就会变成4 * 1。 -
android:previewImage
预览图 -
android:resizeMode
允许横向纵向拉伸 -
android:updatePeriodMillis
刷新间隔,最小刷新间隔是半小时,设置小于半小时也会按半小时算,且这里还有一点要注意,并不是每过半小时就一定会准时刷新,受设备影响这个时间可能略有提前或延迟。还有当手机息屏后可能会进入休眠状态,在休眠状态时不会自动更新,当设备解锁从休眠状态恢复时会立即刷新widget。
1.3 配置AndroidManifest.xml
<receiver
android:name=".MyWidgetProvider"
android:label="@string/app_widget_string">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
在AndroidManifest.xml中需要配置一个广播接受者,其中固定的两个配置参数
-
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
指定这个才能接收到widget更新。 -
android:name="android.appwidget.provider"
告诉系统这个广播接受者是一个widget。
还可以在intent-filter里面配置自定义的action,用法就和普通广播一样。
现在已经可以添加widget显示啦,显示的内容为widget_layout.xml里的布局,没错就是这么简单。
2. 更新widget
上面已经显示了widget,接下来就要给widget更新UI。
更新widget的UI是通过AppWidgetManager的updateAppWidget
方法实例来更新的,我们可以通过AppWidgetManager.getInstance(context)
来获取实例。updateAppWidget有三个重载方法。
-
updateAppWidget(ComponentName provider, RemoteViews views)
指定要刷新widget的ComponentName和RemoteViews,通过AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteView)来刷新。举个例子,我在桌面第一页和第三页都添加了同一个widget,现在若点击其中一个的刷新按钮两个widget要同时都更新界面,这时就可以用这个方法。这个方法也是最常用来更新widget的方式,可以刷新添加到桌面的所有widget。一般来说,更新widget并不要求在AppWidgetProvider中进行,因为AppWidgetProvider本质上就是一个广播,只要通过指定remoteView和ComponentName,可在任何包含上下文的环境下更新widget。 -
updateAppWidget(int[] appWidgetIds, RemoteViews views)
刷新部分指定的widget -
updateAppWidget(int appWidgetId, RemoteViews views)
刷新一个指定的widget
class MyWidgetProvider : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
for (appWidgetId in appWidgetIds) {
appWidgetManager.updateAppWidget(appWidgetId, remoteView)
}
//uploadWidget(context)
}
private fun uploadWidget(context: Context) {
val remoteView = RemoteViews(context.packageName, R.layout.widget_layout)
val componentName = ComponentName(context, javaClass)
AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteView)
}
}
上面代码两种方式都能刷新全部widget
3. widget的点击
package com.example.kotlintest.widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import com.example.kotlintest.R
class MyWidgetProvider : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
val intent = Intent(REFRESH_CLICK).apply {
component = ComponentName(context, MyWidgetProvider::class.java)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
R.id.tv_refresh,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
val remoteView = RemoteViews(context.packageName, R.layout.widget_layout)
remoteView.setOnClickPendingIntent(R.id.tv_refresh, pendingIntent)
uploadWidget(context,remoteView)
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
when (intent.action) {
REFRESH_CLICK -> {
//点击事件
}
}
}
private fun uploadWidget(context: Context,remoteView: RemoteViews) {
val componentName = ComponentName(context, javaClass)
AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteView)
}
companion object {
const val REFRESH_CLICK = "com.example.kotlintest.action.CLICK_REFRESH"
}
}
二. 开发widget中需要注意处理的点
1. 初始化问题
当widget刷新时,如果应用没有处于开启状态下,这时会创建APP进程并初始化Application,之后回调widget的onUpdate方法。然而这里会有一个问题,由于部分app为了性能优化,将部分初始化操作移动到了引导页或Main页面里了,这样当widget想使用某些功能时,由于只创建了Application,在引导页或main页面里进行初始化的那部分功能没有进行初始化,便会抛出各种异常。所以这里开发的时候需要重点检查一遍。
2. UI设置
- 当添加widget出现小组件添加错误、显示失败等,优先检查xml布局是否正确,尤其是不能包含自定义View等。
- 通过RemoteViews更新widget,可能每次更新都创建了一个RemoteViews对象,但是RemoteViews只是一个action集合,只代表你对systemServer端widget的操作,一旦通过RemoteViews更新过widget,有些步骤就可以不用重复设置(列如点击事件)
- widget不支持动画,如果一定要实现动画,可以开子线程循环刷新bitmap。
3. 网络请求
尽量不要直接在AppWidgetProvider中进行网络请求,和耗时操作。
- 在AppWidgetProvider中进行网络请求,当未开启APP情况下,会请求失败抛出SocketTimeoutException异常。这一点很重要,很多系统都会限制在后台程序里静态广播的网络请求。如果有需要,请开启Service,在Service中进行网络请求。
- 由于AppWidgetProvider优先级很低,代表当前进程容易被系统回收,所以尽量不要再AppWidgetProvider中进行耗时操作,否则可能会出现AppWidgetProvider中的任务未执行完进程就已经被系统回收。建议耗时操作开启Service执行。
4. 定时任务
很大一部分app都有定时刷新widget的需求,而系统的刷新间隔要求大于等于30分钟,这显然是满足不了需求。这里有两种方案。
- 单独进程的前台service
- 通过JobScheduler
如果对实时性要求不是太高,可以考虑使用JobScheduler
5. 关于Service通知问题
我们知道在Android8.0后开启Service需要指定为前台通知,这样就会有一个通知栏效果。如果在widget中想开启Service进行网络请求,而又不想出通知,可以使用bindService方式。
bindService是Context的方法,网上大部分文章都拿Activity做例子,导致很多人不知道bindService其实在Application等Context的子类中都能使用。