01. App Widget开发
Widget 基础
Widget 概述
App Widget 即叫应用微件或者小组件/插件。是可以嵌入其他应用(如主屏幕)并 接收定期更新的微型应用视图。这些视图称为界面中的微件。 用户可以在主屏幕面板上移动小部件,如果支持的话,还可以调整它们的大小以根据自己的喜好定制小部件中的信息量。
- 如需使用 Remote View API 和 XML 布局构建应用 widget,请参阅 创建简单的 widget
- 如需使用 Kotlin 和 Compose 样式 API 构建 widget,请参阅 Jetpack Glance
Widget 开发步骤
编写配置文件
在清单中声明 widget (广播)
- 实现
AppWidgetProvider
,是一个广播,处理 APP widget 对应的回调,与小组件交互,需要在
1
2
3
class AppWidgetLocationProvider : BaseAppWidgetProvider() {
// ...
}
AndroidManifest.xml
定义AppWidgetProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 定义你的 App Widget Provider -->
<receiver
android:name=".appwidget.location.provider.AppWidgetLocationProvider"
android:exported="false">
<intent-filter>
<!--这个必须声明-->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<!--这个和SimpleWidgetProvider中的CLICK_ACTION对应-->
<action android:name="me.hacket.appwidget.CLICK" />
<!-- <action android:name="android.intent.action.CONFIGURATION_CHANGED" />-->
</intent-filter>
<!--name也是不能变的-->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info_location" />
</receiver>
<receiver>
元素需要android:name
属性,该属性指定小部件使用的AppWidgetProvider
(AppWidgetProvider
的父类就是BroadcastReceiver
)。<intent-filter>
中的<action>
元素指定小部件接受ACTION_APPWIDGET_UPDATE
广播。这是必须明确声明的唯一一项广播,用以接收小部件的增删改等信息;不添加该 Action,长按桌面添加小组件不会出现该小组件的。<meta-data>
元素指定小部件的资源,并且需要以下属性:android:name
- 指定元数据名称。必须使用android.appwidget.provider
将数据标识为AppWidgetProviderInfo
描述符。android:resource
- 指定AppWidgetProviderInfo
资源位置。
编写 widget 的配置文件
- 定义
AppWidgetProviderInfo
,定义小组件的元数据 (初始化布局、尺寸等);位于xml/appwidget_info_location.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="me.hacket.weather.common.widget.WeatherWidgetConfigureActivity"
android:initialKeyguardLayout="@layout/weather_widget"
android:initialLayout="@layout/weather_widget"
android:minWidth="170dp"
android:minHeight="90dp"
android:previewImage="@mipmap/weather_widget"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="2"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable" />
- minWidth 和 minHeight :指定小部件默认情况下占用的最小空间。 注意:为使小部件能够在设备间移植,小部件的最小大小不得超过 4 x 4 单元格。
- minResizeWidth 和 minResizeHeight:指定小部件的绝对最小大小。
updatePeriodMillis
:定义小部件框架通过调用onUpdate()
回调方法来从AppWidgetProvider
请求更新的频率应该是多大。- initialLayout: 定义小部件初始外观的布局 XML 文件。
- configure: 定义要在用户添加小部件时启动以便用户配置小部件属性的
Activity
。。 - previewImage: 指定预览来描绘小部件经过配置后是什么样子的,用户在选择小部件时会看到该预览。
- autoAdvanceViewId :指定应由小部件的托管应用自动跳转的小部件子视图的视图 ID。
resizeMode :指定可以按什么规则来调整微件的大小,可选值为 “horizontal vertical”,一般默认设置横竖都可以进行调整。 - minResizeHeight :指定可将微件大小调整到的最小高度。
- minResizeWidth: 指定可将微件大小调整到的最小宽度。
- widgetCategory:声明小部件是否可以显示在主屏幕 (
home_screen
) 或锁定屏幕 (keyguard
) 上。只有低于 5.0 的 Android 版本才支持锁定屏幕微件。对于 Android 5.0 及更高版本,只有home_screen
有效,所以现在将这个值写为home_screen
即可。
编写布局
根布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00000000"
android:theme="@style/Theme.Design.NoActionBar">
<StackView
android:id="@+id/stack_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:loopViews="true" />
</FrameLayout>
子布局
可以看到布局很简单,只放了一个 StackView
,它继承自 AdapterViewAnimator
,同 ListView
和 GridView
一样,StackView
也需要子布局,那就来吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_ll_item">
<ImageView
android:id="@+id/widget_iv_bg"/>
<LinearLayout>
<TextView
android:id="@+id/widget_tv_city" />
<TextView
android:id="@+id/widget_tv_date"/>
<ImageView
android:id="@+id/widget_iv_icon" />
<ImageView
android:id="@+id/widget_iv_small_icon" />
<TextView
android:id="@+id/widget_tv_temp" />
</LinearLayout>
</FrameLayout>
包含集合小部件的清单
由于咱们的布局中有 StackView
,包含集合的小部件除了上面中列出的要求之外,要使包含集合的小部件能够绑定到 RemoteViewsService
,还必须在清单文件中使用 BIND_REMOTEVIEWS
权限来声明该服务。这样可防止其他应用自由访问小部件的数据。
1
2
3
4
<service
android:name=".common.widget.WeatherWidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
包含集合小部件的 AppWidgetProvider 类
与常规小部件一样,AppWidgetProvider
子类中的大部分代码通常都在 onUpdate()
中。在创建包含集合的小部件时,必须调用 setRemoteAdapter()
来设置适配器,这样将告知集合视图要从何处获取其数据。然后,RemoteViewsService
可以返回 RemoteViewsFactory
实现,并且微件可以提供适当的数据。当调用此方法时,必须传递指向 RemoteViewsService
实现的 Intent,以及指定要更新的小部件的小部件 ID,来看看具体实现吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { appWidgetId->
updateAppWidget(context, appWidgetManager, appWidgetId)
val cityInfo = loadTitlePref(context, appWidgetId)
// 设置布局
val views = RemoteViews(context.packageName, R.layout.weather_widget)
val intent = Intent(context, WeatherWidgetService::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
}
views.apply {
// 设置 StackView 适配器
setRemoteAdapter(R.id.stack_view, intent)
setEmptyView(R.id.stack_view, R.id.empty_view)
}
val toastPendingIntent: PendingIntent = Intent(
context,
WeatherWidget::class.java
).run {
action = CLICK_ITEM_ACTION
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
PendingIntent.getBroadcast(
context,
0,
this,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
// 设置点击事件的模版
views.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
RemoteViewsService 实现
创建包含集合的小部件的话必须设置适配器:
1
2
3
4
5
class WeatherWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
return WeatherRemoteViewsFactory(this.applicationContext, intent)
}
}
可以看到 WeatherWidgetService
继承自 RemoteViewsService
,并自己实现了 WeatherRemoteViewsFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class WeatherRemoteViewsFactory(private val context: Context, intent: Intent) :
RemoteViewsService.RemoteViewsFactory, CoroutineScope by MainScope() {
private var cityInfo: CityInfo? = null
init {
intent.getStringExtra(CITY_INFO)?.apply {
cityInfo = Gson().fromJson(this, CityInfo::class.java)
}
}
override fun getViewAt(position: Int): RemoteViews {
if (widgetItems.size != WEEK_COUNT) {
return RemoteViews(context.packageName, R.layout.weather_widget_loading)
}
return RemoteViews(context.packageName, R.layout.widget_item).apply {
val weather = widgetItems[position]
setTextViewText(R.id.widget_tv_temp, "${weather.min}-${weather.max}℃")
setTextViewText(
R.id.widget_tv_city,
"${cityInfo?.city ?: ""} ${cityInfo?.name ?: "北京"}"
)
setImageViewBitmap(
R.id.widget_iv_bg,
fillet(context = context, bitmap = zoomImg(context, weather.icon), roundDp = 10)
)
layoutAdapter(weather.icon)
setTextViewText(R.id.widget_tv_date, weather.time)
setImageViewResource(
R.id.widget_iv_icon,
IconUtils.getWeatherIcon(weather.icon)
)
// 设置点击事件
val fillInIntent = Intent().apply {
putExtra(EXTRA_ITEM, weather.time)
}
setOnClickFillInIntent(R.id.widget_ll_item, fillInIntent)
}
}
override fun getLoadingView(): RemoteViews {
// 加载数据时的布局
return RemoteViews(context.packageName, R.layout.weather_widget_loading)
}
}
设置配置 Activity
配置 Activity
在上面咱们已经说过如何添加到小部件的配置文件中,剩下的就和普通的 Activity
一样了。
由于小部件不支持 Compose
,所以上面咱们都是编写的 Layout
,但是在 Activity
中就可以使用 Compose
了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
@AndroidEntryPoint
class WeatherWidgetConfigureActivity : BaseActivity() {
private val viewModel by viewModels<CityListViewModel>()
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 刷新城市数据
viewModel.refreshCityList()
setContent {
PlayWeatherTheme {
Surface(color = MaterialTheme.colors.background) {
ConfigureWidget(
viewModel,
onCancelListener = {
setResult(RESULT_CANCELED)
finish()
}) { cityInfo ->
onConfirm(cityInfo)
}
}
}
}
}
// ConfigWidget
@OptIn(ExperimentalPagerApi::class)
@Composable
private fun ConfigureWidget(
viewModel: CityListViewModel,
onCancelListener: () -> Unit,
onConfirmListener: (CityInfo) -> Unit
) {
val cityList by viewModel.cityInfoList.observeAsState(arrayListOf())
val buttonHeight = 45.dp
val pagerState = rememberPagerState()
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(80.dp))
Text(
text = "小部件城市选择",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = 26.sp,
color = Color(red = 53, green = 128, blue = 186)
)
Box(modifier = Modifier.weight(1f)) {
HorizontalPager(
state = pagerState,
count = cityList.size,
modifier = Modifier.fillMaxSize()
) { page ->
Card(
shape = RoundedCornerShape(10.dp),
backgroundColor = MaterialTheme.colors.onSecondary,
modifier = Modifier.size(300.dp)
) {
val cityInfo = cityList[page]
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = cityInfo.name, fontSize = 30.sp)
}
}
}
DrawIndicator(pagerState = pagerState)
}
Spacer(modifier = Modifier.height(50.dp))
Divider(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
)
Row {
TextButton(
modifier = Modifier
.weight(1f)
.height(buttonHeight),
onClick = {
onCancelListener()
}
) {
Text(
text = stringResource(id = R.string.city_dialog_cancel),
fontSize = 16.sp,
color = Color(red = 53, green = 128, blue = 186)
)
}
Divider(
modifier = Modifier
.width(1.dp)
.height(buttonHeight)
)
TextButton(
modifier = Modifier
.weight(1f)
.height(buttonHeight),
onClick = {
onConfirmListener(cityList[pagerState.currentPage])
}
) {
Text(
text = stringResource(id = R.string.city_dialog_confirm),
fontSize = 16.sp,
color = Color(red = 53, green = 128, blue = 186)
)
}
}
}
}
Widget components
要实现一个小组件,主要涉及三个部分:
- 视图布局文件:定义小组件的视图
AppWidgetProviderInfo:
定义小组件的元数据,例如小组件的布局、更新频率、尺寸等- AppWidgetProvider:主要是在小组件对应回调触发时,与小组件进行互动
View layout
定义 Widget 的布局 (xml),这个布局需要添加到 RemoteViews
,而 RemoteViews 是不支持 ConstraintLayout 的.
支持:LinearLayout/FrameLayout/RelativeLayout/GridLayout, 以及 TextView/Button 等基础控件 (不支持他们的子类)
下图显示了这些组件如何融入整个应用程序小部件处理流程:
注意:Android Studio 可以自动创建一组
AppWidgetProviderInfo
、AppWidgetProvider
和视图布局文件
。选择 “New”>”Widget”>”App Widget”。
如果您的小部件需要用户配置,请实施 app widget
配置的 Activity。这个 Activity 允许用户修改 app widget
设置,例如时钟小部件的时区。
AppWidgetProviderInfo 对象(xml appwidget-provider
)
描述 App Widget 的元数据 (meta-data),如 布局
、更新频率
和 AppWidgetProvider
类。此对象应在 XML(defined in XML) 中定义并且 要在 AndroidManifest.xml 中声明。
appwidget-provider
属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:targetCellWidth="1"
android:targetCellHeight="1"
android:maxResizeWidth="250dp"
android:maxResizeHeight="120dp"
android:updatePeriodMillis="86400000"
android:description="@string/example_appwidget_description"
android:previewLayout="@layout/example_appwidget_preview"
android:initialLayout="@layout/example_loading_appwidget"
android:configure="com.example.android.ExampleAppWidgetConfigurationActivity"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable|configuration_optional">
</appwidget-provider>
尺寸相关属性
默认主屏幕根据具有定义的高度和宽度的单元格网格在其窗口中定位小部件。大多数主屏幕只允许小部件采用网格单元格整数倍的大小,例如水平两个单元格垂直三个单元格。
小部件尺寸属性允许您指定小部件的默认尺寸,并提供小部件尺寸的下限和上限。在这种情况下,小部件的默认大小是小部件首次添加到主屏幕时所呈现的大小。
属性 | 描述 | 备注 |
---|---|---|
minWidth 和 minHeight | minWidth 和 minHeight 属性指定小部件的默认大小(以 dp 为单位)。如果小部件的最小宽度或高度的值与单元格的尺寸不匹配,则这些值将向上舍入到最接近的单元格大小 | - |
targetCellWidth 和 targetCellHeight | targetCellWidth 和 targetCellHeight 属性指定小部件以网格单元格为单位的默认大小。这些属性在 Android 11 及更低版本中被忽略,如果主屏幕不支持基于网格的布局,则可以忽略这些属性。 | Android 12,API 31 及以上 |
minResizeWidth 和 minResizeHeight | 指定小部件的绝对最小尺寸。这些值指定小部件难以辨认或无法使用的最小尺寸。使用这些属性可以让用户将小部件的大小调整为小于默认小部件大小。如果 minResizeWidth 属性大于 minWidth 或未启用水平调整大小,则该属性将被忽略。参考 resizeMode,同样,如果 minResizeHeight 属性大于 minHeight 或未启用垂直调整大小,则该属性将被忽略。 | - |
maxResizeWidth 和 maxResizeHeight | 指定小部件的建议最大尺寸。如果这些值不是网格单元尺寸的倍数,它们将向上舍入到最接近的单元尺寸。如果 maxResizeWidth 属性小于 minWidth 或未启用水平调整大小,则该属性将被忽略。请参阅 resizeMode 。同样,如果 maxResizeHeight 属性大于 minHeight 或未启用垂直调整大小,则该属性将被忽略。在 Android 12 中引入。 | Android 12,API 31 及以上 |
resizeMode | 指定调整小部件大小的规则。您可以使用此属性使主屏幕小部件在水平、垂直或两个轴上调整大小。用户触摸并按住小部件以显示其调整大小手柄,然后拖动水平或垂直手柄以更改其在布局网格上的大小。 resizeMode 属性的值包括 horizontal 、 vertical 和 none 。要将小部件声明为可水平和垂直调整大小,请使用 horizontal\|vertical 。 | - |
- 建议指定两组属性 -
targetCellWidth
和targetCellHeight
以及minWidth
和minHeight
- 以便您的应用程序可以回退到使用minWidth
和minHeight
如果用户的设备不支持targetCellWidth
和targetCellHeight
。如果支持targetCellWidth
和targetCellHeight
属性优先于minWidth
和minHeight
属性。 - 通用计算方式: (N * 70)-30=宽度;通用计算方式: (N * 70)-30=高度
- 宽度和高度的格数按照 google 标准是这样设置的,但是有很多厂家对 Launcher 重新定义,所以比如你设置的是
5 * 1
,但是某些手机上就会变成4 * 1
。
示例: 网格单元宽 30dp,高 50dp,提供了以下属性规范:
1
2
3
4
5
6
7
8
9
10
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="80dp"
android:minHeight="80dp"
android:targetCellWidth="2"
android:targetCellHeight="2"
android:minResizeWidth="40dp"
android:minResizeHeight="40dp"
android:maxResizeWidth="120dp"
android:maxResizeHeight="120dp"
android:resizeMode="horizontal|vertical" />
- 从 Android 12 开始:
使用 targetCellWidth
和 targetCellHeight
属性作为小部件的默认大小。默认情况下,该小部件的大小为 2x2
。该小部件的大小可以调整为 2x1
或最大 4x3
。
- Android 11 及更低版本:
使用 minWidth
和 minHeight
属性来计算小部件的默认大小。默认宽度 = Math.ceil(80 / 30)
= 3,默认高度 = Math.ceil(80 / 50)
= 2;默认情况下,小部件的大小为 3x2。该小部件的大小可以调整为 2x1 或全屏。
注意:实际的小部件尺寸计算比前面的公式更复杂,因为它还包括小部件边距和网格单元之间的间距。
其他属性
| 属性 | 描述 | 备注 | | :————————————————————— | :——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————– | :———————————————————————————————————————- | | updatePeriodMillis
| 定义小部件框架通过调用 onUpdate()
回调方法从 AppWidgetProvider
请求更新的频率。使用此值并不能保证实际更新完全按时进行,我们建议尽可能少地更新(每小时不超过一次)以节省电池。有关选择适当更新周期的注意事项的完整列表,请参阅更新小部件内容的优化。Optimizations for updating widget content | 1800000,最低时长 30 分钟;(在 1.6 以后的版本中,Google 从省电的角度规定,当 updatePeriodMillis 设置的值小于 30 min 时,就会失效。也就是通过设置这个属性值,最短的更新间隔是 30 min | | initialLayout
| 指向定义小部件初始化布局的布局资源。 | - | | configure
| 定义用户添加小部件时启动的 activity
,让他们配置小部件属性。请参阅允许用户配置小部件 Enable users to configure widgets。从 Android 12 开始,您的应用可以跳过初始配置。有关详细信息,请参阅使用小部件的默认配置。Use the widget’s default configuration | - | | description
| 指定要为您的小部件显示的小部件选择器的描述。在 Android 12 中引入。 | - | | previewLayout
(Android 12) 和 previewImage
(Android 11 及更低版本) | 从 Android 12 开始, previewLayout
属性指定可缩放预览,您可以将其作为设置为小组件默认大小的 XML 布局提供。理想情况下,指定为此属性的布局 XML 与具有实际默认值的实际小部件相同的布局 XML。
在 Android 11 或更低版本中, previewImage
属性指定小部件配置后的预览,用户在选择应用小部件时会看到该预览。如果未提供,用户将看到您的应用程序的启动器图标。此字段对应于 AndroidManifest.xml
文件中 <receiver>
元素中的 android:previewImage
属性。
注意:我们建议同时指定 previewImage
和 previewLayout
属性,以便您的应用在用户设备不支持 previewLayout
。有关更多详细信息,请参阅与可扩展小部件预览的向后兼容性。Backward compatibility with scalable widget previews | - | | autoAdvanceViewId
| 指定由 widget's host
auto-advance 的小部件子视图的视图 ID。 | - | |
widgetCategory | 声明您的小部件是否可以显示在主屏幕(
home_screen )、锁定屏幕(
keyguard )或两者上。对于 Android 5.0 及更高版本,仅
home_screen 有效。 | - | |
widgetFeatures | 声明小部件支持的功能。例如,如果您希望您的小部件在用户添加它时使用其默认配置,请同时指定
configuration_optional 和
reconfigurable 标志。这会绕过用户添加小部件后启动配置的
activity`。用户之后仍然可以重新配置小部件。 | - |
示例
/xml/example_appwidget_info.xml
:
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/widget_content"
android:initialLayout="@layout/widget_content"
android:minHeight="110dp"
android:minWidth="180dp"
android:previewImage="@drawable/example_appwidget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen">
</appwidget-provider>
- initialLayout 则表示 widget 显示的布局
- minWidth/minHeight 表示 Widget 默认情况下,占用的最小空间,会被拉伸
- previewImage 表示预览图片,即用户添加时展示的 (这里是
example_appwidget_preview.png
) - resizeMode 指定可以按什么规则来调整大小 (“
horizontal
“、”vertical
” 和 “none
”) - widgetCategory 是否可以显示在主屏幕 (
home_screen
) 和/或锁定屏幕 (keyguard
)
注:这个 XML 文件将会在 AndroidManifest 声明 AppWidgetProvider 组件时,由 meta-data 引用
1
2
3
4
5
6
7
8
<application>
<receiver android:name=".basic.appwidget.demo1.ExampleAppWidgetProvider" android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/example_appwidget_info" />
</receiver>
</application>
AppWidgetProvider 对象
定义允许您以编程方式与小部件交互的基本方法:定义基于广播事件, 通知到 Widget 的方法。可以在更新、启用、停用和删除 Widget 时收到广播。要在 AndroidManifest. Xml 中声明。 App 可以通过它操作 RemoteViews,例如更新等
实现 AppWidgetProvider 类
AppWidgetProvider 类扩展了 BroadcastReceiver, 作为一个辅助类来处理应用微件广播。仅接收与 Widget 有关的事件广播,例如当更新、删除、启用和停用Widget 时发出的广播。
当发生这些广播事件时,AppWidgetProvider 会接收以下方法调用:
onUpdate()
如果要处理任何用户交互事件,都需要在此回调处理onAppWidgetOptionsChanged()
onDeleted(Context, int[])
onEnabled (Context)
onDisabled (Context)
onReceive(Context, Intent)
onUpdate()
最重要的 AppWidgetProvider
回调是 onUpdate()
,因为它会在每个小部件添加到主机时调用,除非您使用不带 configuration_optional
标志的配置活动。如果您的小部件接受任何用户交互事件,则在此回调中注册事件处理程序。如果您的小部件不创建临时文件或数据库,或执行其他需要清理的工作,则 onUpdate()
可能是您需要定义的唯一回调方法。
总结: 该方法会按着用户设置的更新频率 updatePeriodMillis
定时调用,及在首次创建 AppWidget 时候也会调用,主要在这里更新布局
示例
这里我们设置,点击了 Widget 后,跳转到 ExampleActivity 里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ExampleAppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// Perform this loop procedure for each App Widget that belongs to this provider
appWidgetIds.forEach { appWidgetId ->
// Create an Intent to launch ExampleActivity
val pendingIntent: PendingIntent = Intent(context, ExampleActivity::class.java)
.let { intent ->
PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) // set flags to fix fatal when sdk >=31
}
// Get the layout for the App Widget and attach an on-click listener
// to the button
val views: RemoteViews = RemoteViews(
context.packageName,
R.layout.widget_content
).apply {
setOnClickPendingIntent(R.id.forward_button, pendingIntent)
}
// Tell the AppWidgetManager to perform an update on the current app widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
其中:
appWidgetIds
是一个 ID 数组,对应每一添加到主屏幕的 Widget 实例。(即:可以在主屏幕添加多个 Widget)- 不同的 Widget 实例,
updatePeriodMillis
更新周期表依照第一个 Widget 实例. pendingIntent
用于点击 Widget 上的 Button 时,跳转到 ExampleActivity 注意: 在 SDK>=31 上,需要设置 FLAG_IMMUTABLE. 否则会出现 fatal error: Strongly consider using FLAG_IMMUTABLE…ExampleActivity
是一个简单的 Activity,
onDelete()
每次从 widget host
中删除小部件时都会调用此函数。
onEnabled(Context)
当第一次创建小部件的实例时,将调用此函数。例如,如果用户添加多个小部件的实例,则仅在第一次会被回调。
如果您需要打开一个新数据库或执行另一个只需要对所有小部件实例执行一次的设置,那么这是一个很好的地方。
这个 Widget 在用户长按 widget 准备添加到桌面时,就会回调,不需要已经添加到桌面;如果此时用户 remove 掉,onDeleted 会执行
onDisabled(Context)
当从 widget host
中删除小部件的最后一个实例时,将调用此函数。您可以在此处清理 onEnabled(Context)
中完成的所有工作,例如删除临时数据库。
onAppWidgetOptionsChanged()
当首次放置小部件以及调整小部件大小时会调用此函数。使用此回调可以根据小部件的大小范围显示或隐藏内容。通过调用 getAppWidgetOptions()
获取尺寸范围(从 Android 12 开始,获取小部件实例可以采用的可能尺寸的列表),该方法返回包含以下内容的 Bundle
:
OPTION_APPWIDGET_MIN_WIDTH
包含小部件实例的宽度下限(以dp
为单位)。OPTION_APPWIDGET_MIN_HEIGHT
包含小部件实例的高度下限(以dp
为单位)。OPTION_APPWIDGET_MAX_WIDTH
包含小部件实例的宽度上限(以dp
为单位)。OPTION_APPWIDGET_MAX_HEIGHT
包含小部件实例的高度上限(以dp
为单位)。OPTION_APPWIDGET_SIZES
包含小部件实例可以采用的可能大小 (List<SizeF>
) 的列表(以 dp 为单位)。在 Android 12 中引入。
AppWidgetProvider 生命周期
- 首次添加:
1
2
3
4
AppWidgetLocationProvider onReceive, action:android.appwidget.action.APPWIDGET_Enabled
AppWidgetLocationProvider onEnabled
AppWidgetLocationProvider onReceive, action:android.appwidget.action.APPWIDGET_UPDATE
AppWidgetLocationProvider onUpdate, appWidgetIds=[I@9da6230
- 添加第 2 个:
1
2
AppWidgetLocationProvider onReceive, action:android.appwidget.action.APPWIDGET_UPDATE
AppWidgetLocationProvider onUpdate, appWidgetIds=[I@26ed3a
- 移除第 1 个:
1
2
AppWidgetLocationProvider onReceive, action:android.appwidget.action.APPWIDGET_DELETED
AppWidgetLocationProvider onDeleted, appwidgetIds:[52]
- 移除最后一个:
1
2
3
4
AppWidgetLocationProvider onReceive, action:android.appwidget.action.APPWIDGET_DELETED
AppWidgetLocationProvider onDeleted, appwidgetIds:[53]
AppWidgetLocationProvider onReceive, action:android.appwidget.action.APPWIDGET_DISABLED
AppWidgetLocationProvider onDisabled
- 开机重启:
会执行 onEnable,onUpdate 等方法
是同一个对象吗?
不是
 |
onReceive(Context, Intent) 不是 widget 生命周期方法
每次广播时都会在前面的每个回调方法之前调用此方法。您通常不需要实现此方法,因为默认的 AppWidgetProvider
实现会过滤所有小部件广播并根据需要调用前面的方法。
- onReceive 不存在 widget 生命周期中,它是用来接收广播,通知全局的
- 您必须使用
AndroidManifest
中的<receiver>
元素将AppWidgetProvider
类实现声明为广播接收器
AppWidgetProvider
是一个便利类。如果您想直接接收小部件广播,您可以实现自己的 BroadcastReceiver
或覆盖 onReceive(Context,Intent)
回调。您需要关心的意图如下:
ACTION_APPWIDGET_UPDATE
ACTION_APPWIDGET_DELETED
ACTION_APPWIDGET_ENABLED
ACTION_APPWIDGET_DISABLED
ACTION_APPWIDGET_OPTIONS_CHANGED
作用:
- View 事件可通过广播来实现,在该方法中做更新
Declare a widget in the manifest
在 AndroidManifest.xml
中声明 AppWidgetProvider
类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<receiver
android:name=".basic.appwidget.location.AppWidgetLocationProvider"
android:exported="true">
<intent-filter>
<!--这个必须声明,且是固定值-->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="xxx..."/> <!-- 此处可以添加自己需要的,如点击事件 -->
<!--这个和SimpleWidgetProvider中的CLICK_ACTION对应-->
<action android:name="me.hacket.appwidget.CLICK" />
<action android:name="android.intent.action.CONFIGURATION_CHANGED" />
</intent-filter>
<!-- android:name:也是不能变的-->
<!-- android:resource:添加自己需要的给用户提前预览的自定义小组件布局 -->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info_location" />
</receiver>
需要在清单中声明后,才能在小组件设置里添加。
android:name
属性,该属性指定小部件使用的AppWidgetProvider
。除非有单独的进程需要广播到您的AppWidgetProvider
,否则不得导出该组件,但通常情况并非如此。<intent-filter>
元素必须包含具有android:name
属性的<action>
元素。该属性指定AppWidgetProvider
接受 android.appwidget.action.APPWIDGET_Update 广播。这是您必须显式声明的唯一广播,指定这个才能接收到 widget 更新。<meta-data>
元素指定AppWidgetProviderInfo
资源并需要以下属性:android:name
:指定元数据名称。使用android.appwidget.provider
将数据标识为AppWidgetProviderInfo
描述符。android:resource
:指定AppWidgetProviderInfo
资源位置。即定义 AppWidgetProviderInfo 对象所在的 xml- 这个自定义组件的内容是从 meta-data里的 android.appwidget.provider 读取出来。
- 后续,有小组件的事件,则是通知到它所声明的receiver (这里是 ExampleAppWidgetProvider)
RemoteViews 支持的控件
- [[06. RemoteViews]]
如何更新 Widget?
更新 widget API
更新 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)
刷新一组指定 appwidgetIds 的 widget
updateAppWidget(int appWidgetId, RemoteViews views)
刷新一个指定 appwidgetId 的 widget
update widget type 更新类型
在桌面添加了小组件,但其视图只是我们在 AppWidgetProviderInfo
中 initLayout
指定的布局文件,为了真正显示我们需要的效果,还需更新小组件实际内容。更新小部件有三种方法:完全更新、部分更新以及(如果是集合小部件)数据刷新。每个都有不同的计算成本和后果。
更新 widget 内容的计算成本可能很高。为了节省电池消耗,请优化
update type
、update frequency
和 `timing。
Full update (完全更新)
Full update(完全更新),调用 AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews)
以完全更新 widget。这会将之前提供的 RemoteViews
替换为新的 RemoteViews
。这是计算开销最大的更新。
1
2
3
4
5
6
val appWidgetManager = AppWidgetManager.getInstance(context)
val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also {
setTextViewText(R.id.textview_widget_layout1, "Updated text1")
setTextViewText(R.id.textview_widget_layout2, "Updated text2")
}
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
Partial update (部分更新)
部分更新,调用 AppWidgetManager.partiallyUpdateAppWidget()
以更新 widget 的某些部分。这会将新的 RemoteViews 与之前提供的 RemoteViews 合并。如果 widget 未通过 updateAppWidget(int[], RemoteViews)
接收至少一个完整更新,系统会忽略此方法。
1
2
3
4
5
val appWidgetManager = AppWidgetManager.getInstance(context)
val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also {
setTextViewText(R.id.textview_widget_layout, "Updated text")
}
appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews)
Collection data refresh(集合数据刷新)
集合数据刷新,调用 AppWidgetManager.notifyAppWidgetViewDataChanged
使 widget 中集合视图的数据失效。这会触发 RemoteViewsFactory.onDataSetChanged
。在此期间,旧数据会显示在该微件中。您可以使用此方法安全地同步执行开销大的任务。
1
2
val appWidgetManager = AppWidgetManager.getInstance(context)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview)
您可以从应用程序中的任何位置调用这些方法,只要应用程序与相应的 AppWidgetProvider
类具有相同的 UID。
update frequency (小组件更新频率)
小部件会根据为 updatePeriodMillis
属性提供的值定期更新。小部件可以响应 用户交互
、广播更新
或 两者都更新
。
定期更新
您可以通过在 appwidget-provider
XML 中指定 AppWidgetProviderInfo.updatePeriodMillis
的值来控制定期更新的频率。每次更新都会触发 AppWidgetProvider.onUpdate()
方法,您可以在其中放置代码来更新小部件。
- 如果您的小部件需要异步加载数据或需要10 秒以上的时间来更新,请考虑下一节中描述的广播接收器更新的替代方案,因为 10 秒后,系统会认为
BroadcastReceiver
为non-responsive
。 updatePeriodMillis
不支持小于 30 分钟的值。但是,如果要禁用定期更新,可以指定 0- 中国的魔改的手机
updatePeriodMillis
可能会不生效 - 您可以让用户调整配置中的更新频率。例如,他们可能希望股票行情每 15 分钟更新一次,或者一天只更新四次。在这种情况下,请将
updatePeriodMillis
设置为 0 并使用WorkManager
代替。
响应用户交互进行更新
以下是一些根据用户交互更新小部件的推荐方法:
- 从 App 的 activity :直接调用
AppWidgetManager.updateAppWidget
以响应用户交互,例如用户的tap
。 - 从 remote 交互 (notification 或 app widget) :从远程交互(例如通知或应用程序小部件):构造
PendingIntent
,然后从调用的Activity
、Broadcast
或Service
。您可以选择自己的优先级。例如,如果您为PendingIntent
选择Broadcast
,则可以选择前台广播来赋予BroadcastReceiver
优先级。
响应广播事件进行更新
如果小部件是从 BroadcastReceiver
更新的,包括 AppWidgetProvider
,请注意以下有关小部件更新的持续时间和优先级的注意事项。
通常,系统会让通常在应用程序主线程中运行的广播接收器运行最多 10 秒,然后才会认为它们无响应并触发应用程序无响应 (ANR) 错误。如果更新小部件需要更长的时间,请考虑以下替代方案:
- 使用
WorkManager
安排任务。 - 使用
goAsync
方法给接收者更多时间。这让接收者执行 30 秒。注意:您在此处执行的任何工作都会阻止进一步的广播,直到完成为止,因此可能会减慢后续事件的接收速度。具体参考:ecurity considerations and best practices
更新的优先级: 默认情况下,广播(包括使用 AppWidgetProvider.onUpdate
进行的广播)作为后台进程运行。这意味着系统资源过载可能会导致广播接收器的调用延迟。要确定广播的优先级,请将其设为前台进程。例如,当用户点击小部件的特定部分时,将 Intent.FLAG_RECEIVER_FOREGROUND
标志添加到传递给 PendingIntent.getBroadcast
的 Intent
中。
设置点击事件
由于小组件是一个 RemoteViews
,可以通过 RemoteViews.setOnClickPendingInetnt
来设置点击响应,核心也就是构造一个 PendingIntent
。如果是需要点击跳转,则直接包装一个导航 Intent 即可。
通过广播
如果是需要点击刷新组件数据,则可以考虑构建一个广播 Intent,在接收到广播后进行数据请求与组件更新操作。
示例:
1
2
3
4
5
6
7
8
9
10
11
// 设置 button 事件为启动一个 Activity
Intent intent1 = new Intent("open_widget_activity");
PendingIntent pendingIntent1 = PendingIntent.getActivity(context, 0, intent1, 0);
remoteViews.setOnClickPendingIntent(R.id.button1, pendingIntent1);
// 设置 button 事件为发送一个广播
Intent intent2 = new Intent("send_broadcast");
PendingIntent pendingIntent2 = PendingIntent.getBroadcast(context, 0, intent2, 0);
remoteViews.setOnClickPendingIntent(R.id.button2, pendingIntent2);
// 然后需要处理事件的 `Activity` 或者 `Receiver` 接受对应的 `Intent` 即可。
设置图片
RemoteViews 设置图片相关有以下方法:
根据我们的实际场景,主要是加载网络图片,可以使用 ImageLoader 加载图片,并在加载回调中获取到图片的 Bitmap,然后通过 RemoteViews.setImageViewBitmap
设置即可。
注意: Fresco 下载后的 bitmap 的 recycle 问题
updating widget content 更新
- 主动刷新: 部分业务场景,可能在收到接口请求时,需要同步更新小组件的数据
- 进入后台刷新: 应用进入后台后,用户关注的焦点往往是在桌面,这个时机触发一次数据刷新,来尽可能保证用户看到小组件是最新的数据
- 自动刷新: 周期性同步最新数据,避免小组件显示的是过期数据
主动刷新与进入后台刷新,只需要在不同业务方法或者后台回调中主动触发即可,需要进一步考虑的是自动刷新。默认情况下,小组件的自动刷新是基于 AppWidgetProviderInfo.updatePeriodMillis
指定的更新间隔。但这存在几个问可以要考虑:
updatePeriodMillis
频率限制在 30 分钟以上BroadcastReceiver
的响应时间。一般来说,系统允许通常在应用的主线程中运行的广播接收器最多运行 10 秒,然后才会将其视为 ANR。对于网络请求等场景,通常不会直接在广播接收器主线程执行
总的来说对于以上两点,官方的推荐方案还是通过 WorkManager 来安排任务,并将 widget 的 updatePeriodMillis
设置为 0。
WorkManager 支持的最小间隔是 15 分钟。但 WorkManager 还是存在一些国内机型支持不完善、受系统电池策略等问题。可以考虑 updatePeriodMillis
不设置为 0,同时利用 updatePeriodMillis和WorkManager
定时任务的唤醒?
Stateful Widget
Android 12 使用以下现有组件添加了对有状态行为的支持:
小部件仍然是无状态的。您的应用程序必须存储状态并注册状态更改事件。
 |
注意:始终使用
RemoteViews.setCompoundButtonChecked
显式设置当前选中状态,否则在拖动小部件或调整大小时可能会遇到意外结果。
以下代码示例展示了如何实现这些组件。
1
2
3
4
5
6
7
8
9
10
11
12
// Check the view.
remoteView.setCompoundButtonChecked(R.id.my_checkbox, true)
// Check a radio group.
remoteView.setRadioGroupChecked(R.id.my_radio_group, R.id.radio_button_2)
// Listen for check changes. The intent has an extra with the key
// EXTRA_CHECKED that specifies the current checked state of the view.
remoteView.setOnCheckedChangeResponse(
R.id.my_checkbox,
RemoteViews.RemoteResponse.fromPendingIntent(onCheckedChangePendingIntent)
)
如何添加 Widget?
桌面添加小组件
以三星 S23 为例,桌面小组件添加入口包括两种:
- 长按桌面,弹出小组件列表,选择添加对应应用的小组件
- 长按应用启动图标,点击小组件图标,弹出该应用的小组件列表(有的手机不支持,低版本也可能不支持)
华为的手机是在桌面双指往内捏合添加小组件
应用内请求添加 Widget (Android8.0 及以上)
在 Android 8.0(API26)及更高版本中,你可以通过代码来添加应用小部件(App Widget)。这通常涉及请求用户的权限,并调用适当的系统 API。以下是如何实现这一功能的简要步骤和示例代码。
主要步骤
- 请求用户权限以添加小部件:通过
AppWidgetManager.ACTION_APPWIDGET_BIND
意图请求用户权限。 - 判断是否支持添加小组件
- 选择小部件提供者(可选):如果你不确定要添加的小部件类型,可以创建一个小部件选择器。
- 处理用户的权限响应:在响应中处理用户的选择,并添加小部件到主屏幕。
- 在布局 XML 中定义小部件的结构(如果已有小部件定义可以跳过)。
代码:
- 添加必要的权限到
AndroidManifest.xml
1
<uses-permission android:name="android.permission.BIND_APPWIDGET" />
- 定义小部件布局(
res/layout/appwidget_layout.xml
) - 定义小部件配置文件(
res/xml/appwidget_info.xml
) - 创建
AppWidgetProvider
子类 - 在
AndroidManifest.xml
中声明小部件 - 请求用户权限并添加小部件(可能还需要加上 Android 8.0 判断代码,Android 8.0 以下不处理?)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private val REQUEST_BIND_WIDGET = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 按钮点击事件,启动添加小部件的流程
addButton.setOnClickListener {
addAppWidget()
}
}
private fun addAppWidget() {
val appWidgetManager = AppWidgetManager.getInstance(this)
val myProvider = ComponentName(this, MyAppWidgetProvider::class.java)
// 查询指定的 App Widget 提供者是否已经绑定
if (appWidgetManager.isRequestPinAppWidgetSupported) {
// 请求用户添加 app widget
val pinnedWidgetCallbackIntent = Intent(this, MainActivity::class.java).apply {
action = "com.example.APPWIDGET_PINNED_CALLBACK"
}
// 第三个参数一个PendingIntent,在小组件添加成功后触发,可以根据需要做一些添加成功监听
val successCallback = PendingIntent.getActivity(this, 0, pinnedWidgetCallbackIntent, 0)
// 系统会显示一个对话框请求用户确认添加小部件。
appWidgetManager.requestPinAppWidget(myProvider, null, successCallback)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_BIND_WIDGET && resultCode == Activity.RESULT_OK) {
// 用户已允许绑定小部件
data?.let {
val appWidgetId = it.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (appWidgetId != -1) {
val appWidgetManager = AppWidgetManager.getInstance(this)
val views = RemoteViews(packageName, R.layout.appwidget_layout)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
}
}
Android 不支持静默安装小组件,即使是在应用内添加小组件,也只能弹出添加弹窗,需要由用户确认后才能完成添加
APP Widget ListView
App widget 如何禁用?
- app widget 就是一个广播,可以先 enable 设置为 false,
- 通过 API
packageManager.setComponentEnabledSetting
来做动态开启
1
2
3
4
5
6
7
8
9
10
11
12
13
private fun setReceiverEnabled(context: Context, enabled: Boolean) {
val componentName = ComponentName(context, AppWidgetSearchToolProvider::class.java)
val newState = if (enabled) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
context.packageManager.setComponentEnabledSetting(
componentName,
newState,
PackageManager.DONT_KILL_APP
)
}
从 WorkManager 中源码看到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* Helper class for common {@link PackageManager} functions
*/
public class PackageManagerHelper {
private static final String TAG = Logger.tagWithPrefix("PackageManagerHelper");
private PackageManagerHelper() {
}
/**
* Uses {@link PackageManager} to enable/disable a manifest-defined component
*
* @param context {@link Context}
* @param klazz The class of component
* @param enabled {@code true} if component should be enabled
*/
public static void setComponentEnabled(
@NonNull Context context,
@NonNull Class<?> klazz,
boolean enabled) {
try {
PackageManager packageManager = context.getPackageManager();
ComponentName componentName = new ComponentName(context, klazz.getName());
packageManager.setComponentEnabledSetting(componentName,
enabled
? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
Logger.get().debug(TAG,
String.format("%s %s", klazz.getName(), (enabled ? "enabled" : "disabled")));
} catch (Exception exception) {
Logger.get().debug(TAG, String.format("%s could not be %s", klazz.getName(),
(enabled ? "enabled" : "disabled")), exception);
}
}
/**
* Convenience method for {@link #isComponentExplicitlyEnabled(Context, String)}
*/
public static boolean isComponentExplicitlyEnabled(Context context, Class<?> klazz) {
return isComponentExplicitlyEnabled(context, klazz.getName());
}
/**
* Checks if a manifest-defined component is explicitly enabled
*
* @param context {@link Context}
* @param className {@link Class#getName()} name of component
* @return {@code true} if component is explicitly enabled
*/
public static boolean isComponentExplicitlyEnabled(Context context, String className) {
PackageManager packageManager = context.getPackageManager();
ComponentName componentName = new ComponentName(context, className);
int state = packageManager.getComponentEnabledSetting(componentName);
return state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
}
}
Widget 进程选择?
app widget 拉起主进程
- 长按桌面图标时,无论是否添加小组件,都可能会拉起主进程(主要用三星测试)
- Android 小组件运行的进程是宿主桌面程序所在进程,我们这里讨论的指定进程仅仅是收到小组件的更新、添加等广播的进程
- 之前调查的应用来看各自情况都有:今日头条、tiktok 全部在主进程,淘宝、美团等比较多使用单独进程,lazada 两个在单独进程一个在主进程
- 添加小组件以及通过 WorkManager 自动更新时都会收到广播,拉起主进程
Widget limitations
Gestures
Widgets 位于主屏幕上,因此它们必须与主屏幕上建立的 navigation 共存。与全屏应用程序相比,这限制了 widget 中可用的手势支持。
虽然应用程序可能允许用户在屏幕之间水平导航,但为了在主屏幕之间导航,已经在主屏幕上采取了该手势。
小部件唯一可用的手势是touch and vertical swipe。不支持横向手势
Elements
鉴于可用于小部件的手势的限制,某些依赖于受限手势的 UI 构建块不可用于小部件。有关支持的构建块的完整列表以及有关布局限制的更多信息,请参阅 Create the widget layout and Provide flexible widget layouts。
App Widget 通用开发总结
- Widget 数据刷新 WorkManager,配合 CoroutineWorker 可使用协程;不用
updatePeriodMillis
,这个值有的中国产的手机不会更新 - 在
onEnable()
中开启 Worker 任务,onDisable()
中 cancel 掉任务 - 注意 widget 在不同手机上的适配问题,官方推荐用多套布局;也可采用一套布局 + sw 方案,这种方案要注意处理横屏布局,需要单独弄套横屏布局来适配
- Click 可通过广播,可共用一个 action,通过 extra 来区分不同的点击 view 的 id 和数据;这个 action 需要在 Manifest 中声明
- Widget 中有文本和图片,可以先更新文本,后面局部更新图片
- Widget 进行 full update 时要注意 RemoteViews 的覆盖问题,导致 click 事件失效
- AppWidgetProvider 的
onUpdate()
方法中进行 Widget UI 的更新 - 手机重启会,会走
onEnable
和onUpdate
,可在onUpdate
加载本地数据来更新 Widget UI
厂商适配
- [[AppWidget代码申请添加小部件,展示添加弹窗适配]]