文章目录
一、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);
// 清理资源(如取消定时任务)
}
}


被折叠的 条评论
为什么被折叠?



