Android:動態更換桌面ICON
前言
當老闆和產品提出這種需求的時候,我並不感到害怕,心裡甚至有點竊喜,因為大廠基本都有這種效果,那肯定也好實現。當我一查資料的時候,發現情況不容樂觀。
首先我嘗試著使用給我們的 activity 設定別名,也就是 activity-alias,但是我在網上看到好多人都說,這個有以下的坑,當然,我也驗證了,確實有以下坑:
- 在動態更換完ICON以後,可能會發生關閉APP,
- 在三星手機(可能還有其他的手機)上,更換ICON以後,ICON在桌面上的位置會發生變化。
- 更換ICON以後,在桌面上顯示還是原來的ICON,點選原來的ICON會出現 未安裝應用程式 提示,過個幾秒鐘才會更換ICON。
看到這些坑就覺得害怕,就在想大廠應該不會用這種方式,他們更換ICON的時候都沒有出現這些情況,他們應該用的 熱修復 。沒錯,我對他們的技術方案進行了定義,我覺得他們應該採用的是熱修復,然後就跟我們的產品說:我們可以使用熱修復來達到這種效果。最後產品也同意我們使用熱修復了,我們決定使用阿里家的Sophix,這是一款商業化收費的框架,它的接入程度要比其他所有的框架都要簡單。 可是,通過它的文件我才知道,它不支援更換桌面ICON
還是老老實實使用 activity-alias 吧,把它的坑都踩一踩、填一填。
正文
我們這邊先直接上程式碼,有坑的時候,我們再一個一個填:
一般來說:我們定義一個入口的 Activity 一般來說是這樣的:
<pre class="ql-align-justify" style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application>
這樣子的話,就會在我們的桌面上顯示一個入口

如何顯示多入口呢?
<pre class="ql-align-justify" style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity-alias android:name=".StartUpAliasActivity1" android:enabled="true" android:icon="@drawable/ic_camera" 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=".StartUpAliasActivity2" android:enabled="true" android:icon="@drawable/ic_message" 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=".StartUpAliasActivity3" android:enabled="true" android:icon="@drawable/ic_settings" android:targetActivity=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity-alias> </application>
我們這邊又是多定義了三組 activity-alias ,這樣子的話,將會在桌面上多出三個入口。

image
現在我們隨便選一組講解一下:
<pre class="ql-align-justify" style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
<activity-alias <!--這個就是別名,當前入口的別名--> android:name=".StartUpAliasActivity2" <!--是否在桌面上顯示,當前入口是否要在桌面上顯示圖示--> android:enabled="true" <!--在桌面上顯示時的圖示--> android:icon="@drawable/ic_message" <!--給哪個activity設定的別名呢--> android:targetActivity=".MainActivity"> <!--通過桌面圖示開啟的intent-filter的配置--> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity-alias>
通過這個操作,其實我們可以知道更換圖示也就是顯示一個,隱藏剩下的,然後就是顯示另一個,隱藏剩下的。
更換ICON的主要核心程式碼如下:
<pre class="ql-align-justify" style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
fun changeIcon(context: Activity, currentComponentName: String, nextComponentName: String) { val pm = context.packageManager // 隱藏當前顯示的, pm.setComponentEnabledSetting(ComponentName(context, currentComponentName), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) // 顯示需要展示的 pm.setComponentEnabledSetting(ComponentName(context, nextComponentName), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0) }
在 Activity 中的呼叫程式碼:
<pre class="ql-align-justify" style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
changeIcon(this, "cn.fengrong.appiconchanger.MainActivity", "cn.fengrong.appiconchanger.StartUpAliasActivity1")
我們這裡對changeIcon 的引數進行解釋, 這裡有一個坑 :
- currentComponentName :這個引數是指需要隱藏的圖示所對應的ActivityName ,這個引數的來源可以到 AndroidManifest.xml 檔案中檢視,首先看看預設的 ActivityName 是如何獲得的:
- 圖片中有兩個框,預設的 ActivityName 就是通過這兩個框裡面的引數拼接起來的,所以預設的 ActivityName為: cn.fengrong.appiconchanger.MainActivity
- 我們再來看看cn.fengrong.appiconchanger.StartUpAliasActivity1 這個是如何獲得的:
- 有些例子中 currentComponentName 都是直接通過Activity.componentName 去獲取的,但是我這裡將它寫成活的主要是因為,當前的 Activity 有可能並不是應用入口,通常我們的APP都會有一個歡迎頁面,然後再進入到主頁,如果我們在主頁中去修改圖示的話,呼叫 Activity.componentName 就不能到正確的 ActivityName 了, 這就是一個大坑 。
- nextComponentName 這個引數指的是需要展示圖示所對應的ActivityName 。
- 通過以上的這些方法,你就可以動態的更換ICON。
坑
坑一:APP會被關閉
在呼叫更換ICON的方法以後,會有機率出現關閉APP的情況,就算我們將setComponentEnabledSetting 中最後一個引數都設定為PackageManager.DONT_KILL_APP 也於事無補,最後我決定,在關閉App 的時候在呼叫這個方法。我看到有同學在部落格中說到可以這樣做:用一個Activity 進行切換ICON的操作,切換完成以後,在2s之後關閉 activity,這樣子就不會出現關閉APP的情況。有興趣的同學自己自己試試,本人並沒有測試過這種方式。
坑二:桌面圖示重新整理慢
呼叫 changeIcon 方法以後回到桌面上可以看到ICON並沒有被立刻更換掉,點選原來的ICON會出現 未安裝程式應用 提示,然後過了大概6、7秒,甚至更長時間才會更換ICON,在小米手機上(這裡本人只測試三星S9和小米6)上呼叫重新整理桌面的方法還有效果,會將桌面進行重啟,但是三星手機理都不理我。
這種坑的解決辦法是基於我們只在App關閉的時候進行ICON的更換,我們可以看到 changeIcon 方法:
<pre class="ql-align-justify" style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
fun changeIcon(context: Activity, currentComponentName: String, nextComponentName: String) { val pm = context.packageManager pm.setComponentEnabledSetting(ComponentName(context, currentComponentName), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) pm.setComponentEnabledSetting(ComponentName(context, nextComponentName), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0) }
在設定圖示不可見的方法中,我們傳遞的是PackageManager.DONT_KILL_APP ,在設定圖示可見的方法中我們傳遞的是 0 。傳遞0表示會殺死包含該元件的app,桌面圖示立即會更改掉。 這樣設定以後,在 changeIcon 以後執行的方法都不會被呼叫了,因為APP已經被殺死了。
坑三:更改完圖示以後,Android studio就不能更新app了
對於這個坑的解決方法,我們這邊設定了恢復預設ICON的按鈕,並沒有解決這個坑,只是繞過了它。但是APP通過包的安裝更新好像還是可以的。
坑四:更換ICON以後,圖示的位置變了
部分手機會出現這種問題,部分手機不會,這個坑,我也無法解決。
坑五:ICON只能預存,不能從伺服器下載
暫時只能預存進去APP,還不能從伺服器下載