Android 快捷方式实战指南:静态、动态与固定快捷方式详解

前言

什么是 Android 快捷方式?

面对这个问题,你肯定会说:简单,和 Windows 一样,不就是桌面上的图标嘛。

嗯,很正确。它正是手机桌面上显示的一个个应用图标。不过,长按图标弹出的那些小气泡,也是快捷方式的一种,比如微信的"扫一扫"和"收付款"气泡。

总之,快捷方式允许我们在桌面直接快速地使用某个功能,而无需从应用主页一步步打开。把正常打开比作是走楼梯的话,那么快捷方式就是直达电梯。

从底层上来看,快捷方式本质上就是一个包含了特定 Intent 和元数据(如图标、标签)的入口(点击就会跳转到应用的特定页面),由系统 Launcher (启动器) 来管理和展示它。

它的作用是啥?

快捷方式主要有三大作用:

  1. 方便用户打开: 将之前需要三四步操作才能打开的页面,缩减为只需一步。
  2. 让用户看得到: 长按图标显示的快捷方式列表,可以提醒用户有哪些功能项。
  3. 让用户多使用: 固定快捷方式到桌面,可以提升用户打开的次数。

其实,我觉得最主要的作用就是第一点,毕竟提升了用户效率。

发展历程

在 Android 7.1 (API 25) 之前,也就是"广播时代"。快捷方式的创建是通过发送一个带有 com.android.launcher.action.INSTALL_SHORTCUT 的广播来完成的,这种方式需要对应权限,而且桌面无法进行管理,很难知道快捷方式失效的时间,导致经常出现点击应用图标没反应的情况。

在这之后,Google 引入了 ShortcutManager API。应用现在不能随意通过广播来创建快捷方式了,而是需要向系统注册快捷方式信息,Launcher 来负责读取和展示。这带来的最大好处就是:系统可以管理快捷方式,处理其失效的逻辑。

三种快捷方式的类型

这里只简要介绍一下三种快捷方式,详细的使用在后面。

  • 静态快捷方式

    也可以说成"硬编码"快捷方式,这类快捷方式通过在 AndroidManifest.xml 清单文件中声明。打包在 APK 中,随着应用的安装而存在。它适用于一些长期不会变动的核心功能,比如笔记应用的新建笔记功能。因为如果要变动,只能在应用下次更新时修改。

    静态快捷方式和动态快捷方式共享同一个数量上限,可以通过调用 ShortcutManagerCompat.getMaxShortcutCountPerActivity(context) 方法获取精确值。

  • 动态快捷方式

    这类快捷方式通过运行时代码来控制,主要是调用 ShortcutManager API 来创建、更新或删除快捷方式。

    因为是在运行时控制,所以它非常灵活。它的适用场景是动态内容(最近播放)和短期活动(双11大促入口)。如果达到上限还继续添加的话,系统会自动移除最不重要(rank 值最低)的那个。

  • 固定快捷方式

    用户可以手动将应用的静态或动态快捷方式固定到手机桌面上,这类快捷方式就是固定快捷方式,也是大众所认为的快捷方式。比如,我可以将钱包应用中的记账功能固定到桌面,方便我快速打开。

    它是用户手动创建的、由用户主导的快捷方式,我们无法静默创建,只能调用 API 弹出对话框,来向用户请求确认创建。

    注意:固定后,即使我们调用 API 删除了快捷方式,桌面的图标依然会存在,它只能由用户手动删除。

    它的数量没有上限。

为什么静态快捷方式和动态快捷方式共享上限?

原因很简单,因为这两类快捷方式占用的是同一个槽位,也就是长按应用的气泡位:

权限要求

创建动态或静态快捷方式不需要任何权限,因为这是应用内部的行为。

对于通过代码请求固定快捷方式,也不需要权限,只需在代码中调用 API 即可。

调用 ShortcutManagerCompat.isRequestPinShortcutSupported(context) 方法可以检查当前设备是否支持固定快捷方式功能。

创建快捷方式

静态定义

AndroidManifest.xml 文件中找到主 Activity,并在 <activity> 标签中添加 <meta-data> 引用。

xml 复制代码
<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <meta-data
        android:name="android.app.shortcuts"
        android:resource="@xml/shortcuts" />
</activity>

然后创建 res/xml/shortcuts.xml 文件来描述这个快捷方式:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <shortcut
        android:enabled="true"
        android:icon="@drawable/ic_launcher_foreground"
        android:shortcutId="static_shortcut"
        android:shortcutLongLabel="@string/short_label"
        android:shortcutShortLabel="@string/long_label"
        tools:targetApi="25">
        <intent
            android:action="android.intent.action.VIEW"
            android:targetClass="com.example.demo.MainActivity"
            android:targetPackage="com.example.demo" />
    </shortcut>
</shortcuts>

可以通过 android:shortcutDisabledMessage 属性来指定禁用时显示的提示信息。

上述引用的字符串:

xml 复制代码
<resources>
    <string name="short_label">短标签</string>
    <string name="long_label">长长长标签</string>
</resources>

关键属性含义:

  • shortcutId: 快捷方式的唯一标识,后续的更新删除操作需要依赖它。

  • shortcutShortLabel:必填的短标题,用于气泡名称的显示(空间有限时显示)。

  • shortcutLongLabel :长标题,用于固定后名称的显示(空间充足时显示)。

  • <intent>: 点击时执行的动作。

运行效果:

  1. 长按应用图标。

  1. 固定快捷方式

动态创建

addDynamicShortcuts() 新增

代码实现如下:

kotlin 复制代码
/**
 * 创建动态快捷方式
 */
fun dynamicShortcut() {
    val shortcutInfo = ShortcutInfoCompat.Builder(this@MainActivity, "dynamic_shortcut")
        .setShortLabel("动态快捷方式")
        .setLongLabel("这是一个动态快捷方式")
        .setIcon(
            IconCompat.createWithResource(
                this@MainActivity,
                R.drawable.ic_launcher_foreground
            )
        )
        .setIntent(Intent(Intent.ACTION_VIEW).apply {
            setClass(this@MainActivity, MainActivity::class.java)
        })
        .build()

    ShortcutManagerCompat.addDynamicShortcuts(
        this,
        listOf(shortcutInfo)
    )
}

运行结果:

  1. 长按应用图标。
  1. 固定该动态快捷方式。

我们当前的做法,点击后确实能打开目标页面,但用户按下返回键,就会直接退出应用,造成用户体验不佳。所以,如果目标页面是一个经过多步操作才能打开的页面,应该调用 Builder.setIntents(Intent[]) 方法,把目标页面放到 Intent 数组的最后一个位置,前面的则作为回退栈。

代码示例:

kotlin 复制代码
// 构建 Intent 回退栈
// 主页面作为栈底
val backIntent = Intent(Intent.ACTION_MAIN).apply {
    setClass(this@MainActivity, MainActivity::class.java)
    // 如果是从桌面快捷方式进入,通常需要开启新任务栈
    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}

// 目标页面作为栈顶
val targetIntent = Intent(Intent.ACTION_VIEW).apply {
    setClass(this@MainActivity, DetailActivity::class.java)
}

val shortcutInfo = ShortcutInfoCompat.Builder(this@MainActivity, "back_stack_shortcut")
    .setShortLabel("回退栈示例")
    .setIntents(arrayOf(backIntent, targetIntent))
    .build()

这样配置以后,用户点击快捷方式进入 DetailActivity 页面,按下返回键,会回到 MainActivity,而不是直接退出应用。

setDynamicShortcuts() 完全覆盖

addDynamicShortcuts() 是往之前的动态快捷列表中添加,而 setDynamicShortcuts() 是完全覆盖该列表。

具体的演示代码就不赘述了,就是先添加两个动态快捷列表,然后再覆盖动态快捷列表。不过有一点需要注意,快捷方式的 shortcutId 必须唯一。

requestPinShortcut() 固定快捷方式

这类快捷方式的创建主要是需要用户参与交互。

代码如下:

kotlin 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Button(
                    onClick = {
                        createPinShortcuts()
                    }
                ) {
                    Text(text = "固定快捷方式到桌面")
                }
            }
        }
    }

    /**
     * 创建固定快捷方式
     */
    private fun createPinShortcuts() {
        // 检查是否支持固定快捷方式
        val isSupported = ShortcutManagerCompat.isRequestPinShortcutSupported(this@MainActivity)
        if (!isSupported) {
            Toast.makeText(this@MainActivity, "当前启动器不支持固定快捷方式", Toast.LENGTH_SHORT)
                .show()
            return
        }

        val packageName = this.packageName
        val className = MainActivity::class.java.name

        val currentTime = System.currentTimeMillis()
        val shortcutId = "my_shortcut_id_${currentTime}"

        val pinShortcutInfo = ShortcutInfoCompat.Builder(this@MainActivity, shortcutId)
            .setShortLabel("固定功能")
            .setLongLabel("固定功能长标签")
            .setIcon(IconCompat.createWithResource(this@MainActivity, R.mipmap.ic_launcher))
            .setIntent(Intent(Intent.ACTION_MAIN).apply {
                component = ComponentName(packageName, className) // 明确指定 Intent 的组件
                // 创建一个全新的任务栈,多用于从桌面快捷方式启动
                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
            })
            .build()

        // 可选:验证 Intent 是否有效
        val intentActivities =
            packageManager.queryIntentActivities(pinShortcutInfo.intent, 0)
        if (intentActivities.isEmpty()) {
            Toast.makeText(
                this@MainActivity,
                "无效的 Intent,没有找到可以处理此 Intent 的 Activity",
                Toast.LENGTH_SHORT
            ).show()
            return
        }

        // 弹出确认对话框尝试创建快捷方式并记录结果
        ShortcutManagerCompat.requestPinShortcut(
            this@MainActivity,
            pinShortcutInfo,
            null // 此次固定快捷方式的结果回调,通常不进行监听,因为无需知道用户是否确认创建
        ).also { result ->
            if (result) {
                Toast.makeText(
                    this@MainActivity,
                    "创建快捷方式的请求已成功发送",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }
    }
}

核心代码是 ShortcutManagerCompat.requestPinShortcut() 这一行。

运行结果:
#

更新与删除快捷方式

更新

静态快捷方式

前面我们说过了,静态快捷方式是"死"的。所以更新它的唯一方式是发布新版本 APK 时,修改 res/xml/shortcuts.xml 文件。

动态快捷方式

核心的方法为 ShortcutManagerCompat.updateShortcuts(),每次更新都是通过 shortcutId 进行匹配的。如果 id 不存在,系统会静默忽略。

主要的可更新内容就是图标、标题以及 Intent,不能修改 id

代码如下:

kotlin 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Column {
                    Button(
                        onClick = {
                            createDynamicShortcuts()
                        }
                    ) {
                        Text(text = "创建快捷方式")
                    }

                    Button(
                        onClick = {
                            updateDynamicShortcuts()
                        }
                    ) {
                        Text(text = "更新快捷方式")
                    }
                }
            }
        }
    }

    /**
     * 创建动态快捷方式
     */
    private fun createDynamicShortcuts() {
        val shortcutInfo = ShortcutInfoCompat.Builder(this@MainActivity, "dynamic_shortcut")
            .setShortLabel("动态快捷方式")
            .setIcon(
                IconCompat.createWithResource(
                    this@MainActivity,
                    R.mipmap.ic_launcher
                )
            )
            .setIntent(
                Intent(Intent.ACTION_MAIN).apply {
                    setClass(this@MainActivity, MainActivity::class.java)
                }
            )
            .build()

        ShortcutManagerCompat.addDynamicShortcuts(
            this,
            listOf(shortcutInfo)
        )
    }

    /**
     * 更新动态快捷方式
     */
    private fun updateDynamicShortcuts() {
        val updateShortcutInfo = ShortcutInfoCompat.Builder(this@MainActivity, "dynamic_shortcut")
            .setShortLabel("动态快捷方式(更新后)")
            .setIcon(
                IconCompat.createWithResource(
                    this@MainActivity,
                    R.mipmap.ic_launcher
                )
            )
            .setIntent(
                Intent(Intent.ACTION_MAIN).apply {
                    setClass(this@MainActivity, MainActivity::class.java)
                }
            )
            .build()

        // 根据快捷方式id更新
        ShortcutManagerCompat.updateShortcuts(this@MainActivity, listOf(updateShortcutInfo))
    }
}

运行结果:

固定快捷方式

固定快捷方式的更新无需我们关心,因为它关联的是已有的快捷方式。如果关联的是动态快捷方式,那么当动态快捷方式更新后,桌面上的图标会自动同步更新。

删除

首先,我得告诉你:已固定的快捷方式我们是无法通过代码删除的,只能由用户手动移除。这涉及到了 Android 系统的隐私设计,主要是为了用户的安全着想。

其次我们来说说具体怎么删除,静态定义的快捷方式不要我多说了吧,只能在更新时修改。好,我们接着说如何删除动态快捷方式。

动态快捷方式

涉及到的 API 主要有两个:removeDynamicShortcuts(List<String> ids)removeAllDynamicShortcuts()

以前者为例,代码如下:

kotlin 复制代码
class MainActivity : ComponentActivity() {

    private val shortcutIds = mutableListOf<String>()

    companion object {
        private const val PREFS_NAME = "shortcut_prefs"
        private const val KEY_SHORTCUT_IDS = "shortcut_ids"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        loadShortcutIds()
        enableEdgeToEdge()
        setContent {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Column {
                    Button(
                        onClick = {
                            val currentTime = System.currentTimeMillis()
                            val shortcutId = "dynamic_shortcut_$currentTime"
                            Log.d("MainActivity", "创建动态快捷方式: $shortcutId")
                            createDynamicShortcuts(
                                "dynamic_shortcut_$currentTime",
                                "标签${shortcutIds.size + 1}"
                            )
                            if (!shortcutIds.contains(shortcutId)) {
                                shortcutIds.add(shortcutId)
                            }
                        }
                    ) {
                        Text(text = "创建动态快捷方式")
                    }

                    Button(
                        onClick = {
                            val lastShortcutId = shortcutIds.lastOrNull().also {
                                Log.d("MainActivity", "删除动态快捷方式: $it")
                            } ?: return@Button
                            deleteLastShortcut(lastShortcutId)
                            shortcutIds.removeLastOrNull()
                        }
                    ) {
                        Text(text = "删除上一次创建的动态快捷方式")
                    }
                }
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        saveShortcutIds()
    }

    /**
     * 保存 shortcutIds
     */
    private fun saveShortcutIds() {
        val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
        prefs.edit {
            putString(KEY_SHORTCUT_IDS, shortcutIds.joinToString(","))
        }
    }

    /**
     * 加载 shortcutIds
     */
    private fun loadShortcutIds() {
        val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
        val savedShortcuts = prefs.getString(KEY_SHORTCUT_IDS, "")
        val shortcuts = savedShortcuts?.split(",")?.filter {
            it.isNotEmpty()
        }
        shortcuts?.let {
            shortcutIds.clear()
            shortcutIds.addAll(it)
        }
    }

    /**
     * 创建动态快捷方式
     */
    private fun createDynamicShortcuts(shortcutId: String, shortLabel: String) {
        val shortcutInfo = ShortcutInfoCompat.Builder(this@MainActivity, shortcutId)
            .setShortLabel(shortLabel)
            .setIcon(
                IconCompat.createWithResource(
                    this@MainActivity,
                    R.mipmap.ic_launcher
                )
            )
            .setIntent(
                Intent(Intent.ACTION_MAIN).apply {
                    setClass(this@MainActivity, MainActivity::class.java)
                }
            )
            .build()

        ShortcutManagerCompat.addDynamicShortcuts(
            this@MainActivity,
            listOf(shortcutInfo)
        )
    }

    /**
     * 删除最后添加的动态快捷方式
     */
    private fun deleteLastShortcut(shortcutId: String) {
        // 可选的解释信息
        val message = "该功能已被移除"

        // 禁用固定的快捷方式
        ShortcutManagerCompat.disableShortcuts(this@MainActivity, listOf(shortcutId), message)

        ShortcutManagerCompat.removeDynamicShortcuts(
            this,
            listOf(shortcutId)
        )
    }
}

关键在于:在 Activity 销毁时,需要保存ID列表到本地;启动时,再进行恢复。

连续点击三次"创建动态快捷方式"按钮,将"标签三"快捷方式固定到桌面,再点击"删除上一次创建的动态快捷方式"按钮,运行结果:

在上述代码中,我们禁用了固定的动态快捷方式,这样该快捷方式会变灰,点击时不会跳转,而是会弹出提示。

与之相对的 API 有着 enableShortcuts(List<String> ids),作用是启用固定的快捷方式。

快捷方式的管理

获取快捷方式列表

如果你也觉得手动维护 shortcutIds 列表有点繁琐,其实啊,我们也可以通过 API 直接查询系统中已存在的快捷方式列表。

比如获取所有当前已发布的动态快捷方式:ShortcutManagerCompat.getDynamicShortcuts(context)

kotlin 复制代码
val existingIds = ShortcutManagerCompat.getDynamicShortcuts(this@MainActivity).map { it.id }

如果要获取其他类型的快捷方式,只需使用 ShortcutManagerCompat.getShortcuts(context, matchFlags)。以获取所有快捷方式为例,代码如下:

kotlin 复制代码
ShortcutManagerCompat.getShortcuts(
    this@MainActivity,
    ShortcutManagerCompat.FLAG_MATCH_MANIFEST
            or ShortcutManagerCompat.FLAG_MATCH_DYNAMIC
            or ShortcutManagerCompat.FLAG_MATCH_PINNED
).forEach {
    if (it.isDeclaredInManifest) {
        Log.d(
            "MainActivity",
            "静态快捷方式: ID: ${it.id}, 标签: ${it.shortLabel}, 描述: ${it.longLabel}..."
        )
    } else if (it.isDynamic) {
        Log.d(
            "MainActivity",
            "动态快捷方式: ID: ${it.id}, 标签: ${it.shortLabel}, 描述: ${it.longLabel}, 排序权重: ${it.rank}..."
        )

        if (it.isPinned) {
            Log.d(
                "MainActivity",
                "固定快捷方式: ID: ${it.id}, 标签: ${it.shortLabel}, 描述: ${it.longLabel}..."
            )
        }
    }

}

运行结果:

yaml 复制代码
静态快捷方式: ID: static_shortcut, 标签: 长长长标签, 描述: 短标签...
动态快捷方式: ID: dynamic_shortcut_1772588646775, 标签: 标签3, 描述: null, 排序权重: 0...
动态快捷方式: ID: dynamic_shortcut_1772588645445, 标签: 标签2, 描述: null, 排序权重: 1...
固定快捷方式: ID: dynamic_shortcut_1772588645445, 标签: 标签2, 描述: null...
动态快捷方式: ID: dynamic_shortcut_1772588644280, 标签: 标签1, 描述: null, 排序权重: 2...

假如你需要监听快捷方式的状态,你可以在 onResumeonStart 方法中,主动调用此方法来获取最新状态。

另外还有一个 FLAG_MATCH_CACHED 标志,加上会返回缓存的快捷方式。用户使用快捷方式后,启动器可能会缓存该快捷方式,即使该快捷方式已经被删除,缓存快捷方式还会保留一段时间。我们一般不会使用,它主要用于系统内部的优化。

最佳实践

快捷方式ID的设计

看完前面的内容,你应该了解到了快捷方式ID的重要性:更新或删除等操作都需要获取到快捷方式的ID。

对于其设计,我们应该避免使用 shortcut_id_1shortcut1 等ID。而是应该遵守语义化原则,ID对应其功能,比如 action_scan_qr 表示扫一扫。

另外,ID一旦发布后,就不要轻易修改。如果修改了,用户已固定的快捷方式就会失效。

兼容性 API

我们一直演示的 ShortcutManagerCompat,其实是 ShortcutManager 的兼容版本,对于不同的 Android 版本进行了适配,我们无需手动判断版本差异,非常方便。

实际开发中,也尽量选择 AndroidX 库下的兼容类。

隐私安全

快捷方式的本质是一个 Intent,我们应该尽量使用显示 Intent(既明确指定 Component),使用隐式 Intent 可能会被其他应用劫持。

敏感数据(如密码)不要存在 Intent 中,可能会被恶意应用解析。如果必须传递,需要在目标 Activity 中进行严格的权限校验。

测试

我们来看一些比较有用的 ADB 调试命令。

查看默认启动器

  • 命令:adb shell cmd shortcut get-default-launcher

    • 解析:其中 adb shell 的作用是进入 Android 设备底层 Linux 命令行环境,后续命令中的作用也是如此。cmd 是 Android 系统中的服务管理工具,使用它即可与系统服务进行交互。shortcut 是我们指定的系统服务 (ShortcutManager 对应的系统服务),指令参数 get-default-launcher 用于查询系统当前默认的启动器。

调用它的目的是确认当前手机使用的启动器,以定位显示异常问题。因为快捷方式的展示和管理是由启动器决定的,不同的启动器对于数量限制、排序规则有不同的实现。

输出示例:

bash 复制代码
Launcher: ComponentInfo{com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity}
Success

上述输出表示当前启动器的包名是 com.google.android.apps.nexuslauncher

打印快捷方式详细信息

  • 命令:adb shell cmd shortcut dump <包名>

    • 解析:dump 意为"转储"或"打印",将会要求系统将保存的关于快捷方式的数据都打印出来。

这个命令很关键,可以查看快捷方式的详细信息。

输出很长,关键在于 Shortcuts 快捷方式列表,以及每个 ShortcutInfo 的详细状态。

重置速率限制

  • 命令:adb shell cmd shortcut reset-throttling

    • 解析:reset-throttling 的意思是重置"节流"机制。

节流?节谁的流?

其实,为了防止恶意应用频繁调用快捷方式相关 API 导致系统发热或卡顿,Android 对快捷方式的操作频率进行了限制 (Throttling)。

而我们在调试阶段,可能会频繁调用而触发系统限制,导致系统拒绝我们的请求。我们可以执行这个命令来清除应用的频率限制计数器,让我们可以马上继续调试。

相关推荐
hqk1 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING2 小时前
RN容器启动优化实践
android·react native
恋猫de小郭5 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker10 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴10 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭20 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab21 小时前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos