你可能用过一些 Android APP
的小组件,比如:
- 支付宝的小组件:之前疫情期间添加了对应小组件卡片在桌面,可点击小卡片上的查看健康码的按钮,可一键打开健康码。
- 音乐类
APP
的小组件:添加对应对应小组件后, 可在 APP 的主屏幕中轻松看到当前播放歌曲的相关信息:歌曲封面、歌曲名、歌手名称、所属专辑名称等。 - 时钟类
APP
的小组件:可添加各种样式的时钟小组件在屏幕,装饰你的主屏幕,在你喜欢的小组件上来查看时间。 - 天气类
APP
的小组件:可在主屏幕直接看到天气相关信息,不用再打开天气的APP
。
如果你所开发的项目的产品经理并没有相关开发需求,可能很多 Android
开发并没有接触过相关桌面主屏幕的相关小组件开发,这篇文章主要介绍下相关开发的知识点。
AppWidgetProvider:
帮助实现
AppWidget
提供程序的便利类。 你可以用AppWidgetProvider
做的事情,你也可以用一个普通的BroadcastReceiver
做。AppWidgetProvider
只是从onReceive(Context,Intent)
中接收到的Intent
中解析出相关字段,并使用接收到的extra
调用hook
方法。扩展此类并覆盖
onUpdate
、onDeleted
、onEnabled
或onDisabled
方法中的一个或多个以实现您自己的AppWidget
功能。
java
public class WidgetDemoProvider extends AppWidgetProvider {
/**
* 当 Widget 第一次被添加时调用,例如用户添加了两个你的 Widget,那么只有在添加第一个 Widget 时该
* 方法会被调用。所以该方法比较适合执行你所有 Widgets 只需进行一次的操作。对用广播的 Action 为
* ACTION_APPWIDGET_ENABLE。
*/
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
}
/**
* 与 onEnabled 恰好相反,当你的最后一个 Widget 被删除时调用该方法,所以这里用来清理之前在 onEnabled() 中进行的操作。
* 当最后一个该类型的小部件从桌面移除时调用,对应的广播的 Action 为 ACTION_APPWIDGET_DISABLED。
*
*/
public void onDisabled(Context context) {
super.onDisabled(context);
}
/**
* 当 Widget 第一次被添加或者大小发生变化时调用该方法,可以在此控制 Widget 元素的显示和隐藏。
* 当小部件布局发生更改的时候调用。对应广播的 Action 为 ACTION_APPWIDGET_OPTIONS_CHANGED。
*
* @param appWidgetManager 您可以调用 AppWidgetManager.updateAppWidget 的 AppWidgetManager 对象。
* @param appWidgetId 大小改变的widget的appWidgetId。
* @param newOptions
*/
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
}
/**
* 当小部件从备份中还原,或者恢复设置的时候,会调用,实际用的比较少。对应广播的 Action 为 ACTION_APPWIDGET_RESTORED。
* @param oldWidgetIds
* @param newWidgetIds
*/
@Override
public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
super.onRestored(context, oldWidgetIds, newWidgetIds);
}
/**
* AppWidget 更新事件
*
* 小部件被添加时或者每次小部件更新时都会调用一次该方法,配置文件中配置小部件的更新周期 updatePeriodMillis,每次更新都会调用。对应广播 Action 为:ACTION_APPWIDGET_UPDATE 和 ACTION_APPWIDGET_RESTORED
*
* @param appWidgetManager 更新 AppWidget 状态; 获取有关已安装 AppWidget 提供程序和其他 AppWidget 相关状态的信息。
* @param appWidgetIds 需要更新的 appWidgetIds。 请注意,这可能是此提供程序的所有 AppWidget 实例,也可能只是其中的一个子集。
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
}
/**
* 当 Widget 被删除时调用该方法。
*
* 每删除一个小部件就调用一次。对应的广播的 Action 为: ACTION_APPWIDGET_DELETED 。
*
* @param appWidgetIds 已从其组件集群中删除的 appWidgetIds。
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
}
/**
* 接收广播的回调函数
* onReceive() 中处理的是 Widget 相关的广播事件,然后分发到各个回调函数中onUpdate(), onDeleted(), onEnabled(), onDisabled, onAppWidgetOptionsChanged()。
*
* @param context
* @param intent
*/
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.onEnabled(context);
}
else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
}
...省略
super.onReceive(context, intent);
}
}
根据自己的业务,写好 WidgetDemoProvider
的自定义代码后,需要在 AndroidManifest.xml
里注册一下:
xml
<receiver android:name=".widget.WidgetDemoProvide"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="xxx..."/> <!-- 此处可以添加自己需要的 -->
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/appwidget_demo" /> <!-- 此处可以添加自己需要的给用户提前预览的自定义小组件布局 -->
</receiver>
<receiver>
元素需要android:name
属性,该属性指定小部件使用的AppWidgetProvider
(AppWidgetProvider
的父类就是BroadcastReceiver
)。
<intent-filter>
中的<action>
元素指定小部件接受ACTION_APPWIDGET_UPDATE
广播。这是必须明确声明的唯一一项广播,用以接收小部件的增删改等信息。
<meta-data>
元素指定小部件的资源,并且需要以下属性:
android:name
- 指定元数据名称。必须使用android.appwidget.provider
将数据标识为AppWidgetProviderInfo
描述符。
android:resource
- 指定AppWidgetProviderInfo
资源位置。
appwidget_demo.xml
的示例代码:
xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="180dp"
android:minHeight="180dp"
android:updatePeriodMillis="1800000"
android:previewImage="@mipmap/app_widget_preview_3x3"
android:initialLayout="@layout/app_widget_preview_layout_3_3"
android:resizeMode="horizontal|vertical">
</appwidget-provider>
minWidth
和minHeight
:指定小部件默认情况下占用的最小空间。注意:为使小部件能够在设备间移植,小部件的最小大小不得超过 4 x 4 单元格。
minResizeWidth
和minResizeHeight
:指定小部件的绝对最小大小。
updatePeriodMillis
:定义小部件框架通过调用 onUpdate() 回调方法来从 AppWidgetProvider 请求更新的频率应该是多大。
initialLayout
:指向用于定义小部件布局的布局资源。
configure
:定义要在用户添加小部件时启动以便用户配置小部件属性的 Activity。
previewImage
:指定预览来描绘小部件经过配置后是什么样子的,用户在选择小部件时会看到该预览。
autoAdvanceViewId
:指定应由小部件的托管应用自动跳转的小部件子视图的视图 ID。
resizeMode
:指定可以按什么规则来调整微件的大小,可选值为"horizontal|vertical",一般默认设置横竖都可以进行调整。
minResizeHeight
:指定可将微件大小调整到的最小高度。
minResizeWidth
:指定可将微件大小调整到的最小宽度。
widgetCategory
:声明小部件是否可以显示在主屏幕 (home_screen
) 或锁定屏幕 (keyguard
) 上。只有低于 5.0 的 Android 版本才支持锁定屏幕微件。对于 Android 5.0 及更高版本,只有home_screen
有效,所以现在将这个值写为home_screen
即可。
如果在自己的相关业务代码里,比如 activity
里如何触发 WidgetDemoProvider
相关数据以及页面更新。可以看以下示例代码:
java
private void sendNotify(){
try {
Class javaClass = Class.forName("xxx.WidgetDemoProvider");
final Intent intent = new Intent(this, javaClass);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
int[] ids = AppWidgetManager.getInstance(GlobeContext.context).getAppWidgetIds(new ComponentName(GlobeContext.context, javaClass));
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
sendBroadcast(intent);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
一些开发注意事项:
- 小组件的宽高是可以支持用户自行调整的,只需简单的设置最低宽高,但是可调整的最小粒度是根据手机的
icon
为标准。小组件数量无限制,用户对小组件的大小和具体功能喜好都不太一样,所以解决方案就简单粗暴一点,你能想到的适配尺寸,每种尺寸搞一个,用户自己选择合适的尺寸就好。大、中、小、大中、中小、微小、超大等尺寸,可以全部做一遍。
ini
android:resizeMode="horizontal|vertical"
widget 可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以竖直拉伸
- 更新时间分为主动更新和定时更新。
主动更新 :即在APP
中可以动态更新这个桌面小组件,这种情况更新没有时间限制。
定时更新 :小组件需要展示的数据可能已经发生了变化,但是APP
已经被系统杀死了,无法主动更新数据,就会导致小组件展示的数据可能是已过期的或者是旧的,这时候就可以用到小组件的定时更新功能,但是这个定时更新有一个限制,基于省电逻辑,最快的更新周期为 30 分钟。(如果是在onUpdate
方法中写个定时器定时更新,这样是不行的,会被系统给杀死,杀死之后小组件不会消失,而是一直显示最后一次更新时候的状态,直到下一次更新数据,类似于电子水墨屏的逻辑。) - 一般点击整个小组件,我们直接调起
APP
。点击跳转页面需要用到PendingIntent
,这玩意的Flag
有很多种模式,具体可以查看文章底部的参考文档,坑就坑在这个Flag
,31 之后的系统有改动,会报错,所以 31 的系统需要用PendingIntent.FLAG_IMMUTABLE
,具体看代码。
java
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.onEnabled(context);
}
else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
}
if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
Uri data = intent.getData();
int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
switch (buttonId) {
case R.id.widget_layout:
Intent intent = new Intent(context, RemotePlayerActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(goIntent);
RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.app_widget_layout);
//将按钮与点击事件绑定
remoteView.setOnClickPendingIntent(R.id.widget_layout,getPendingIntent(context, R.id.widget_layout));
break;
}
}
super.onReceive(context, intent);
}
java
private PendingIntent getPendingIntent(Context context, int buttonId) {
Intent intent = new Intent();
intent.setClass(context, WidgetDemoProvider.class);
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
intent.setData(Uri.parse("harvic:" + buttonId));
PendingIntent pendingIntent;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
} else {
pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
}
return pendingIntent;
}
- 通过
PendingIntent
就可以直接调起APP
的相关页面,不过这里也有坑,假设你APP
的启动页面是MainActivity
页面,点击小组件你就让它跳转到MainActivity
页面走正常的APP
启动流程,就等同于是点击小组件就能启动APP
,哪怕APP
被杀死了。坑就坑在于,通过这种方式打开的APP
,它不会走Application
类,也就是你如果是在Application
中初始化了某些东西,但是APP
已经被系统杀死了,这时候你再点击小组件启动APP
,这时就会发现好多组件用不了,因为没初始化。
所以相对省事的做法就是把Application
的所有需要初始化的东西都放MainActivity
里面初始化了,目前为了工信部隐私相关合规,应该很多APP
的初始化代码应该已经从Application
放到用户点同意隐私协议弹框后再去初始化了。
RemoteViews
,从字面意思理解为它是一个远程视图。是一种远程的View
,它在其它进程中显示,却可以在另一个进程中更新。RemoteViews
在Android
中的使用场景主要有:自定义通知栏和桌面小部件。
RemoteViewsService
,是管理RemoteViews
的服务。一般,当AppWidget
中包含GridView
、ListView
、StackView
等集合视图时,才需要使用RemoteViewsService
来进行更新、管理。RemoteViewsService
更新集合视图的一般步骤是:通过setRemoteAdapter()
方法来设置RemoteViews
对应RemoteViewsService
。之后在
RemoteViewsService
中,实现RemoteViewsFactory
接口。然后,在RemoteViewsFactory
接口中对集合视图的各个子项进行设置,例如ListView
中的每一Item
。
RemoteViewsFactory
通过RemoteViewsService
中的介绍,我们知道RemoteViewsService
是通过
RemoteViewsFactory
来具体管理layout
中集合视图的,RemoteViewsFactory
是RemoteViewsService
中的一个内部接口。RemoteViewsFactory
提供了一系列的方法管理集合视图中的每一项。例如:
public RemoteViews getViewAt(int position):
通过getViewAt()来获取"集合视图"中的第position项的视图,视图是以RemoteViews的对象返回的。
public int getCount() :
通过getCount()来获取"集合视图"中所有子项的总数。
- **用户可重新设置原有 widget。**在
Android 12
之前,重新设置widget
意味着用户必须删除现有widget
,然后使用新配置重新添加。Android 12
在多个方面改进了widget
的配置方式,从而帮助用户采用更简单的方式对widget
进行个性化配置。可重组的widget
允许用户对widget
进行自定义设置。在Android 12
中,用户将无需通过删除和重新添加widget
来调整这些原有设定。
要使用这一功能,您需在appwidget-provider
中把widgetFeatures
属性设置为reconfigurable
。
当用户配置该widget
时,新的配置会被记录在ListWidgetConfigureActivity
中。
如果您的widget
依赖默认设置,在Android 12
中您可跳过初始化操作,通过默认配置来设置widget
。
xml
<appwidget-provider
android:configure="com.example.android.appwidget.ListWidgetConfigureActivity"
android:widgetFeatures="reconfigurable|configuration_optional"
... />
- Android 12 中 Widget 的尺寸限制改进 。除了现有的
minWidth
、minHeigh
、minResizeWidth
以及minResizeHeight
以外,Android 12
还添加了新的appwidget-provider
属性。您可以使用新的maxResizeWidth
和maxResizeHeight
属性,来定义用户所能够调整的widget
尺寸的最大高度和宽度。新的targetCellWidth
和targetCellHeight
属性能够定义设备主屏幕上的widget
默认尺寸。如果之前有targetCellWidth
和targetCellHeight
属性的话,小部件也不至于像现在这么乱而导致用户不想使用。
xml
<appwidget-provider
android:maxResizeWidth="240dp"
android:maxResizeHeight="180dp"
android:minWidth="180dp"
android:minHeight="110dp"
android:minResizeWidth="180dp"
android:minResizeHeight="110dp"
android:targetCellWidth="3"
android:targetCellHeight="2"
... />
<!-- maxResizeWidth:定义用户所能够调整的小部件尺寸的最大宽度
maxResizeHeight:定义用户所能够调整的小部件尺寸的最大高度
targetCellWidth:定义设备主屏幕上的小部件默认宽度所占格数(即使不同型号的手机中也会占定义好的格数,但手机系统版本必须在 Android 12 及以上)
targetCellHeight:定义设备主屏幕上的小部件默认高度所占格数 -->
-
新的小部件控件。
Android 12
使用以下现有控件新增了对有状态行为的支持:CheckBox
、Switch
、
RadioButton
,上面这几个控件大家应该非常熟悉了,但在Android 12
之前在小部件中想要使用的话也是不可能的。 -
Android 12
以上可以通过system_app_widget_background_radius
和system_app_widget_inner_radius
系统参数来设置微件圆角的半径。
👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀