注:此方案在部分機(jī)型存在不兼容現(xiàn)象,具體表現(xiàn)不一致侨艾,可參考文章評(píng)論的反饋执虹。如果想投入生產(chǎn),請(qǐng)務(wù)必先評(píng)估出現(xiàn)的風(fēng)險(xiǎn)點(diǎn)能不能接受唠梨。
前言
也許你也注意到了袋励,在臨近雙11之際,手機(jī)上電商類(lèi)APP的應(yīng)用圖標(biāo)已經(jīng)悄無(wú)聲息換成了雙11專(zhuān)屬圖標(biāo)当叭,比如某寶和某東:
可能你會(huì)說(shuō)茬故,這有什么奇怪的,應(yīng)用市場(chǎng)開(kāi)啟自動(dòng)更新不就可以了么蚁鳖?
真的是這樣嗎?
為此徙垫,我特意查看了我手機(jī)上的某寶APP的當(dāng)前版本哨查,并對(duì)比了歷史版本上的圖標(biāo),發(fā)現(xiàn)并不對(duì)應(yīng)。
默認(rèn)是88會(huì)員節(jié)專(zhuān)屬圖標(biāo)叠国,而現(xiàn)在顯示的是雙11圖標(biāo)项棠。
那么浑测,作為開(kāi)發(fā)者的嗅覺(jué)岖圈,讓你自然而然想要從技術(shù)角度揣測(cè)是怎么實(shí)現(xiàn)的,而這便是這篇文章想要與你分享的。
知識(shí)儲(chǔ)備
<activity-alias>
某一個(gè)Activity 的別名可都,用于實(shí)例化該目標(biāo)Activity签杈。目標(biāo)必須與別名在同一應(yīng)用中谚咬,并且在清單中必須在別名之前進(jìn)行聲明互捌。
介紹下幾個(gè)重要的屬性:
android:enabled:必須設(shè)為“true”腌巾,系統(tǒng)才能通過(guò)別名實(shí)例化目標(biāo) Activity
android:icon:通過(guò)別名呈現(xiàn)給用戶時(shí)目標(biāo) Activity 的圖標(biāo)灯荧。
android:name:別名的唯一名稱(chēng)厉斟。與目標(biāo) Activity 的名稱(chēng)不同号涯,別名名稱(chēng)是任意的,它不引用實(shí)際類(lèi)。
android:targetActivity:可通過(guò)別名激活的 Activity 的名稱(chēng)。
PackageManager#setComponentEnabledSetting
可以利用 PackageManager 在清單文件中所定義的任何組件上切換啟用狀態(tài)奔穿,包括您想啟用或停用的任何一個(gè)Activity。
有了以上知識(shí)儲(chǔ)備后耗拓,下面就該剖析一下這個(gè)需求的具體場(chǎng)景了。
場(chǎng)景剖析
以電商類(lèi)APP雙11活動(dòng)為例旅赢,在雙11活動(dòng)開(kāi)始前的某個(gè)時(shí)間點(diǎn)(比如10天前)就要開(kāi)始對(duì)活動(dòng)的預(yù)熱,此時(shí)就要實(shí)現(xiàn)圖標(biāo)的自動(dòng)更換梗脾,而在活動(dòng)結(jié)束之后自赔,也必須要能更換回正常圖標(biāo)灾测,并且要求過(guò)程盡量對(duì)用戶無(wú)感知,更不能影響用戶對(duì)APP的正常使用望门。
具體拆分成要實(shí)現(xiàn)的功能點(diǎn)便是:圖標(biāo)更換丽惶、自動(dòng)操作、用戶無(wú)感知诅福。
方案實(shí)現(xiàn)
1.圖標(biāo)更換:禁用Launcher組件浅役,啟用Alias組件尝艘,并將targetActivity指向原先的Launcher組件闽颇。
2.自動(dòng)操作:指定日期轉(zhuǎn)換為時(shí)間戳怠褐,并與當(dāng)前時(shí)間戳對(duì)比,超過(guò)預(yù)設(shè)時(shí)間則執(zhí)行替換操作汰规。
3.用戶無(wú)感知:盡量選擇APP不活躍的階段的忿族,比如切換應(yīng)用/回到桌面時(shí)。
代碼實(shí)踐
首先豆励,我們需要在AndroidManifest清單文件中添加<activity-alias>元素丽旅,默認(rèn)為禁用狀態(tài),name屬性作為我們找到此組件的唯一標(biāo)志靶累,而icon屬性即是我們要替換的圖標(biāo)資源,并通過(guò)targetActivity屬性將作為L(zhǎng)ANCHUER的SplashActivity作為實(shí)例化的目標(biāo) Activity:
<activity android:name=".SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!--88會(huì)員節(jié)專(zhuān)屬Activity別名-->
<activity-alias
android:name=".SplashAliasActivity"
android:enabled="false"
android:icon="@mipmap/ic_launcher_88"
android:targetActivity=".SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<!--雙11專(zhuān)屬Activity別名-->
<activity-alias
android:name=".SplashAlias2Activity"
android:enabled="false"
android:icon="@mipmap/ic_launcher_11_11"
android:targetActivity=".SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
隨后多柑,我們圖標(biāo)替換的工作視作一項(xiàng)任務(wù)诫隅,定義一個(gè)數(shù)據(jù)類(lèi):
/**
* 切換圖標(biāo)任務(wù)
*/
data class SwitchIconTask (val launcherComponentClassName: String, // 啟動(dòng)器組件類(lèi)名
val aliasComponentClassName: String, // 別名組件類(lèi)名
val presetTime: Long, // 預(yù)設(shè)時(shí)間
val outDateTime: Long) // 過(guò)期時(shí)間
定義一個(gè)LauncherIconManager單例漫贞,負(fù)責(zé)圖標(biāo)更換相關(guān)的工作豪嗽。開(kāi)放添加圖標(biāo)切換任務(wù)的接口躁倒,做好參數(shù)合法性的校驗(yàn):
/**
* 啟動(dòng)器圖標(biāo)管理器
*/
object LauncherIconManager {
/** 切換圖標(biāo)任務(wù)Map */
private val taskMap: LinkedHashMap<String, SwitchIconTask> = LinkedHashMap()
/**
* 添加圖標(biāo)切換任務(wù)
* @param newTasks 新任務(wù)完丽,可以傳多個(gè)
*/
fun addNewTask(vararg newTasks: SwitchIconTask) {
for (newTask in newTasks) {
// 防止重復(fù)添加任務(wù)
if (taskMap.containsKey(newTask.aliasComponentClassName)) return
// 校驗(yàn)任務(wù)的預(yù)設(shè)時(shí)間和過(guò)期時(shí)間
for (queuedTask in taskMap.values) {
if (newTask.presetTime > newTask.outDateTime) throw IllegalArgumentException("非法的任務(wù)預(yù)設(shè)時(shí)間${newTask.presetTime}, 不能晚于過(guò)期時(shí)間")
if (newTask.presetTime <= queuedTask.outDateTime) throw IllegalArgumentException("非法的任務(wù)預(yù)設(shè)時(shí)間${newTask.presetTime}, 不能早于已添加任務(wù)的過(guò)期時(shí)間")
}
taskMap[newTask.aliasComponentClassName] = newTask
}
}
...
}
LauncherIconManager.addNewTask(
SwitchIconTask(
SplashActivity::class.java.name,
"$packageName.SplashAliasActivity",
format.parse("2020-08-02").time,
format.parse("2020-08-09").time
),
SwitchIconTask(
SplashActivity::class.java.name,
"$packageName.SplashAlias2Activity",
format.parse("2020-11-05").time,
format.parse("2020-11-12").time
)
)
通過(guò)Application#registerActivityLifecycleCallbacks方法注冊(cè)了對(duì)應(yīng)用內(nèi)Activity生命周期的監(jiān)聽(tīng)站楚,通過(guò)是否有活躍狀態(tài)的Activity判斷應(yīng)用是否進(jìn)入了后臺(tái):
/**
* 應(yīng)用運(yùn)行狀態(tài)注冊(cè)器
*/
object RunningStateRegister {
fun register(application: Application, callback: StateCallback) {
application.registerActivityLifecycleCallbacks(object : SimpleActivityLifecycleCallbacks() {
private var startedActivityCount = 0
override fun onActivityStarted(activity: Activity) {
if (startedActivityCount == 0) {
callback.onForeground()
}
startedActivityCount++
}
override fun onActivityStopped(activity: Activity) {
startedActivityCount--
if (startedActivityCount == 0) {
callback.onBackground()
}
}
})
}
}
class BaseApplication : Application() {
override fun onCreate() {
super.onCreate()
LauncherIconManager.register(this)
}
}
判斷應(yīng)用進(jìn)入后臺(tái)后,就可以開(kāi)始對(duì)圖標(biāo)的更換工作了:
/**
* 啟動(dòng)器圖標(biāo)管理器
*/
object LauncherIconManager {
...
/**
* 注冊(cè)以監(jiān)聽(tīng)?wèi)?yīng)用運(yùn)行狀態(tài)
*/
fun register(application: Application) {
RunningStateRegister.register(application, object: RunningStateRegister.StateCallback{
override fun onForeground() {
}
override fun onBackground() {
proofreadingInOrder(application)
}
})
}
/**
* 依次校對(duì)預(yù)設(shè)時(shí)間
* @param context 上下文
*/
fun proofreadingInOrder(context: Context) {
for (task in taskMap.values) {
if (proofreading(context, task)) break
}
}
/**
* 校對(duì)預(yù)設(shè)時(shí)間/過(guò)期時(shí)間
* @param context 上下文
* @return true 已過(guò)預(yù)設(shè)時(shí)間 false 未達(dá)預(yù)設(shè)時(shí)間或已過(guò)期
*/
private fun proofreading(context: Context, task: SwitchIconTask) =
when {
isPassedOutDateTime(task) -> {
disableComponent(context, ActivityUtil.getLauncherActivityName(context)!!)
enableComponent(context, task.launcherComponentClassName)
false
}
isPassedPresetTime(task) -> {
disableComponent(context, ActivityUtil.getLauncherActivityName(context)!!)
enableComponent(context, task.aliasComponentClassName)
true
}
else -> false
}
/**
* 是否已超過(guò)預(yù)設(shè)時(shí)間
* @param task 任務(wù)
*/
private fun isPassedPresetTime(task: SwitchIconTask) =
System.currentTimeMillis() > task.presetTime
/**
* 是否已超過(guò)過(guò)期時(shí)間
* @param task 任務(wù)
*
*/
private fun isPassedOutDateTime(task: SwitchIconTask) =
System.currentTimeMillis() > task.outDateTime
...
}
以上代碼均已上傳到GitHub搏嗡。核心的類(lèi)都封裝到Library模塊了窿春,并提供Demo模塊演示如何使用。
如果覺(jué)得項(xiàng)目不錯(cuò)的話點(diǎn)個(gè)Star吧~
https://github.com/madchan/LauncherIconLib
效果預(yù)覽
總結(jié)
通過(guò)以上構(gòu)建的方案采盒,便可讓我們的APP在預(yù)設(shè)的時(shí)間點(diǎn)實(shí)現(xiàn)對(duì)應(yīng)用圖標(biāo)的自動(dòng)替換旧乞,缺點(diǎn)是只能加載隨APK打包的圖片資源,適用于運(yùn)營(yíng)活動(dòng)時(shí)間相對(duì)固定的的場(chǎng)景磅氨。
參考文章
<activity-alias>
https://developer.android.google.cn/guide/topics/manifest/activity-alias-element