自定义 Launcher 加载第三方 App Widget
自定义 Launcher 加载第三方 Widget,不是把第三方 App 的布局文件拿过来 inflate,而是让系统完成绑定,再由 AppWidgetHost 创建 AppWidgetHostView:
text
AppWidgetManager
↓ 查询已安装的 AppWidgetProvider
AppWidgetHost.allocateAppWidgetId()
↓ 分配一个 Widget 实例 ID
bindAppWidgetIdIfAllowed()
↓ 未授权时弹出系统确认框
ACTION_APPWIDGET_CONFIGURE
↓ Provider 有配置页时先完成配置
AppWidgetHost.createView()
↓
把 AppWidgetHostView 加到 Launcher 桌面容器
第三方 App 提供的是 AppWidgetProvider 和 RemoteViews,Launcher 不需要知道它的具体布局实现;系统负责把更新后的 Widget 内容交给 HostView。
1. Manifest 配置
自定义 Launcher 至少需要声明 Widget Host 权限:
xml
<uses-permission android:name="android.permission.BIND_APPWIDGET" />
如果这个应用本身就是桌面启动器,入口 Activity 通常还要声明 HOME:
xml
<activity
android:name=".LauncherActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
注意:声明 BIND_APPWIDGET 不代表普通 APK 已经拥有永久绑定权限。普通应用第一次绑定第三方 Widget 时,仍然要经过系统确认;系统级/特权 Launcher 才可能直接完成绑定。
2. 桌面只需要一个承载容器
示例使用 FrameLayout,真实 Launcher 可以替换成自己的分页桌面、CellLayout 或拖拽容器:
xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/desktop"
android:layout_width="match_parent"
android:layout_height="match_parent" />
3. 核心代码:查找并加载第三方 Widget
下面只保留 Android 组件入口和必要回调,没有额外封装 WidgetManager、WidgetBinder、addWidget() 等类或辅助函数。生命周期回调是 Android 框架要求的入口,不能省略。
kotlin
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Intent
import android.os.Bundle
import android.os.Process
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
class LauncherActivity : AppCompatActivity() {
private val appWidgetHost = AppWidgetHost(this, 1024)
private val appWidgetManager = AppWidgetManager.getInstance(this)
private var pendingAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_launcher)
// 实际项目这里应该打开自己的 Widget 选择器。
// 这里用包名演示如何选择某个第三方 App 的 Widget。
val providerInfo = appWidgetManager.getInstalledProvidersForProfile(Process.myUserHandle())
.firstOrNull {
it.provider.packageName == "com.example.weather"
}
?: return
val appWidgetId = appWidgetHost.allocateAppWidgetId()
pendingAppWidgetId = appWidgetId
val options = Bundle().apply {
putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN)
}
val bindAllowed = appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, providerInfo.profile, providerInfo.provider, options)
if (bindAllowed) {
val info = appWidgetManager.getAppWidgetInfo(appWidgetId)
if (info == null) {
appWidgetHost.deleteAppWidgetId(appWidgetId)
return
}
if (info.configure == null) {
val hostView = appWidgetHost.createView(this, appWidgetId, info)
findViewById<FrameLayout>(R.id.desktop).addView(hostView)
} else {
val configureIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
configureIntent.component = info.configure
configureIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
startActivityForResult(configureIntent, 104)
}
} else {
// 普通 APK 通常会走到这里,系统会弹出允许绑定 Widget 的确认框。
val bindIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
bindIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
bindIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, providerInfo.provider)
bindIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, providerInfo.profile)
bindIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options)
startActivityForResult(bindIntent, 103)
}
}
override fun onStart() {
super.onStart()
// 开始接收系统发给 Host 的 Widget 更新。
appWidgetHost.startListening()
}
override fun onStop() {
appWidgetHost.stopListening()
super.onStop()
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 103) {
val appWidgetId = data?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, pendingAppWidgetId) ?: pendingAppWidgetId
if (resultCode != RESULT_OK ||
appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID
) {
if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
appWidgetHost.deleteAppWidgetId(appWidgetId)
}
return
}
val info = appWidgetManager.getAppWidgetInfo(appWidgetId)
if (info == null) {
appWidgetHost.deleteAppWidgetId(appWidgetId)
return
}
if (info.configure == null) {
val hostView = appWidgetHost.createView(this, appWidgetId, info)
findViewById<FrameLayout>(R.id.desktop).addView(hostView)
} else {
val configureIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
configureIntent.component = info.configure
configureIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
startActivityForResult(configureIntent, 104)
}
}
if (requestCode == 104) {
val appWidgetId = data?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, pendingAppWidgetId) ?: pendingAppWidgetId
if (resultCode != RESULT_OK ||
appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID
) {
if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
appWidgetHost.deleteAppWidgetId(appWidgetId)
}
return
}
val info = appWidgetManager.getAppWidgetInfo(appWidgetId)
if (info == null) {
appWidgetHost.deleteAppWidgetId(appWidgetId)
return
}
val hostView = appWidgetHost.createView(this, appWidgetId, info)
findViewById<FrameLayout>(R.id.desktop).addView(hostView)
}
}
}
4. 绑定流程中最容易漏掉的点
AppWidgetHost 的 Host ID 不能每次随机生成
kotlin
val appWidgetHost = AppWidgetHost(this, 1024)
1024 是 Host 在自己包内的稳定标识。不要在每次启动时随机生成,也不要给多个 Host 实例复用同一个 ID。
appWidgetId 是 Widget 实例的身份证
kotlin
val appWidgetId = appWidgetHost.allocateAppWidgetId()
这个 ID 需要和 Launcher 自己保存的桌面位置、尺寸、Provider 信息关联起来。实际项目不能像示例一样每次 onCreate() 都重新分配,否则 Launcher 每启动一次都会多出一个系统 Widget 实例。
Widget 被用户删除、绑定取消或配置失败时,要释放 ID:
kotlin
appWidgetHost.deleteAppWidgetId(appWidgetId)
bindAppWidgetIdIfAllowed() 返回 false 不是失败
它表示当前 Host 还没有绑定授权。此时要启动:
kotlin
Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
系统确认框返回 RESULT_OK 后,才算真正完成 Provider 绑定;返回取消时必须删除刚刚分配的 appWidgetId。
Provider 有配置页时,不能直接显示
通过 AppWidgetProviderInfo.configure 判断是否存在配置 Activity:
kotlin
if (info.configure != null) {
// 先启动 info.configure,成功后再 createView()
}
很多天气、日历、邮箱 Widget 都需要先选择账号、城市或数据源。配置页没有返回 RESULT_OK 时,不要把半成品 Widget 加到桌面上。
5. 示例代码和真正 Launcher 的差别
仓库示例中用 getInstalledProvidersForProfile(...)[2] 直接取第 3 个 Provider,这只是为了快速演示,不能放进真正的 Launcher:
kotlin
val appWidgetProviderInfo = appWidgetProviderInfoList[2]
正式实现应该:
kotlin
val providers = appWidgetManager.getInstalledProvidersForProfile(Process.myUserHandle())
// 把 providers 转成自己的 Widget 选择页面,用户点选后再走绑定流程。
另外,真正的 Launcher 还要处理以下状态:
text
Provider 列表变化 → 刷新 Widget 选择器
Launcher 进程重启 → 从本地记录恢复 appWidgetId 和桌面位置
Widget 被删除 → deleteAppWidgetId()
Provider 卸载 → 移除对应 HostView 和桌面记录
桌面尺寸变化 → updateAppWidgetSize()/updateAppWidgetOptions()
工作资料夹/多用户 → 使用对应 UserHandle 查询和绑定
最终结论
自定义 Launcher 加载第三方 Widget 的关键不是"拿到第三方布局",而是完整走完系统规定的 Host 生命周期:
text
固定 Host ID
→ allocateAppWidgetId()
→ bindAppWidgetIdIfAllowed()
→ ACTION_APPWIDGET_BIND
→ ACTION_APPWIDGET_CONFIGURE
→ createView()
→ startListening()/stopListening()
→ deleteAppWidgetId()
其中 AppWidgetHostView 是 Launcher 和第三方 Widget 之间的桥。Launcher 负责位置、尺寸、生命周期和授权流程;第三方 App 负责 Provider 和 RemoteViews,双方不需要共享布局文件。