Android动态更换桌面图标
一、需求简介
实现 - 动态更改应用程序图标,且支持切换回常规图标。本文将详细介绍运行时如何更改 Android 应用程序图标的流程。
二、解决方案
应用程序图标是从清单文件设置的,Android系统读取manifest文件并相应地设置应用程序图标。解决方法:就是使用一个activity-alias(如果你对activity-alias不熟悉,可以查看_官方文档)。
三、方案实现
实现步骤:
- 增加对应个数的标签
- 增加代码控制事件
- 代码控制显示哪个图标
接下来就一步一步的来实现:
1、增加对应个数的activity-alias标签
如果有两个图标,我们就增加两个activity-alias标签,这个标签是在AndroidManifest.xml的标签内的,和标签同一级,其中一个代码如下:
<activity-alias
android:name="com.wj.test.DefaultIconActivity"
android:enabled="false"
android:icon="@mipmap/icon_1"
android:label="@string/app_name"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name="com.wj.test.GuoQinIconActivity"
android:enabled="false"
android:icon="@mipmap/icon_2"
android:label="@string/app_name"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
这里需要注意一下它的这几个属性:
属性 | 含义 |
---|---|
name | 可任意取值,只要能保证是唯一标识即可,为了方便管理建议有规律一些 |
targetActivity | 这个属性的值就是代表指向的是哪个Activity,而这个标签本身代表是该Activity的别名,记得指向的Activity要在该标签之前申明,否则可能运行不起来 |
icon | 指的是该别名对应的应用图标 |
label | 指的是该别名对应的应用名字 |
enabled | 默认是true,true就会显示在桌面上,这里为了保证桌面只显示一个图标,则中的属性都是false,而在之后代码中动态控制这个属性,来显示和隐藏对应的图标 |
至于,这个和Activity的没有区别,其实完全可以把当作Activity组件来看,只是不是真身,是别名罢了。
2、增加操作事件
通过点击或业务控制图标的切换执行逻辑。
3、代码控制显示哪个图标
这一步其实也就是调用PackageManager中的一个方法即可,方法如下:
private void changeLauncher(String name) {
PackageManager pm = getPackageManager();
//隐藏之前显示的桌面组件
pm.setComponentEnabledSetting(getComponentName(),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
//显示新的桌面组件
pm.setComponentEnabledSetting(new ComponentName(MainActivity.this, name),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}
都是调用PackageManager的setComponentEnabledSetting方法,第一个参数表示操作的组件是哪个,第二个参数表示显示还是隐藏,第三个组件表示是否关掉app。
四、注意事项
1、这里有个技巧,建议不要直接去执行切换图标,因为执行切换图标之后始终会关闭这次打开的app,所以我们 可以先记录下要换成哪个图标,在程序退出的时候再切换图标,这样一来就不会关闭该app了。
2、细心的朋友会发现,在调试阶段,我改了这个app的启动图标,再执行代码启动,发现启动不了,其实这是因为代码中默认启动那个组件和修改后的那个组件不一致了,所以就启动不了,而对于程序的更新和安装是没有影响的。
3、在执行动态更换图标操作之后,不是马上生效的,和手机性能有关,会在几秒钟之内改变图标,但是对于普通桌面图标的改变,该缺点还是可以接受的,毕竟不是用户手动触发,也不影响体验。
五、业务工具
object AppIconUpdateUtil {
private const val TAG = "AppIconUpdateUtil"
private const val TIME_M: Long = 3
private const val DEF_BACK_DURATION: Long = TIME_M * 1000 // 后台停留时长
/********* 后台配置的资源ID *********/
private const val ID_ICON_DEFAULT = "app_icon_default"
private const val ID_ICON_GUO_QIN = "app_icon_guo_qing"
/********* 本地配置的图标 *********/
private const val DEFAULT_ICON_CLASS = "com.wj.test.DefaultIconActivity" // 默认图标
private const val GUO_QIN_ICON_CLASS = "com.wj.test.GuoQinIconActivity" // 国庆图标
// 最近进入后台的时间记录
private var latestBackTime: Long = 0
fun checkAppIconUpdate(front: Boolean) {
synchronized(this) {
scope.launch(Dispatchers.IO) {
if (front) {
latestBackTime = 0
Log.i(TAG, "HomeActivity进入前台: 清除后台开始时间 latestBackTime = 0 !")
return@launch
} else {
latestBackTime = System.currentTimeMillis()
}
delay(DEF_BACK_DURATION)
val curBackDuration = System.currentTimeMillis() - latestBackTime
if (latestBackTime > 0 && curBackDuration >= DEF_BACK_DURATION) {
Log.i(TAG, "HomeActivity后台停留达到${TIME_M}秒: 应用图标检测更新!")
checkAppIconUpdate()
} else {
Log.i(TAG, "HomeActivity后台停留不足${TIME_M}秒:应用图标不检测!")
}
}
}
}
private fun checkAppIconUpdate() {
synchronized(this) {
scope.launch(Dispatchers.IO) {
runCatching {
// 从接口请求后保存,获取保存的icon信息
val iconCacheInfo = AppIconKvUtil.getAppIconInfo()
val curAppIconId = AppIconKvUtil.getCurAppIconId()
Log.i(
TAG,
"缓存的应用图标配置信息:${iconCacheInfo}, 当前的图标ID: $curAppIconId"
)
// 有缓存信息
val cacheMode = GsonUtils.fromJson(iconCacheInfo, AppIconConfigInfo::class.java)
if (cacheMode != null && !cacheMode.iconId.isNullOrEmpty()) {
val startTime = cacheMode.startTime ?: 0
val endTime = cacheMode.endTime ?: 0
val startTimeIsValid =
(startTime > 0 && startTime <= System.currentTimeMillis())
val endTimeIsValid = (endTime > 0 && endTime > System.currentTimeMillis())
Log.i(
TAG,
"应用图标开始时间:${TimeUtil.getDateTime3(startTime)} 结束时间: ${
TimeUtil.getDateTime3(endTime)
}"
)
if (startTimeIsValid && endTimeIsValid) {
Log.i(TAG, "配置的icon有效:去切换为配置图标 ${cacheMode.iconId}!")
changeToIcon(cacheMode.iconId)
return@launch
}
}
Log.i(TAG, "icon配置无效, 去复位默认图标!")
changeToIcon(ID_ICON_DEFAULT)
}.onFailure { e ->
Log.e(
TAG,
"checkAppIconUpdate 应用图标处理错误: ${Log.getStackTraceString(e)}"
)
}
}
}
}
private fun changeToIcon(iconId: String?) {
scope.launch(Dispatchers.Main) {
when (iconId) {
ID_ICON_GUO_QIN -> {
changeToNewIcon()
}
else -> {
reset()
}
}
}
}
private fun changeToNewIcon() {
kotlin.runCatching {
val curAppIconId = AppIconKvUtil.getCurAppIconId()
if (curAppIconId != ID_ICON_GUO_QIN) {
Log.i(TAG, "应用图标切换为: $ID_ICON_GUO_QIN")
val pm: PackageManager = getApplication().packageManager
//隐藏之前显示的桌面组件
pm.setComponentEnabledSetting(
ComponentName(getApplication(), DEFAULT_ICON_CLASS),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
//显示新的桌面组件
pm.setComponentEnabledSetting(
ComponentName(getApplication(), GUO_QIN_ICON_CLASS),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
)
AppIconKvUtil.saveCurAppIconId(ID_ICON_GUO_QIN)
} else {
Log.i(TAG, "当前应用图标ID:$curAppIconId, 不需要切换!")
}
}.onFailure { e ->
Log.e(TAG, "应用图标切换国庆错误: ${Log.getStackTraceString(e)}")
}
}
private fun reset() {
kotlin.runCatching {
val curAppIconId = AppIconKvUtil.getCurAppIconId()
if (curAppIconId.isNotEmpty() && curAppIconId != ID_ICON_DEFAULT) {
Log.i(TAG, "当前不是默认图标, 复位到图标Id: $ID_ICON_DEFAULT")
val pm = getApplication().packageManager
//隐藏之前显示的桌面组件
pm.setComponentEnabledSetting(
ComponentName(getApplication(), GUO_QIN_ICON_CLASS),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
//显示新的桌面组件
pm.setComponentEnabledSetting(
ComponentName(getApplication(), DEFAULT_ICON_CLASS),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
)
AppIconKvUtil.saveCurAppIconId(ID_ICON_DEFAULT)
} else {
Log.i(TAG, "当前是默认图标, 不需要切换!")
}
}.onFailure { e ->
Log.e(TAG, "复位应用图标切换错误: ${Log.getStackTraceString(e)}")
}
}
/**
* 获取最新的应用图标配置
*/
fun requestAppIconInfo() {
scope.launch(Dispatchers.IO) {
runCatching {
delay(100)
Log.i(TAG, "requestAppIconInfo() 执行最新的应用图标配置请求")
request(requestBlock = {
AppIconRepository().getLatestAppIconInfo(AppIconConfigReq())
}, callBack = {
onSuccess = { appIconConfigInfo ->
// 保存当前配置的icon
AppIconKvUtil.saveAppIconInfo(appIconConfigInfo.toJson())
}
})
}.onFailure { e ->
Log.e(
TAG,
"requestAppIconInfo() 执行最新应用图标请求出现错误: ${Log.getStackTraceString(e)}"
)
}
}
}
}