一.概念
1. AppWidgetProvider是什么
- 继承自
BroadcastReceiver,是桌面小组件的「控制器」 - 系统通过广播(
ACTION_APPWIDGET_UPDATEACTION_APPWIDGET_DELETED等)通知它「何时更新、何时删除」 - 一个类对应一种小组件,可同时在桌面存在多个实例
2.小组件的本质
- 远程
View(RemoteViews)+ 定时广播 +PendingIntent触发 - 运行在非主进程,不能 直接操作
findViewById,必须通过RemoteViews提供的 API 设置文本、图片、点击事件等
3.主要控件 RemoteViews
RemoteViews 是 Android 中专门用来"跨进程更新 UI"的轻量级视图对象。它并不是真正的 View,而是一份"描述文件":你把想做的 UI 操作(改文字、换图片、设点击事件等)先记录到 RemoteViews 里,然后一次性交给系统,系统会在另一个进程(通知栏或桌面)里帮你重建并应用这些操作。因此,它天生就适合通知栏、桌面小组件、锁屏等"本 App 无法直接触碰"的远程场景。
一、核心特点
- 跨进程
运行在 SystemServer / Launcher 等外部进程,本进程无法findViewById,也无法直接操作 View。 - 操作原子化
所有修改(文本、图片、可见性、点击事件)先缓存成 Action 列表,调用notify()或updateAppWidget()时才一次性通过 Binder 递送并在远端反射执行。 - 仅支持有限视图
布局:LinearLayout、FrameLayout、RelativeLayout、GridLayout、ListView、GridView、StackView、ViewFlipper、AdapterViewFlipper
控件:TextView、Button、ImageView、ImageButton、ProgressBar、AnalogClock、Chronometer、TextClock
API 31+ 新增:CheckBox、RadioButton、RadioGroup、Switch
除此之外一律不支持,包括它们的子类或自定义 View - 常见用法
| 目标 | 示例 |
|---|---|
| 设置文本 | remoteViews.setTextViewText(R.id.tv, "hello widget"); |
| 设置子控件高度 | remoteViews.setInt(R.id.tv, "setHeight", 20); |
| 换图片 | remoteViews.setImageViewBitmap(R.id.iv, bitmap); |
| 绑定点击事件 | remoteViews.setOnClickPendingIntent(R.id.digital_widget, pendingIntent); |
注意:点击事件必须通过 PendingIntent 封装,支持启动 Activity、Service 或发送广播
二、小组件的简单使用
- 创建小组件
每创建一个小组件将在AndroidManifest.xml注册接收者
java
<receiver
android:name=".widget.NewWidget"
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/new_widget_info" />
</receiver>
元数据文件 res/xml/new_widget_info.xml
java
<?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:initialKeyguardLayout="@layout/new_widget"
android:initialLayout="@layout/new_widget"
android:minWidth="40dp"
android:minHeight="40dp"
android:previewImage="@drawable/example_appwidget_preview"
android:resizeMode="none"
android:updatePeriodMillis="0" <!-- 自动刷新周期 -->
android:widgetCategory="home_screen|keyguard" />
布局文件 res/layout/widget_layout.xml
ini
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/digital_widget"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="fitXY" />
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:singleLine="true"
android:text="title"
android:textColor="@color/white"
android:textSize="12dp"
android:visibility="invisible" />
</LinearLayout>
运行 → 长按桌面 → 小组件 → 找到你的 Demo 拖到桌面即可

三、高阶小组件:实现轮播
实现小组件轮播并在每一页绑定不同的点击事件,可通过AdapterViewFlipper轮播容器结合 remoteViews.setRemoteAdapter绑定RemoteViewsService进行轮播刷新数据
布局digital_widget.xml
java
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/digital_widget"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<AdapterViewFlipper
android:id="@+id/view_flipper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateFirstView="false"
android:autoStart="true"
android:flipInterval="3000"
android:inAnimation="@animator/alpha_in"
android:outAnimation="@animator/alpha_out" />
</LinearLayout>
在接收到小组件创建刷新广播时进行remoteViews的绑定与刷新
java
private void updateWidget(Context context, AppWidgetManager wm, int widgetId, WidgetBean info) {
RemoteViews remoteViews = relayoutWidget(context, wm, widgetId, info);
wm.updateAppWidget(widgetId, remoteViews);
}
private RemoteViews relayoutWidget(Context context, AppWidgetManager wm, int widgetId, WidgetBean info) {
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.digital_widget);
rv.setViewVisibility(R.id.view_flipper, View.VISIBLE);
Intent serviceIntent = new Intent(context, ViewWidgetService.class);
Bundle bundle = new Bundle();
bundle.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
bundle.putString(CommonConstant.APP_WIDGET_DATA, new Gson().toJson(info));
serviceIntent.putExtras(bundle);
serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME)));
rv.setRemoteAdapter(R.id.view_flipper, serviceIntent);
Intent intent2 = new Intent(context, HandlerActivity.class);
intent2.setAction(CommonConstant.ITEM_CLICK_ACTION);
intent2.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntentTemplate = PendingIntent.getActivity(context, widgetId, intent2, PendingIntent.FLAG_UPDATE_CURRENT);
// 绑定轮播的点击事件
rv.setPendingIntentTemplate(R.id.view_flipper, pendingIntentTemplate);
wm.updateAppWidget(widgetId, rv);
wm.notifyAppWidgetViewDataChanged(widgetId, R.id.view_flipper);
return rv;
}
ViewWidgetService 继承RemoteViewsService
注册服务
java
<service
android:name=".service.ViewWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
java
public class ViewWidgetService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new ViewRemoteViewsFactory(this, intent);
}
}
ViewRemoteViewsFactory
java
public class ViewRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private static String TAG = ViewRemoteViewsFactory.class.getSimpleName();
private Context mContext;
private int mAppWidgetId;
private List<AdvertInfodetail> advertInfodetails;
private WidgetBean widgetBean;
public ViewRemoteViewsFactory(Context context, Intent intent) {
this.mContext = context;
// 获取传递的数据
Bundle bundle = intent.getExtras();
if (bundle != null) {
mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
String data = intent.getStringExtra(CommonConstant.APP_WIDGET_DATA);
widgetBean = new Gson().fromJson(data, WidgetBean.class);
if (widgetBean != null) {
advertInfodetails = ObjectUtil.jsonToAdvertInfoList(widgetBean.getExtra());
}
LogUtil.i(TAG, "new ViewRemoteViewsFactory:" + mAppWidgetId);
}
}
@Override
public void onCreate() {
LogUtil.i(TAG, "ViewRemoteViewsFactory onCreate:" + mAppWidgetId + ";" + Thread.currentThread().getName());
}
@Override
public void onDataSetChanged() {
LogUtil.i(TAG, "ViewRemoteViewsFactory onDataSetChanged:" + mAppWidgetId);
}
@Override
public void onDestroy() {
LogUtil.i(TAG, "ViewRemoteViewsFactory onDestroy:" + mAppWidgetId);
}
@Override
public int getCount() {
return advertInfodetails == null ? 0 : advertInfodetails.size();
}
@Override
public RemoteViews getViewAt(int position) {
// 绑定轮播的单个视图
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.item);
if (TextUtils.equals(CommonConstant.SPAN1X1, widgetBean.getSpanXY()) || advertInfodetails.size() <= 1) {
rv.setViewVisibility(R.id.ll_indicator, View.GONE);
} else {
showIndicatorView(rv, position);
rv.setViewVisibility(R.id.ll_indicator, View.VISIBLE);
}
AdvertInfodetail info = advertInfodetails.get(position);
try {
Bitmap bitmap = Glide.with(mContext).asBitmap().load(info.getImage()).submit().get();
Bitmap compare = ImageUtil.computeMaximumWidgetBitmap(mContext, bitmap);
Bitmap showBitmap = ImageUtil.get1x1RoundBitmap(compare, mContext, widgetBean.getSpanXY());
rv.setImageViewBitmap(R.id.pic, showBitmap);
} catch (Exception e) {
LogUtil.e(TAG, e.getMessage());
Bitmap placeholder;
if (CommonConstant.SPAN4X2.equals(widgetBean.getSpanXY())) {
placeholder = ImageUtil.drawableToBitmap(mContext.getResources().getDrawable(R.mipmap.bg_placeholder_4x2));
} else if (CommonConstant.SPAN1X1.equals(widgetBean.getSpanXY())) {
placeholder = ImageUtil.drawableToBitmap(mContext.getResources().getDrawable(R.mipmap.bg_placeholder));
} else {
placeholder = ImageUtil.drawableToBitmap(mContext.getResources().getDrawable(R.mipmap.bg_placeholder_2x2));
}
rv.setImageViewBitmap(R.id.pic, ImageUtil.get1x1RoundBitmap(placeholder, mContext, widgetBean.getSpanXY()));
}
Bundle bundle = new Bundle();
bundle.putInt(CommonConstant.APP_WIDGET_ID_KEY, mAppWidgetId);
bundle.putString(CommonConstant.WIDGET_INFO_KEY, ObjectUtil.beanToJson(info));
if (widgetBean != null) {
bundle.putString(CommonConstant.APP_WIDGET_CLICK_TITLE, widgetBean.getTitle());
bundle.putString(CommonConstant.APP_WIDGET_CLICK_SPANXY, widgetBean.getSpanXY());
}
Intent fillIntent = new Intent();
fillIntent.putExtras(bundle);
// 实现单个的不同点击事件
rv.setOnClickFillInIntent(R.id.item, fillIntent);
return rv;
}
private void showIndicatorView(RemoteViews rv, int position) {
for (int i = 0; i < advertInfodetails.size(); i++) {
int indicatorId = mContext.getResources().getIdentifier("indicator" + i, "id", mContext.getPackageName());
rv.setViewVisibility(indicatorId, View.VISIBLE);
rv.setImageViewResource(indicatorId, position == i ? R.mipmap.ic_indicator_select : R.mipmap.ic_indicator_normal);
}
}
@Override
public RemoteViews getLoadingView() {
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.item);
return rv;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return true;
}
}
最终实现效果图:
