App Widget桌面小部件已经被很多APP所使用,在日常的生活中也有很多人在使用,最常见的就是时钟、天气、日历、记事本这样的小工具,但是不少主流APP也提供了一些快捷功能的小部件,例如音乐快捷播放、代办事项、课程表、股票、购物快捷搜索、邮箱邮件等等,桌面小部件是APP部分业务功能的快捷入口,可以有效提升用户的使用体验缩短触达链路。
和Activity类似,在 XML 资源中为定义初始布局并将其保存在项目的 res/layout/目录中,App Widget 布局基于RemoteViews,它不支持所有类型的容器布局和视图控件。
RemoteViews 可支持的容器和控件如下:
容器
控件
并且,不支持继承了上述容器和控件的View,也就是不支持自定义View。
例如:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/my_widget_background">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent" />
FrameLayout>
AppWidgetProviderInfo 用于描述一个 App Widget 的基本属性,如布局文件、布局尺寸、布局可变方向、最小可变布局尺寸、预览图、更新频率、配置Activity。
使用单个
元素在 XML 资源文件中定义 AppWidgetProviderInfo 对象,并将其保存在项目的 res/xml 文件夹中。
例如:
<appwidget-provider
xmlns:android = "http://schemas.android.com/apk/res/android"
android:minWidth = "250dp"
android:minHeight = "110dp"
android:updatePeriodMillis = "86400000"
android:previewImage = "@drawable/preview"
android:initialLayout = "@layout/example_appwidget"
android:configure = "com.example.android.ExampleAppWidgetConfigure"
android:resizeMode = "horizontal|vertical" />
以下是
属性的摘要:
【注意】为适配不同的屏幕尺寸,小部件的最小尺寸不应大于 4 x 4 单元格。
【注意】如果设备在更新时处于睡眠状态,则设备将唤醒以执行更新。如果每小时更新不超过一次,这可能不会对电池寿命造成重大问题。但是,如果需要更频繁地更新或不需要在设备休眠时更新,那么可以根据不会唤醒设备的闹钟执行更新,使用 AppWidgetProvider 接收的 Intent 设置 AlarmManager,将闹钟类型设置为ELAPSED_REALTIME或 RTC,然后设置 updatePeriodMillis为0。
AppWidgetProvider类是小部件的核心类,继承了BroadcastReceiver,可以处理广播消息。
AppWidgetProvider 仅接收与 App Widget 相关的事件广播,例如 App Widget 何时更新、删除、启用和禁用。
当这些广播事件发生时,AppWidgetProvider 的主要生命周期方法会被调用:
例如:
public class ExampleAppWidgetProvider extends AppWidgetProvider {
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
final int N = appWidgetIds.length;
for (int i=0; i<N; i++) {
int appWidgetId = appWidgetIds[i];
// 创建一个打开Activity的Intent
Intent intent = new Intent(context, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
// 点击事件的处理通过setOnClickPendingIntent而不是setOnclickListener
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
views.setOnClickPendingIntent(R.id.button, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
}
小部件和广播的注册方式类似,AppWidgetProvider在AndroidManifest.xml文件中声明后即可生效:
<receiver android:name="ExampleAppWidgetProvider" >
<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>
【1.2 AppWidgetProviderInfo 元数据】中提到:updatePeriodMillis属性定义了系统更新 App Widget 的频率,且最小值为1800000——即30分钟。
但是大部分情况下,30分钟更新一次数据会产生很大的误差,在用户使用时必然是希望实时刷新的,这时候自动刷新已经无法满足用户的需求了,必须要进行手动触发刷新,提升更新的频率。
App Widget 是一个广播接收器,它的生命周期里的onUpdate()是一个特定的广播触发的,可以通过Service定时发送刷新的广播,达到提升更新频率的目的。
/**
* 控制 桌面小部件 更新
*/
public class WidgetService extends Service {
/**
* 周期性更新 widget 的周期
*/
private static final int UPDATE_TIME = 1000;
private Timer mTimer;
private TimerTask mTimerTask;
@Override
public void onCreate() {
super.onCreate();
// 每经过指定时间,发送一次广播
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
sendBroadcast(updateIntent);
}
};
mTimer.schedule(mTimerTask, UPDATE_TIME, UPDATE_TIME);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
mTimerTask.cancel();
mTimer.cancel();
}
}
这一类型样式的实现有一个共同点,需要依赖Adapter完成item的UI以及逻辑的编写。
平时开发中,需要通过目标控件的setAdapter()方法将对应的Adapter绑定到控件上,但是RemoteViews不能通过findViewById()等方法拿到目标控件的实例,也就无法使用Adapter。
这时候就需要使用RemoteViews.setRemoteAdapter(int viewId, Intent intent)方法设置,但这里用的是Intent,这个Intent指向一个RemoteViewsService(实现RemoteViewsFactory onGetViewFactory(Intent intent)方法)。
真实的Adapter就是RemoteViewsFactory实例了,通过其RemoteViews getViewAt(int position)方法完成item的UI以及逻辑的编写。
public class StackWidgetProvider extends AppWidgetProvider {
public static final String TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION";
public static final String EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM";
...
// Called when the BroadcastReceiver receives an Intent broadcast.
// Checks to see whether the intent's action is TOAST_ACTION. If it is, the app widget
// displays a Toast message for the current item.
@Override
public void onReceive(Context context, Intent intent) {
AppWidgetManager mgr = AppWidgetManager.getInstance(context);
if (intent.getAction().equals(TOAST_ACTION)) {
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);
Toast.makeText(context, "Touched view " + viewIndex, Toast.LENGTH_SHORT).show();
}
super.onReceive(context, intent);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// update each of the app widgets with the remote adapter
for (int i = 0; i < appWidgetIds.length; ++i) {
// Sets up the intent that points to the StackViewService that will
// provide the views for this collection.
Intent intent = new Intent(context, StackWidgetService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
// When intents are compared, the extras are ignored, so we need to embed the extras
// into the data so that the extras will not be ignored.
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);
// The empty view is displayed when the collection has no items. It should be a sibling
// of the collection view.
rv.setEmptyView(R.id.stack_view, R.id.empty_view);
// This section makes it possible for items to have individualized behavior.
// It does this by setting up a pending intent template. Individuals items of a collection
// cannot set up their own pending intents. Instead, the collection as a whole sets
// up a pending intent template, and the individual items set a fillInIntent
// to create unique behavior on an item-by-item basis.
Intent toastIntent = new Intent(context, StackWidgetProvider.class);
// Set the action for the intent.
// When the user touches a particular view, it will have the effect of
// broadcasting TOAST_ACTION.
toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);
toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
}
public class StackWidgetService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private static final int mCount = 10;
private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
private Context mContext;
private int mAppWidgetId;
public StackRemoteViewsFactory(Context context, Intent intent) {
mContext = context;
mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
// Initialize the data set.
public void onCreate() {
// In onCreate() you set up any connections / cursors to your data source. Heavy lifting,
// for example downloading or creating content etc, should be deferred to onDataSetChanged()
// or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
for (int i = 0; i < mCount; i++) {
mWidgetItems.add(new WidgetItem(i + "!"));
}
...
}
...
// Given the position (index) of a WidgetItem in the array, use the item's text value in
// combination with the app widget item XML file to construct a RemoteViews object.
public RemoteViews getViewAt(int position) {
// position will always range from 0 to getCount() - 1.
// Construct a RemoteViews item based on the app widget item XML file, and set the
// text based on the position.
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);
// Next, set a fill-intent, which will be used to fill in the pending intent template
// that is set on the collection view in StackWidgetProvider.
Bundle extras = new Bundle();
extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);
Intent fillInIntent = new Intent();
fillInIntent.putExtras(extras);
// Make it possible to distinguish the individual on-click
// action of a given item
rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);
...
// Return the RemoteViews object.
return rv;
}
...
}
如果希望用户在添加新的 App Widget 时配置一些自定义的设置,可以创建一个 App Widget 配置Activity,这时创建App Widget时不会调用 onUpdate() 方法。这个Activity 将自动启动,并允许用户在创建时配置 App Widget 的一些自定义设置,例如 App Widget 颜色、大小、更新周期或其他功能设置。
清单文件中添加:
<activity android:name=".ExampleAppWidgetConfigure">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
intent-filter>
activity>
此外,必须在 AppWidgetProviderInfo XML 文件中声明 Activity,并带有 android:configure属性:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
...
android:configure="com.example.android.ExampleAppWidgetConfigure"
... >
appwidget-provider>
当 App Widget 使用配置 Activity 时,Activity 负责在配置完成后更新 App Widget:
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
mAppWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();
【注意】当你的配置Activity打开时,将Activity结果设置为RESULT_CANCELED。这样,如果用户在到达结束之前退出 Activity,则通知 App Widget 配置已取消,并且不会添加 App Widget。
今天的分享就到这里,文章多有不足,各位小伙伴有什么想法可以直接评论或是私信,要是对你有所帮助就给我一个赞吧,喜欢我的小伙伴可以关注我哦~
支持我的小伙伴们可以微信搜索“Android思维库”,或者微信扫描下方二维码,关注我的公众号,每天都会推送新知识~