1、背景
App能根据特定活动,动态切换应用桌面图标达到宣传目的,例如淘宝双十一,国庆节等等。那么怎样才能在不发新版本的情况下,动态切换应用图标呢?
2、方案
注意⚠️:
- Android 系统不允许应用程序直接更改已安装应用的图标。每个应用的图标是由该应用自身的代码和资源定义的,只有应用的开发者可以通过【更新应用】来更改图标
- 动态替换icon,只能替换本地内置的icon,无法从服务器端获取来更新icon;必须把相关资源提前放入到 App 中,无法通过网络下载的方式再去替换应用 icon。
1.图标更换:在AndroidManifest设置应用入口Activity的别名,然后通过setComponentEnabledSetting动态启用或禁用别名进行图标切换
2.控制图标显示:冷启动App时,调用接口判断是否需要切换icon
3.触发时机:监听App前后台切换,当App处于后台时切换图标,使得用户无感知。
三、关键代码
ini
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--
/**
* @Author : zhh227
* date : 2025/01/05
*/
-->
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat">
<activity android:name=".ExchangeIconActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity-alias
android:name=".MainActivity1"
android:enabled="false"
android:icon="@mipmap/icon_1"
android:label="@string/app_name"
android:targetActivity=".ExchangeIconActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivity2"
android:enabled="false"
android:icon="@mipmap/icon_2"
android:label="@string/app_name"
android:targetActivity=".ExchangeIconActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
</application>
</manifest>
kotlin
class ExchangeIconActivity: AppCompatActivity() {
var changeTo: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_icon)
val tvInfo = findViewById(R.id.tv_info) as? TextView
try {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
tvInfo?.text = """
${"Name:" + componentName.className}
Version:${packageInfo.versionName}
""".trimIndent()
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
fun changeToIcon1(v: View?) {
changeTo = javaClass.name + "1"
changeLauncher(changeTo!!)
}
fun changeToIcon2(v: View?) {
changeTo = javaClass.name + "2"
changeLauncher(changeTo!!)
}
fun reset(v: View?) {
changeTo = javaClass.name
changeLauncher(changeTo!!)
}
override fun onDestroy() {
if (changeTo != null) {
changeLauncher(changeTo!!)
}
super.onDestroy()
}
private fun changeLauncher(name: String) {
val pm = packageManager
//隐藏之前显示的桌面组件
pm.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
//显示新的桌面组件
pm.setComponentEnabledSetting(
ComponentName(this@ExchangeIconActivity, name),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
)
}
}
4、适用范围
1、该功能支持Android 8.9.20及以上的版本
2、Android部分机型有兼容性问题,暂不支持使用该功能
3、部分样式有新版本限制,不适配设置为默认样式
5、可能的坑
1、动态替换icon,只能替换本地内置的icon,无法从服务器端获取来更新icon;(网易云音乐应该也是)
2、动态替换icon以后,应用内更新的时候必须要切换到原始icon),否则可能导致更新安装失败(崩溃)(AS上表现为adb运行会失败),或者升级后应用图标出现多个甚至应用图标都不显示的情况(可以通过下面【5】规则解决掉,所以这是一个坑点,不是肯定会发生的问题。);
3、Android系统动态替换app icon会有延迟,在不同的手机系统上刷新icon的时间不一样,大概在10秒左右,在这个时间内点击icon会提示应用未安装(提示可能会有差别,有些厂商或者机型点了没有反应);
4、更换icon的代码运行后一会应用就闪退了,或者导致显示中的Dialog和PopupWindow报错崩溃(这个问题和第二个问题有很大的相关性,按下面【5】给出的规则实行的话是可以解决的。
5、本地编译代码进行覆盖安装或者升级包会:
vbnet
Error while executing: am start -n "com.heng.changeiconapp/com.heng.changeiconapp.MainActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xxxx.changeiconapp/.MainActivity }
Error type 3
Error: Activity class {com.heng.changeiconapp/com.xxxx.changeiconapp.MainActivity} does not exist.
Error while Launching activity
Failed to launch an application on all devices
要Clean Project
。接着再选择 Rebuild Project
,在设备上卸载旧版本的应用,然后重新安装新版本。
apk覆盖安装或者升级包无影响。
6、防坑专用
- 关于 Activity 的 android:enabled 属性
不建议在代码中动态设置 Activity 的 android:enabled 属性。这种做法可能在图标切换过程中导致应用闪退,经过测试发现,小米、华为以及官方模拟器均存在此问题。
- 清单文件中的 android:enabled 设置
在清单文件中将 Activity 的 android:enabled 属性设置为 false,并在后续版本中保持此值不变。如果在后续版本中将其设置为 true,可能会导致应用升级后出现多个图标的情况。
- 设置默认的 Activity-alias
为应用配置一个默认的 Activity-alias,以确保图标的唯一显示。此设置可替代第一点中提到的将 Activity 的 android:enabled 设置为 false 可能导致桌面上无应用图标的问题。
- Activity-alias 的动态管理
对于 android:enabled="true" 的默认显示项,尽量避免在中途进行更改。如果确实需要更新默认值,应通过代码进行动态管理。
- 避免多个 Activity-alias
不要为 android:enabled="true" 的 Activity-alias 设置多个实例。若通过代码尝试隐藏其中一个或多个,可能会导致图标消失,此问题在第二点中已有提及。
- 新版本中的 Activity-alias 设置
在后续版本中新增的 Activity-alias 必须设置 android:enabled="false",并在清单文件中保持此值,然后通过代码动态更新。
- 保留旧版本的 Activity-alias
新版本的 Activity-alias 必须包含所有旧版本的 Activity-alias,以防止在覆盖安装后出现应用图标消失或者崩溃的情况。
- 关于 enabled=false 的 Activity 的限制
设置为 enabled=false 的 Activity 无法通过显式 Intent 进行调用,这将导致错误。例如,如果在应用中推送服务发送了打开 SplashActivity 的通知,而该 SplashActivity 设置为 enabled=false,则无法打开,并会出现错误日志。为避免此问题,建议将启动入口的 Activity 单独定义,确保其仅作为启动入口使用,避免被其他地方调用。