Android ---【案例篇】带你学习Widget,从Launcher加载各个应用的Widget到各个应用是怎么创建的Widget整个流程

一、App 如何定义 Widget(小卡片)

App 需要通过固定的组件和配置文件声明 Widget,系统会识别这些信息并允许 Launcher 加载。

1. 定义 Widget 的 “配置信息”(必选)

在 res/xml/ 目录下创建 Widget 的配置文件(如 appwidget_info.xml),声明 Widget 的大小、更新频率、布局等基础信息:

<!-- res/xml/appwidget_info.xml -->
<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    <!-- Widget的最小尺寸(宽x高,单位:dp),Launcher会根据这个适配网格 -->
    android:minWidth="180dp"
    android:minHeight="180dp"
    <!-- 拉伸时的自适应尺寸(可选,用于不同屏幕适配) -->
    android:resizeMode="horizontal|vertical"
    <!-- Widget的布局文件 -->
    android:initialLayout="@layout/widget_layout"
    <!-- 自动更新间隔(毫秒,0表示不自动更新) -->
    android:updatePeriodMillis="0"
    <!-- 点击Widget时打开的Activity(可选) -->
    android:configure="com.example.MyWidgetConfigureActivity"/>
2. 定义 Widget 的布局(必选)

在 res/layout/ 目录下创建 Widget 的布局文件(如 widget_layout.xml),定义小卡片的 UI:

<!-- res/layout/widget_layout.xml -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:background="@drawable/widget_bg">

    <TextView
        android:id="@+id/widget_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我的小卡片"
        android:textSize="16sp"/>

</LinearLayout>
3. 实现 Widget 的逻辑处理类(必选)

创建一个类继承 AppWidgetProvider(本质是广播接收器),处理 Widget 的创建、更新、删除等生命周期事件:

// com/example/MyAppWidgetProvider.java
public class MyAppWidgetProvider extends AppWidgetProvider {
    public static final String ACTION_WIDGET_CLICK = "com.example.action.WIDGET_CLICK";
    public static final String EXTRA_WIDGET_ID = "widget_id";

    // Widget首次添加到桌面时调用
    @Override
    public void onEnabled(Context context) {
        super.onEnabled(context);
        // 初始化数据(如开启定时任务)
    }
    @Override
    public void onReceive(Context context, Intent intent) {
        if (ACTION_WIDGET_CLICK.equals(intent.getAction())) {
            // 接收Widget点击的广播,执行逻辑(如刷新数据、切换开关状态)
            int widgetId = intent.getIntExtra(EXTRA_WIDGET_ID, -1);
            Log.d("WidgetClick", "收到点击,Widget ID:" + widgetId);

            // 示例:刷新Widget数据
            refreshWidgetData(context, widgetId);
        }
    }

    // 刷新Widget的具体逻辑
    private void refreshWidgetData(Context context, int widgetId) {
        AppWidgetManager manager = AppWidgetManager.getInstance(context);
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
        views.setTextViewText(R.id.widget_text, "点击已处理,时间:" + System.currentTimeMillis());
        manager.updateAppWidget(widgetId, views);
    }

    // Widget需要更新时调用(如定时更新或手动触发)
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        // 遍历所有已添加的Widget实例,更新UI
        for (int widgetId : appWidgetIds) {
            updateWidgetUI(context, appWidgetManager, widgetId);
        }
    }

    // 更新Widget的UI
    private void updateWidgetUI(Context context, AppWidgetManager manager, int widgetId) {
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);

        // 1. 创建一个发送广播的Intent
        Intent clickIntent = new Intent(WidgetClickReceiver.ACTION_WIDGET_CLICK);
        clickIntent.putExtra(WidgetClickReceiver.EXTRA_WIDGET_ID, widgetId);
        // 注意:跨进程广播需要设置包名,避免其他App接收
        clickIntent.setPackage(context.getPackageName());

        // 2. 创建PendingIntent(用于发送广播)
        PendingIntent pendingIntent = PendingIntent.getBroadcast(
            context,
            widgetId, // 请求码,确保唯一
            clickIntent,
            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );

        // 3. 给按钮设置点击事件(绑定PendingIntent)
        views.setOnClickPendingIntent(R.id.widget_btn, pendingIntent);

        // 更新Widget
        manager.updateAppWidget(widgetId, views);
    }

    // Widget被删除时调用
    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        super.onDeleted(context, appWidgetIds);
        // 清理资源(如取消定时任务)
    }
}
4. 在 Manifest 中声明 Widget(必选)

将 Widget 的 Provider 注册到 AndroidManifest.xml 中,系统才能识别:

<!-- AndroidManifest.xml -->
<application>
    <!-- 声明Widget的Provider(本质是广播接收器) -->
    <receiver android:name=".MyAppWidgetProvider">
        <intent-filter>
            <!-- 接收Widget相关的系统广播 -->
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
        </intent-filter>
        <!-- 关联Widget的配置信息 -->
        <meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/appwidget_info"/>
    </receiver>
</application>

至此,App 的 Widget 定义完成。系统会通过 Manifest 识别该 Widget,并允许 Launcher 查询和加载。

二、Launcher 如何加载 Widget(小卡片)

Launcher 作为桌面应用,通过 Android 系统提供的 AppWidgetManager 和 AppWidgetHost 框架加载并显示 Widget,核心流程如下:

1. 初始化 Widget 宿主(AppWidgetHost)

Launcher 需要创建一个 AppWidgetHost 实例,作为 Widget 的 “容器”,负责与系统交互管理 Widget:

// Launcher的初始化代码(如LauncherActivity或桌面Fragment)
public class Launcher extends Activity {
    private AppWidgetManager mAppWidgetManager;
    private AppWidgetHost mAppWidgetHost;
    private static final int APPWIDGET_HOST_ID = 10086; // 宿主ID,自定义唯一值

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 初始化AppWidgetManager(系统服务,管理所有Widget)
        mAppWidgetManager = AppWidgetManager.getInstance(this);
        // 初始化AppWidgetHost(Launcher作为宿主)
        mAppWidgetHost = new AppWidgetHost(this, APPWIDGET_HOST_ID);
        // 启动Host,接收Widget的更新事件
        mAppWidgetHost.startListening();
    }
}
2. 查询系统中所有可用的 Widget

Launcher 通过 AppWidgetManager 获取已安装 App 的所有 Widget 信息(如名称、图标、配置信息),用于在 “添加 Widget” 列表中展示:

// 获取所有可用的Widget信息
List<AppWidgetProviderInfo> allWidgets = mAppWidgetManager.getInstalledProviders();

// 遍历Widget信息,展示到UI列表中
for (AppWidgetProviderInfo info : allWidgets) {
    // Widget的名称(从App的AndroidManifest中获取)
    String widgetName = info.label;
    // Widget的图标
    Drawable widgetIcon = info.loadIcon(getPackageManager());
    // Widget的尺寸信息
    int minWidth = info.minWidth;
    int minHeight = info.minHeight;
    // ... 将这些信息显示到“添加Widget”的列表中
}
3. 用户选择 Widget 后,绑定并加载到桌面

当用户从列表中选择某个 Widget 时,Launcher 通过 AppWidgetHost 向系统请求 “绑定” 该 Widget,获取一个唯一的 widgetId,然后创建 AppWidgetHostView 显示 Widget:

// 用户选择Widget后,绑定Widget
private void addWidgetToLauncher(AppWidgetProviderInfo widgetInfo) {
    // 1. 向系统申请一个新的widgetId(唯一标识该Widget实例)
    int widgetId = mAppWidgetHost.allocateAppWidgetId();

    // 2. 检查是否需要配置(如果widgetInfo.configure不为空,表示需要先打开配置页)
    if (widgetInfo.configure != null) {
        // 启动配置Activity,配置完成后再绑定
        Intent configIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE);
        configIntent.setComponent(widgetInfo.configure);
        configIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
        startActivityForResult(configIntent, REQUEST_CONFIGURE_WIDGET);
    } else {
        // 3. 直接绑定Widget
        bindWidget(widgetId, widgetInfo);
    }
}

// 绑定Widget并显示到桌面
private void bindWidget(int widgetId, AppWidgetProviderInfo widgetInfo) {
    // 检查绑定权限(系统要求)
    boolean allowed = mAppWidgetManager.bindAppWidgetIdIfAllowed(widgetId, widgetInfo.provider);
    if (!allowed) {
        // 权限不足,通常是系统限制,忽略
        return;
    }

    // 4. 创建Widget的显示视图(AppWidgetHostView)
    AppWidgetHostView hostView = mAppWidgetHost.createView(this, widgetId, widgetInfo);
    // 设置Widget的大小(根据Launcher的网格计算)
    hostView.setAppWidget(widgetId, widgetInfo);

    // 5. 将hostView添加到桌面的布局中(如Launcher的工作区)
    ViewGroup desktopContainer = findViewById(R.id.desktop_container);
    desktopContainer.addView(hostView);
}
4. 接收 Widget 的更新并刷新

Widget 的内容更新由 App 通过 AppWidgetManager 触发,Launcher 中的 AppWidgetHostView 会自动接收更新并刷新 UI(无需额外代码,框架已处理)。

核心原理总结
App 侧:通过 AppWidgetProvider 声明 Widget 的逻辑,appwidget_info.xml 定义配置,Manifest 注册后系统可识别。
Launcher 侧:通过 AppWidgetManager 查询 Widget 信息,AppWidgetHost 管理 Widget 实例,AppWidgetHostView 负责显示,最终将 Widget 添加到桌面布局。

这种设计是跨进程的:Widget 的 UI 和逻辑属于 App 进程,Launcher 通过系统服务间接控制显示,保证了安全性和独立性。

三、点击事件流程(广播方式)

1.当用户点击 Widget 上的按钮时

Launcher 进程:检测到点击,取出按钮绑定的 PendingIntent(包含广播 Intent 信息)。
系统进程:Launcher 将 PendingIntent 交给系统,系统解析出其中的广播 Intent(action 为com.example.action.WIDGET_CLICK,包名为 App 包名)。
App 进程:如果 App 未启动,系统会先启动 App 进程。
系统将广播发送给 App 中注册的 MyAppWidgetProvider。
MyAppWidgetProvider 的 onReceive 方法被调用,执行刷新 Widget 等逻辑。

核心逻辑上面有,下面再贴一遍:

// com/example/MyAppWidgetProvider.java
public class MyAppWidgetProvider extends AppWidgetProvider {
    public static final String ACTION_WIDGET_CLICK = "com.example.action.WIDGET_CLICK";
    public static final String EXTRA_WIDGET_ID = "widget_id";

    // Widget首次添加到桌面时调用
    @Override
    public void onEnabled(Context context) {
        super.onEnabled(context);
        // 初始化数据(如开启定时任务)
    }
    @Override
    public void onReceive(Context context, Intent intent) {
        if (ACTION_WIDGET_CLICK.equals(intent.getAction())) {
            // 接收Widget点击的广播,执行逻辑(如刷新数据、切换开关状态)
            int widgetId = intent.getIntExtra(EXTRA_WIDGET_ID, -1);
            Log.d("WidgetClick", "收到点击,Widget ID:" + widgetId);

            // 示例:刷新Widget数据
            refreshWidgetData(context, widgetId);
        }
    }

    // 刷新Widget的具体逻辑
    private void refreshWidgetData(Context context, int widgetId) {
        AppWidgetManager manager = AppWidgetManager.getInstance(context);
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
        views.setTextViewText(R.id.widget_text, "点击已处理,时间:" + System.currentTimeMillis());
        manager.updateAppWidget(widgetId, views);
    }

    // Widget需要更新时调用(如定时更新或手动触发)
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        // 遍历所有已添加的Widget实例,更新UI
        for (int widgetId : appWidgetIds) {
            updateWidgetUI(context, appWidgetManager, widgetId);
        }
    }

    // 更新Widget的UI
    private void updateWidgetUI(Context context, AppWidgetManager manager, int widgetId) {
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);

        // 1. 创建一个发送广播的Intent
        Intent clickIntent = new Intent(WidgetClickReceiver.ACTION_WIDGET_CLICK);
        clickIntent.putExtra(WidgetClickReceiver.EXTRA_WIDGET_ID, widgetId);
        // 注意:跨进程广播需要设置包名,避免其他App接收
        clickIntent.setPackage(context.getPackageName());

        // 2. 创建PendingIntent(用于发送广播)
        PendingIntent pendingIntent = PendingIntent.getBroadcast(
            context,
            widgetId, // 请求码,确保唯一
            clickIntent,
            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );

        // 3. 给按钮设置点击事件(绑定PendingIntent)
        views.setOnClickPendingIntent(R.id.widget_btn, pendingIntent);

        // 更新Widget
        manager.updateAppWidget(widgetId, views);
    }

    // Widget被删除时调用
    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        super.onDeleted(context, appWidgetIds);
        // 清理资源(如取消定时任务)
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梁同学与Android

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值