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;
    }


}

最终实现效果图:

相关推荐
xiangpanf2 小时前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx5 小时前
安卓线程相关
android
消失的旧时光-19435 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon6 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon6 小时前
VSYNC 信号完整流程2
android
dalancon6 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013847 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android8 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才8 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶9 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle