概要
在安卓开发中,提供了Widget小部件,这种小部件可用于嵌入主屏幕或其它程序当中作为一个小型应用视图,提供实时信息以及快捷操作。在 Android 15 之前,提供微件选择器预览的唯一方法是指定静态图片或布局资源,这样导致用户在小部件选择器中的预览和放在主屏幕上的完全不同。Android 15 添加了对生成的预览的支持。这意味着,应用微件提供程序可以生成 RemoteViews 以用作选择器预览,而不是静态资源。本文章将从基础介绍 Android 小部件开始,逐步带领大家了解 Android 15 中的 Generated Previews API,以及如何通过代码实现这一功能。
一、什么是 Android 小部件?
1. 定义
Android 小部件是一种可以嵌入主屏幕或其他应用中的小型应用视图,提供实时更新的信息或快捷操作。
2.组成
AppWidgetProvider:用于处理小部件的生命周期事件(如更新、删除)。
RemoteViews:定义小部件的 UI 和交互。
AppWidgetProviderInfo:描述小部件的元数据。
3.Android Widget运行模式
系统会读取AndroidManifest中注册的AppWidgetProvider和定义元数据的AppWidgetProviderInfo,AppWidgetProviderInfo会存储widget对应的一些元数据例如布局或默认尺寸等。然后provider作为与Widget连接的基本方法,会链接账户来自定义布局并更新Widget,且provider会作为一个接收方接收广播来提供更新。
下图引自Android developer
4.创建第一个Android小部件
根据前文,我们知道了创建一个Widget需要创建以下几个文件:AppWidgetProvider,AppWidgetProviderInfo。
第一步:创建WidgetProvider
首先创建一个FirstWidgetProvider继承AppWidgetProvider类,在其中,我们需要重写AppWidgetProvider中的方法,在这里onUpdate()方法方法较为重要,它们主要用于widget的动态变化实现,后文会着重讲这个方法。
public class FirstWidgetProvider extends AppWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
}
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
}
@Override
public void onEnabled(Context context) {
}
@Override
public void onDisabled(Context context) {
}
}
第二步:创建WidgetProviderInfo.xml
然后再创建对应的first_widget_provider_info.xml,这里对最小宽度和高度进行设定,以设定初始widget的大小。并且设置初始布局为first_widget_provider.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_widget_description"
android:initialLayout="@layout/first_widget_provider"
android:minWidth="200dp"
android:minHeight="150dp"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen" />
第三步: 创建first_widget_provider.xml
在widget的布局first_widget_provider.xml中,我们只进行了简单的一个TextView的显示。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.FirstWidget.AppWidget.Container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/Theme.FirstWidget.AppWidgetContainer">
<TextView
android:id="@+id/appwidget_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_margin="8dp"
android:text="@string/appwidget_text"
android:textSize="24sp"
android:textStyle="bold|italic"
android:textAlignment="center"/>
</RelativeLayout>
第四步:在AndroidManifest注册
设计好的Widget应当在AndroidManifest注册,才可以在widget选择器中选择
<receiver
android:name=".FirstWidgetProvider"
android:exported="true"
android:label="FirstWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/first_widget_provider_info" />
</receiver>
效果
最终得到一个EXAMPLE字体样式的简单widget,这就是基本widget创建的流程了
二、回到正题,我们如何让widget能够在选择器中动态预览呢
在从widget选择器中选择时,发现了安卓自己提供的widget中,clock小部件能够在选择器中随着时间变化动态预览,包括时钟和时间显示小部件
在Android15以前,widget的预览主要通过widget的布局中设置属性android:previewImage
和android:previewLayout
设置用户在选择器中观察到的预览
假设我们不对这两个属性进行设置的话,在选择器中看到的就是一个默认的安卓图标,什么预览都没有,这很影响用户对小部件的选择,用户不知道该小部件具体是做什么的,因此用户只能将其拖出进行查看。
为了优化用户的使用体验,对属性android:previewImage
和android:previewLayout
进行设置,设置一个静态预览,用户能够了解到该widget的功能。
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_widget_description"
android:initialLayout="@layout/first_widget_provider"
android:minWidth="40dp"
android:minHeight="40dp"
android:previewImage="@drawable/preview"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="1"
android:targetCellHeight="1"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen" />
如下图,FirstWidget的预览图变为我设置的@drawable/preview
而这仅仅是一个静态图,我们应当如何做,才能将preview动态变化。我将目光放到了widgetProvider类以及Generated Previews API上
widget动态预览的实现
尽管提供了Generated Previews API,我们仍然很难对widget的预览进行实时更新,那么有没有办法对其依照当前信息判断来动态改变widget的预览呢。
根据官网给出的解释:由于没有用于提供预览的回调,因此应用可以选择在运行期间的任何时间发送预览。预览的更新频率取决于微件的用例。那么,我们只要将widget的preview的设置的方法,放在对于动态widget的onUpdate()方法即可实现widget的动态预览。
代码编写
第一步:编写TimeBasedWidgetProvider
在这之中,实现了onUpdate()方法,由于onUpdate()
方法在widget初次加入、更新等都会调用,在这里面,对widget更新,同时对widget preview更新。
我们设计了setUpdateAlarm
方法,使得其定期更新widget。
然后还设计了updateWidget
,用于对widget的更新,具体方法就是用RemoteViews进行远程view的设置,使得widget能够更新RemoteVeiws,加载布局,更改widget的样式。主要是判断了此时的时间,然后更新textview和imageview,分为早上,中午,晚上。
然后在updateWidget
方法中,我们还设计调用了setGeneratedPreview
方法,同样是通过创建RemoteViews,来设置加载预览的内容,逻辑相同,只是此处调用了Android 15提供的Generated Previews API
中的方法setWidgetPreview()
,该方法主要有3个参数:
1、ComponeName
这个参数指定了 Widget 的组件,主要是确定哪个 Widget 接收器(AppWidgetProvider)将会提供该预览,appContext
是应用的上下文,SociaLiteAppWidgetReceiver::class.java
是你的 AppWidgetProvider
类,它负责定义该 Widget 的行为和界面。你需要传递这个 Widget 接收器的 Class 对象。
2、AppWidgetProviderInfo
这是指定 Widget 所在类别的常量,用于告诉系统这个预览图适用于哪个类别的 Widget。AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
:表示该 Widget 用于桌面屏幕。
3、RemoteViews
是用于创建 Widget 布局和视图的类,也是我们前面所说的远程视图用于widget加载布局。
AppWidgetManager.getInstance(appContext).setWidgetPreview(
ComponentName(
appContext,
SociaLiteAppWidgetReceiver::class.java
),
AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN,
RemoteViews("com.example", R.layout.widget_preview)
)
public class TimeBasedWidgetProvider extends AppWidgetProvider {
// 小部件更新间隔,单位为毫秒
private static final long UPDATE_INTERVAL = 1 * 1000; // 每60秒更新一次
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
Log.i("???", "onUpdate");
// 每次小部件更新时都检查时间并更新内容
for (int appWidgetId : appWidgetIds) {
updateWidget(context, appWidgetManager, appWidgetId);
}
// 设置定时任务,定期更新小部件内容
setUpdateAlarm(context);
}
private void setUpdateAlarm(Context context) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, TimeBasedWidgetProvider.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
long triggerTime = SystemClock.elapsedRealtime() + UPDATE_INTERVAL;
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME, triggerTime, UPDATE_INTERVAL, pendingIntent);
}
private void updateWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
Log.i("???", "updateWidget");
// 创建RemoteViews对象,加载布局
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
// 获取当前时间段
String timeText = getTimeText();
int imageResId = getImageResId();
// 更新TextView和ImageView内容
remoteViews.setTextViewText(R.id.widget_text, timeText);
remoteViews.setImageViewResource(R.id.widget_image, imageResId);
// 推送更新
appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
// 设置生成的预览
setGeneratedPreview(context);
}
private String getTimeText() {
// 获取当前时间段的文本
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (hour >= 6 && hour < 12) {
return "早安";
} else if (hour >= 12 && hour < 18) {
return "下午好";
} else {
return "晚上好";
}
}
private int getImageResId() {
// 获取当前时间段的图标
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (hour >= 6 && hour < 12) {
return R.drawable.ic_morning; // 早晨图标
} else if (hour >= 12 && hour < 18) {
return R.drawable.ic_afternoon; // 下午图标
} else {
return R.drawable.ic_night; // 晚上图标
}
}
// 设置生成的预览(随时间变化)
private void setGeneratedPreview(Context context) {
Log.i("???", "setGeneratedPreview");
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
ComponentName componentName = new ComponentName(context, TimeBasedWidgetProvider.class);
// 创建RemoteViews,设置预览内容
RemoteViews previewRemoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
// 预览内容同样根据时间段来显示
previewRemoteViews.setTextViewText(R.id.widget_text, getTimeText());
previewRemoteViews.setImageViewResource(R.id.widget_image, getImageResId());
// 设置预览
appWidgetManager.setWidgetPreview(componentName, AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN, previewRemoteViews);
}
}
第二步:编写appwidget_info.xml
这里主要设定了一个initialLayout和previewLayout,initialLayout
主要用于初始化布局, previewLayout
用于预览的布局,接下来我们来定义对应的布局
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="250dp"
android:minHeight="100dp"
android:updatePeriodMillis="1000"
android:initialLayout="@layout/widget_layout"
android:previewLayout="@layout/widget_layout"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen" />
第三步:定义widget_layout.xml
这里我们定义了一个TextView和ImageView,并且设定了text和对应的图片src,如下图所示为例。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<TextView
android:id="@+id/widget_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello, Widget!"
android:textSize="18sp"
android:padding="16dp"/>
<ImageView
android:id="@+id/widget_image"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_morning"/>
</LinearLayout>
第四步:在AndroidManifest注册
<receiver android:name=".TimeBasedWidgetProvider"
android:label="时间变化小部件"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info" />
</receiver>
结果与分析
我们在前面appwidget_info.xml
中设定了android:previewLayout="@layout/widget_layout"
,那么按道理来说我们的widget预览图原本应当是如上图的"Hello, Widget!"以及太阳的图标。那么接下来我们来看具体是什么样的吧。
预览图:
实际widget图:
现在是晚上2点38分,可以发现无论是实际图还是预览图,他们都是根据当前晚上的时间显示的布局包括图片、文字。也就是说,此widget的预览图成功进行了动态预览。在onUpdate
方法调用时,同时调用了对应的setGeneratedPreview
方法动态设置了widget的预览图。
验证
我们在onUpdate
方法,updateWidget
方法,setGeneratedPreview
方法分别设置了log的输出(参考上文完整代码),最终在logcat捕获到了以下信息,验证了setGeneratedPreview的正确调用。
小结
最终我们成功通过setWidgetPreview
方法,在onUpdate时对widget的预览图进行了动态改变。
参考链接:Android 15 功能和 API 概览 | Android Developer
创建简单的 widget
创建高级 widget