Android 小组件AppWidgetProvider的使用

一.概念

1. AppWidgetProvider是什么

  • 继承自 BroadcastReceiver,是桌面小组件的「控制器」
  • 系统通过广播(ACTION_APPWIDGET_UPDATE ACTION_APPWIDGET_DELETED等)通知它「何时更新、何时删除」
  • 一个类对应一种小组件,可同时在桌面存在多个实例

2.小组件的本质

  • 远程 ViewRemoteViews)+ 定时广播 + PendingIntent 触发
  • 运行在非主进程,不能 直接操作 findViewById,必须通过 RemoteViews 提供的 API 设置文本、图片、点击事件等

3.主要控件 RemoteViews

RemoteViews 是 Android 中专门用来"跨进程更新 UI"的轻量级视图对象。它并不是真正的 View,而是一份"描述文件":你把想做的 UI 操作(改文字、换图片、设点击事件等)先记录到 RemoteViews 里,然后一次性交给系统,系统会在另一个进程(通知栏或桌面)里帮你重建并应用这些操作。因此,它天生就适合通知栏、桌面小组件、锁屏等"本 App 无法直接触碰"的远程场景。

一、核心特点

  1. 跨进程
    运行在 SystemServer / Launcher 等外部进程,本进程无法 findViewById,也无法直接操作 View。
  2. 操作原子化
    所有修改(文本、图片、可见性、点击事件)先缓存成 Action 列表,调用 notify()updateAppWidget() 时才一次性通过 Binder 递送并在远端反射执行。
  3. 仅支持有限视图
    布局:LinearLayoutFrameLayoutRelativeLayoutGridLayoutListViewGridViewStackViewViewFlipperAdapterViewFlipper
    控件:TextViewButtonImageViewImageButtonProgressBarAnalogClockChronometerTextClock
    API 31+ 新增:CheckBoxRadioButtonRadioGroupSwitch
    除此之外一律不支持,包括它们的子类或自定义 View
  4. 常见用法
目标 示例
设置文本 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 或发送广播

二、小组件的简单使用

  1. 创建小组件

每创建一个小组件将在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;
    }


}

最终实现效果图:

相关推荐
弥巷2 小时前
【Android】常见滑动冲突场景及解决方案
android·java
angushine2 小时前
解决MySQL慢日志输出问题
android·数据库·mysql
fouryears_234172 小时前
Android 与 Flutter 通信最佳实践 - 以分享功能为例
android·flutter·客户端·dart
成都大菠萝3 小时前
Android ANR
android
Ryan ZHENG5 小时前
[Android][踩坑]Android Studio导入core-libart.jar
android·android studio·jar
q***R3085 小时前
Kotlin注解处理
android·开发语言·kotlin
Digitally5 小时前
如何将文件从三星平板传输到电脑
android
CHINAHEAO5 小时前
Bagisto单独将后台设置成中文
android
E***U9455 小时前
React Native开发
android·react native·react.js