Android 自定义 Launcher 加载第三方 App Widget

自定义 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 提供的是 AppWidgetProviderRemoteViews,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 组件入口和必要回调,没有额外封装 WidgetManagerWidgetBinderaddWidget() 等类或辅助函数。生命周期回调是 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,双方不需要共享布局文件。

官方依据

相关推荐
律宏阔2 小时前
Android 9 开发板实现系统侧滑返回
android
2601_962293532 小时前
Appium+python自动化(十二)- Android UIAutomator终极定位凶器(超详解)
android·自动化测试·appium·定位·uiautomator
mmsx2 小时前
MapLibre 自定义比例尺:Web 墨卡托屏幕距离换算公式与实现
android·源码·地图·maplibre
可乐鸡翅yeah_3 小时前
HLS CORS 跨域问题完整排查实战,解决 M3U8 与 TS 分片跨域报错
android·ios·音视频·m3u8·音视频在线播放
邪修king4 小时前
Linux系统篇(二十三) 基础 IO:从“文件”到“文件描述符”,彻底理解重定向
android·java·linux
灵境(虚幻知音)4 小时前
效能评估指标体系构建:方法、流程与模板
android·数据库
光影少年4 小时前
React18 对RN 的影响
android·前端·react.js·ios·前端框架
杉氧4 小时前
丝滑的奥秘:Reanimated 3 动画引擎与手势处理(Gesture Handler)
android·前端·react native
我命由我123455 小时前
Compose Codelab 学习 - Jetpack Compose 中的状态
android·java·java-ee·android studio·android jetpack·android-studio·android runtime