小部件是主屏幕定制的一个重要方面。您可以将它们视为应用程序最重要的数据和功能的“概览”视图,这些数据和功能可以直接在用户的主屏幕上访问。用户可以在主屏幕面板上移动小部件,如果支持的话,还可以调整它们的大小以根据自己的喜好定制小部件中的信息量。
简单UI的小部件
此类小部件通常仅显示关键信息元素,布局简单。小部件属于RemoteViews
,常用的控件是支持的,如TextView、Images,但是不支持自定义的控件,具体参考:创建应用微件布局
- 声明
AppWidgetProviderInfo
XML:
定义了小部件的基本品质。AppWidgetProviderInfo
使用单个元素在 XML 资源文件中 定义对象<appwidget-provider>
并将其保存在项目的res/xml/
文件夹中。
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/all_note_appwidget_layout"
android:minWidth="150dp"
android:minHeight="54dp"
android:previewImage="@drawable/todo_appwidget_preview"
android:resizeMode="none"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen" />
- initialLayout:指向定义小部件布局的布局资源
- previewImage:在选择应用小部件时看到的预览图
- updatePeriodMillis:小部件更新频率,实际更新不完全按时进行
- widgetCategory:声明您的小部件是否可以显示在主屏
home_screen
、锁定屏幕keyguard
或两者上
- 在清单中声明一个小部件:
AndroidManifest.xml
中声明小部件信息
<receiver
android:name=".widget.AllNoteAppWidgetProvider"
android:label="@string/all_notes"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <!-- 必须 -->
<action android:name="com.android.note.widget.UPDATE_ALL_NOTE"/> <!-- 自定义 -->
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/all_note_appwidget_info" />
</receiver>
- receiver:指定AppWidgetProvider小部件
- intent-filter:指定接受 AppWidgetProvider广播。
ACTION_APPWIDGET_UPDATE
这是您必须显式声明的唯一广播。根据需要自动 AppWidgetManager 将所有其他小部件广播发送到AppWidgetProvider。还可以自定义其他广播用于更新小部件的数据视图 - meta-data:
android:name
指定元数据名称,用于android.appwidget.provider
将数据标识为 AppWidgetProviderInfo描述符。android:resource
指定AppWidgetProviderInfo资源位置。
- 使用AppWidgetProvider来处理小部件广播
类AppWidgetProvider扩展 BroadcastReceiver为一个便利类来处理小部件广播,它仅接收与小部件相关的事件广播,例如小部件何时更新、删除、启用和禁用。所以我们重写AppWidgetProvider处理小部件广播并更新小部件以响应小部件生命周期事件。
public class AllNoteAppWidgetProvider extends AppWidgetProvider {
public static final String UPDATE_ALL_NOTE_COUNT_ACTION = "com.android.note.widget.UPDATE_ALL_NOTE";
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 当添加相同的小部件时,需要全部遍历更新
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
@Override
public void onReceive(Context context, Intent intent) {
// 接收到自定义的广播,用于做处理后更新小部件
if (UPDATE_ALL_NOTE_COUNT_ACTION.equals(intent.getAction())) {
final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
final int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context,
this.getClass()));
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
} else {
super.onReceive(context, intent);
}
}
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
// 小部件的布局,实际就是RemoteViews
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.all_note_appwidget_layout);
//做数据处理
NoteDataManager manager = NoteDataManagerImpl.getInstance(context);
int size = manager.getNotesForPresetFolder(NoteDataManagerImpl.ALL_NOTE_FOLDER).size();
// 更新视图,作为RemoteViews,只能使用其通过的方法来设置值
views.setTextViewText(R.id.size, context.getString(R.string.widget_note_count, size));
// 添加一个跳转应用的意图
Intent composeIntent = new Intent(context, NoteEditorActivity.class);
composeIntent.putExtra(NoteEditorPresenter.OPEN_TYPE, 1);
composeIntent.putExtra(FolderUtil.KEY_ID, 0);
composeIntent.putExtra(FolderUtil.KEY_FILTER_TYPE, 0);
PendingIntent composeNoteIntent = PendingIntent.getActivity(context, WidgetUtil.getUniqueCode(),
composeIntent, PendingIntent.FLAG_IMMUTABLE);
views.setOnClickPendingIntent(R.id.btn_add, composeNoteIntent);
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra(Constants.KEY_DEFAULT_VIEW, Constants.TAG_NOTE);
PendingIntent pendingIntent = PendingIntent.getActivity(context, WidgetUtil.getUniqueCode(),
intent, PendingIntent.FLAG_MUTABLE);
views.setOnClickPendingIntent(R.id.header, pendingIntent);
// 通过 appWidgetManager 更新视图
appWidgetManager.updateAppWidget(appWidgetId, views);
}
// 用于外部调用更新小部件
public static void updateCount(Context context) {
context.sendBroadcast(new Intent(UPDATE_ALL_NOTE_COUNT_ACTION, null, context, AllNoteAppWidgetProvider.class));
}
}
- onReceive(Context, Intent):接收来自小部件的广播或者自定义的广播,以便来做一些操作。
- onUpdate():更新小部件调用此方法。当用户添加小部件时也会调用此方法,因此它应该执行基本设置,例如为 View对象定义事件处理程序或启动作业来加载要在小部件中显示的数据。
- 需要更新小部件时的更新方法
AllNoteAppWidgetProvider.updateCount(context);
- 效果参考
含集合的小部件
集合小部件专门用于显示相同类型的许多元素,例如来自图库应用程序的图片集合、来自新闻应用程序的文章或来自通信应用程序的消息。集合小部件通常专注于两个用例:浏览集合以及将集合的元素打开到其详细视图。集合小部件可以垂直滚动。小部件使用以下视图类型之一呈现数据,这些视图类型称为集合视图:
- ListView:显示垂直滚动列表中的项目的视图。
- GridView:显示二维滚动网格中的项目的视图。
- StackView:堆叠卡片视图(有点像名片盒),用户可以向上或向下轻拂前面的卡片以分别查看上一张或下一张卡片。
- AdapterViewFlipper:一个由适配器支持的简单动画 ViewAnimator,可以在两个或多个视图之间进行动画处理。一次只显示一个孩子。
由于这些集合视图由适配器支持,因此 Android 框架必须包含额外的架构来支持它们在小部件中的使用。在小部件的上下文中,Adapter
被替换为 RemoteViewsFactory
,它是界面的薄包装Adapter。当请求集合中的特定项目时,RemoteViewsFactory将创建集合的项目并将其作为对象返回 RemoteViews。要在您的小部件中包含集合视图,请实现RemoteViewsService
和 RemoteViewsFactory
。
关于创建小部件的大部分配置同上操作,关于集合视图的小部件其他操作步骤如下:
- 小部件的initialLayout中含有集合视图,如
ListView
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/app_widget_background"
android:orientation="vertical"
android:padding="16dp">
<ListView
android:id="@+id/note_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/widget_header"
android:listSelector="@drawable/transparent_selector"
android:scrollbars="none" />
</RelativeLayout>
- 在
AndroidManifest.xml
中声明RemoteViewsService
<service
android:name=".widget.NoteListWidgetService"
android:exported="true"
android:permission="android.permission.BIND_REMOTEVIEWS" />
- permission:通过在清单文件中使用 权限 声明该服务来执行此操作
BIND_REMOTEVIEWS
,可以防止其他应用程序随意访问您的小部件的数据。
- 实现
RemoteViewsService
和RemoteViewsFactory
public class NoteListWidgetService extends RemoteViewsService {
private static final String TAG = "NoteListWidgetService";
public NoteListWidgetService() {
super();
}
@Override
public IBinder onBind(Intent intent) {
return super.onBind(intent);
}
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
// 创建 RemoteViewsFactory
return new ListViewRemoteViewsFactory(this, intent);
}
private static class ListViewRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private Context mContext;
private int mAppWidgetId;
private ArrayList<NoteItem> mItems = new ArrayList<>();
private AppWidgetManager mAppWidgetManager;
public ListViewRemoteViewsFactory(Context context, Intent intent) {
mContext = context;
// 获取传递来的 AppWidgetId
mAppWidgetId =intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
@Override
public void onCreate() {
// 加载数据
NoteDataManager mManager = NoteDataManagerImpl.getInstance(mContext);
mItems = (ArrayList)mManager.getNotesForPresetFolder(2);
}
@Override
public void onDataSetChanged() {
// 更新数据
NoteDataManager mManager = NoteDataManagerImpl.getInstance(mContext);
mItems = (ArrayList) mManager.getNotesForPresetFolder(2);
}
@Override
public void onDestroy() {
mItems.clear();
}
@Override
public int getCount() {
return Constants.WIDGET_LIST_COUNT;
}
@Override
public RemoteViews getViewAt(int i) {
int size = mItems.size();
if (i < size) {
NoteItem item = mItems.get(i);
Date date = new Date(item.getLongDate());
// 创建的ListView项的第一种布局类型
RemoteViews itemView = new RemoteViews(mContext.getPackageName(), R.layout.widget_listview_item_note);
itemView.setTextViewText(R.id.note_time, DateUtil.getDisplayNoteTime(mContext, date));
itemView.setTextViewText(R.id.note_title, NoteUtils.parseContent(mContext, item.getContent(), true));
// 点击跳转应用,及传递参数,这里没有使用PendingIntent,而是在AppWidgetProvider中
//通过setPendingIntentTemplate创建了模板,这里只需要Intent即可
Intent fillInIntent = new Intent();
fillInIntent.putExtra(FolderUtil.KEY_ID, item.getId());
fillInIntent.putExtra(NoteEditorPresenter.OPEN_TYPE, NoteEditorPresenter.TYPE_EDIT_NOTE);
itemView.setOnClickFillInIntent(R.id.listview_linearlayout, fillInIntent);
return itemView;
}
//这里属于ListView项的第二种布局类型
return new RemoteViews(mContext.getPackageName(), R.layout.app_widget_note_placeholder);
}
@Override
public RemoteViews getLoadingView() {
return null;
}
@Override
public int getViewTypeCount() {
// 布局类型的数目
return 2;
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public boolean hasStableIds() {
return false;
}
}
}
关于 RemoteViewsService.RemoteViewsFactory
:类似于Adapter的使用
- onCreate:首次加载,提供数据
- onDataSetChanged:后续更新,提供新数据
- onDestroy:清除数据
- getCount:ListView显示数据的条数
- getViewAt:返回ListView每项的视图,数据和界面更新的操作都在这里
- getViewTypeCount:ListView项的视图种类的个数,用于实现不同的布局,至少返回
1
,否则有数据也不显示,显示正在加载中
- getItemId:返回当前项的位置
- 在
AppWidgetProvider
更新时候调用
// 通过 NoteListWidgetService 更新,需要传递appWidgetId
Intent serviceIntent = new Intent(context, NoteListWidgetService.class);
serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
appWidgetIds[i]);
views.setRemoteAdapter(R.id.note_list, serviceIntent);
// 创建 PendingIntent 模板
Intent itemIntent = new Intent(context, NoteEditorActivity.class);
PendingIntent listPendingIntent = PendingIntent.getActivity(context, 1,
itemIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
views.setPendingIntentTemplate(R.id.note_list, listPendingIntent);
// 通过appWidgetManager更新 ListView,调用 RemoteViewsFactory 的 onDataSetChanged
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds[i], R.id.note_list);
- 效果参考:默认显示3条数据,若无,则使用空白占位
可配置的小部件
可控制的小部件
CheckBox
Android12更新
Android12微件改进
https://developer.android.google.cn/about/versions/12/features/widgets?hl=zh-cn
相关参考:
https://developer.android.com/guide/topics/appwidgets/overview?hl=zh-cn
https://developer.android.com/develop/ui/views/appwidgets/overview
https://developer.android.com/guide/topics/appwidgets?hl=zh-cn
https://developer.android.com/guide/practices/ui_guidelines/widget_design?hl=zh-cn#anatomy_determining_size
RemoteView
https://android-dot-google-developers.gonglchuangl.net/reference/android/widget/RemoteViews
sample
https://android.googlesource.com/platform/development/+/master/samples/ApiDemos/src/com/example/android/apis/appwidget