Android動態修改應用圖示和名稱

Wepon發表於2019-01-10

遇到的坑

這裡我把做這個功能中遇到的一些問題寫在前面,是為了大家能先了解有什麼問題存在,遇到這些問題的時候就不慌了,這裡我把應用圖示和名稱先統一使用icon代替進行說明。

1、動態替換icon,只能替換內建的icon,無法從伺服器端獲取來更新icon;

2、動態替換icon以後,應用內更新的時候必須要切換到原始icon),否則可能導致更新安裝失敗(AS上表現為adb執行會失敗),或者升級後應用圖示出現多個甚至應用圖示都不顯示的情況(這些問題都可以通過下面我推薦的開發規則解決掉,所以這是一個坑點,不是肯定會發生的問題,只不過大多數人會遇到。);

3、Android系統動態替換app icon會有延遲,在不同的手機系統上重新整理icon的時間不一樣,大概在10秒左右,在這個時間內點選icon會提示應用未安裝(提示可能會有差別,目前我的小米就不會提示任何資訊,點了沒有反應);

4、更換icon的程式碼執行後一會應用就閃退了,或者導致顯示中的Dialog和PopupWindow報錯崩潰(這個問題和第二個問題有很大的相關性,按我下面給出的規則實行的話是可以解決的。

update: 2019/02/25

5、在android9.0系統上使用了修改應用圖示功能後,在最近工作列裡面不顯示我們的app。關於這個問題在最後的開發規則裡面也會給出解決方案。

多入口配置

多入口配置,字面意思就是應用程式的多個入口配置,在AndroidManifest.xml中有一個叫activity-alias的標籤,這個標籤從字面上看就能理解是activity別名的意思,這裡我給出一個示例作下相應的說明。

activity-alias例子說明:

        <activity-alias
            android:name="NewActivity1"   // 註冊這個元件的名字,不需要生成檔案
            android:enabled="false"       // 是否顯示這個啟動項
            android:label="Alias1"        // 名稱,也就是對應這個啟動項顯示在桌面上的app名稱
            android:icon="@mipmap/ic_launcher_round"    //圖示,也就是對應這個啟動項顯示在桌面上的app圖示 
            android:targetActivity=".MainActivity"      //對應的原來的Activity元件,這裡路徑要跟註冊的Activity對應。
            >
            <intent-filter>  // LAUNCHER 啟動入口
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity-alias>
複製程式碼

顯示多個啟動入口

然後這裡我先做一個多個啟動入口全部顯示的app示例,這裡需要寫的程式碼都在清單檔案中,程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.wepon.switchicondemo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher_round"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        
        <!--原Activity-->
        <activity
            android:enabled="true"
            android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <!--別名1-->
        <activity-alias
            android:name="NewActivity1"
            android:enabled="true"
            android:label="Alias1"
            android:icon="@mipmap/ic_launcher_round"
            android:targetActivity=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity-alias>
        
        <!--別名2-->
        <activity-alias
            android:name="NewActivity2"
            android:enabled="true"
            android:label="Alias2"
            android:icon="@mipmap/ic_launcher"
            android:targetActivity=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

    </application>

</manifest>
複製程式碼

執行後的效果如下:

Android動態修改應用圖示和名稱
可以看到桌面上顯示了三個圖示,進入的都是MainActivity這個頁面,圖示我用的自動生成的,就懶的去找圖示了,效果上能看出來就行。

當然了,實際專案中我們只會顯示一個圖示,這裡我們只需要把"別名1"和"別名2"的android:enabled="true"改為"false"就行了,這樣就只顯示一個圖示了,就不放效果圖了。

程式碼控制切換不同的應用圖示顯示

馬上春節了,我們產品說到哪個時間點我們的應用圖示就要換成春節用的圖示了,當然,前面說了這些圖示要先在應用寫好,不是通過伺服器動態拿的,而是應用內已經寫好的。那這個時候我們就需要通過程式碼進行應用圖示的動態切換了,這裡我給出Demo裡面佈局如圖:

Android動態修改應用圖示和名稱

這裡三個按鈕點選後切換到相應的應用圖示和名稱,"原ACTIVITY"代表只顯示MainActivity這個原來的啟動入口,"ALIAS_1"代表別名1,以此類推。

這三個按鈕點選對應的程式碼如下:

 /**
     * 設定Activity為啟動入口
     * @param view
     */
    public void setActivity(View view) {
        PackageManager packageManager = getPackageManager();
        packageManager.setComponentEnabledSetting(new ComponentName(this, getPackageName() +
                ".NewActivity1"), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager
                .DONT_KILL_APP);
        packageManager.setComponentEnabledSetting(new ComponentName(this, getPackageName() +
                ".NewActivity2"), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager
                .DONT_KILL_APP);
        packageManager.setComponentEnabledSetting(new ComponentName(this, getPackageName() +
                ".MainActivity"), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager
                .DONT_KILL_APP);
    }

    /**
     * 設定別名1為啟動入口
     * @param view
     */
    public void setAlias1(View view) {
        PackageManager packageManager = getPackageManager();
        packageManager.setComponentEnabledSetting(new ComponentName(this, getPackageName() +
                        ".NewActivity1"), PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                PackageManager.DONT_KILL_APP);
        packageManager.setComponentEnabledSetting(new ComponentName(this, getPackageName() +
                ".NewActivity2"), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager
                .DONT_KILL_APP);
        packageManager.setComponentEnabledSetting(new ComponentName(this, getPackageName() +
                ".MainActivity"), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager
                .DONT_KILL_APP);
    }
    /**
     * 設定別名2為啟動入口
     * @param view
     */
    public void setAlias2(View view) {
        PackageManager packageManager = getPackageManager();
        packageManager.setComponentEnabledSetting(new ComponentName(this, getPackageName() +
                        ".NewActivity1"), PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP);
        packageManager.setComponentEnabledSetting(new ComponentName(this, getPackageName() +
                ".NewActivity2"), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager
                .DONT_KILL_APP);
        packageManager.setComponentEnabledSetting(new ComponentName(this, getPackageName() +
                ".MainActivity"), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager
                .DONT_KILL_APP);
    }
    
複製程式碼

!!!這裡要注意一個點,就是ComponentName裡面的路徑一定要寫全了,如果在報錯日誌看到類似找不到這個路徑的日誌的話,那十有八九就是這個問題了。

切換的程式碼其實很少,大家看了基本上也都明白了,這裡就不做過多解釋了。這裡我基於隱藏所以別名的情況下,也就是隻顯示原來的一個APP圖示的情況,點一下"ALIAS_1"這個按鈕,也就是將圖示切換到"別名1",最終效果如下:

Android動態修改應用圖示和名稱

可以看到只顯示這一個入口了,但是如果大家在點了"ALIAS_1"之後,馬上就返回到主頁看盯著這個app的圖示,我們會發現在它在大概10s內是沒有變化的,在大概10s後才更新成我們切換的那個圖示,還有,在它沒更新成功的時候如果我們點這個原來的圖示,一般會吐司一條“未安裝”之類的資訊(華為是未安裝),這裡我的小米是點了沒有反應,要等大概10s秒後更新成功了才能點這個圖示進入應用。所以,通過程式碼我們"已經做到了"圖示的切換,但是!!!

那是不是這樣就完了呢??顯然不是的,問題還挺多的,我一一道來。

不知道大家在點了切換的按鈕後有沒有一直停在app裡面,沒有的話我們嘗試點完後在app裡面不要回到桌面,如果停在app裡面的話,我們會在大概10s,也就是更新成功的時候,應用就會發生閃退了,也就是坑4這個問題。這個問題我做了很多測試,總結了一下原因和規避的方法,原因是我們在程式碼裡面設定了我們原來的真實的那個MainActiviy的enable為false,程式碼如下:

        packageManager.setComponentEnabledSetting(new ComponentName(this, getPackageName() +
                ".MainActivity"), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager
                .DONT_KILL_APP);
複製程式碼

只要程式碼設定了真實的那個Activity的enable為false,也就是程式碼對應的PackageManager.COMPONENT_ENABLED_STATE_DISABLED,那就會導致我們的應用閃退,那是不是我們不設定這個就好了呢?那我們不設定這個的話怎麼隱藏真實的MainActivity的圖示呢?這個解決方法後面我會提出來。

但是,你以為只有這個問題嗎?其實還有坑,只是這個坑不容易發現,這個時候我們回到我們當前的情況,也就是當前我們已經切換到"別名1"了,桌面上也只有這個圖示了,我們也能點選這個圖示正常使用我們的應用,這些都沒有問題,我們以為都是正常的了。但是,這個時候,如果我們通過adb,使用Android Studio執行專案的時候,會提示launch app失敗,失敗的資訊如下:


01/10 16:48:54: Launching app
$ adb shell am start -n "com.wepon.switchicondemo/com.wepon.switchicondemo.MainActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
Error while executing: am start -n "com.wepon.switchicondemo/com.wepon.switchicondemo.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.wepon.switchicondemo/.MainActivity }
Error type 3
Error: Activity class {com.wepon.switchicondemo/com.wepon.switchicondemo.MainActivity} does not exist.

Error while Launching activity

複製程式碼

同樣導致的問題還有一個,就是我們程式碼動態切換了app圖示之後,應用升級,也就是更新應用的時候,會導致安裝失敗,或者是安裝完成後出現多個圖示甚至是沒有圖示出現在桌面上了!!這些問題是要遇到執行,或者升級包的時候才會發現的,但是那時候發現就晚了,所以這是一個比較大的坑,這裡對應的坑就是我在前面提到的坑2這個點。

這裡還有一種情況也會導致坑2的發生,例如,我們Demo現在是一個MainActivity和兩個別名,如果我們在下一個版本把這兩個別名刪除了,或者刪除了我們當前安裝包正在顯示的別名,那麼安裝的新版本可能就不會有應用圖示顯示了,那就會導致我們應用安裝成功了,但是卻沒有入口!

類似的問題還有一些,主要都是在應用升級後發生,而且不管是導致安裝失敗、安裝後沒有圖示或者安裝後產生多個圖示,這些現象都是非常嚴重的,但是這些問題我們都是可以避免的,這裡我總結了一些規則,按這些規則進行操作的話是不會產生以上這些問題的,當然,如果還有其他問題的話歡迎交流,因為我們的app也在做這個功能。

動態修改圖示的開發規則,防坑專用

1、Activity的android:enabled屬性,一定不要在程式碼裡面去設定enabled這個值,否則會在切換圖示的過程導致應用閃退,目前測試了小米、華為和官方模擬器都有在這個問題。

2、清單檔案中設定Activity的android:enabled="false”,這個在之後的版本就固定這個值,如果設定為true了,則有可能在應用升級後出現多個圖示;

3、然後為我們的應用設定一個預設的Activity-alias用來顯示圖示(也是唯一一個顯示的,一般我們也只需要顯示一個圖示),也是用來代替第一點設定Activity的android:enabled="false”可能導致的桌面上沒有應用圖示的問題;

4、Activity-alias的android:enabled="true"的預設顯示的項儘可能不要中途進行變動,如果確實需要使用新的預設值,則使用程式碼進行動態變換;

5、Activity-alias的android:enabled="true"的不要設定為多個,否則會出現多個圖示,如果試圖通過程式碼進行隱藏其中的一個或者幾個,可能會出現圖示消失的情況,這個第2點已經有提過了;

6、後面新的版本如果要加新的Activity-alias,那麼都要設定android:enabled=“false”,這個清單檔案中的值要設定成false,然後再通過程式碼動態變換;

7、後面新的版本的Activity-alias必須包含上一個版本的所有Activity-alias,主要是防止覆蓋安裝後應用圖示消失的情況;

update:2019年1月14日下午5:09 新發現需要注意的問題--------------

8、設定enabled為false的Activity無法在程式碼中通過顯式intent開啟,會報錯。例如:我在應用裡面推送服務推送了一條指定開啟頁面SplashActivity的通知訊息,而這個SplashActivity剛好設定了enabled為false的話,是打不開的,會有錯誤日誌如下,其它同理(所以在專案裡我將啟動入口的Activity單獨寫出來了,除了作為啟動入口用,就沒有別的地方再用到這個Activity了。):

Android動態修改應用圖示和名稱

update:2019年2月25日 新發現需要注意的問題--------------

9、這個問題是關於一開始說的第5個點,在9.0系統的最近工作列不不顯示我們的應用了,如果遇到這個問題,可以嘗試設定一個閃屏activity,啟動模式設定為SingleInstance,通過這個設定的閃屏activity來啟動我們的應用就可以了。或者設定我們的主頁activity為SingleInstance啟動模式也是可以的,關鍵是看大家的專案需求,設定不一樣從後臺回到應用顯示的頁面也就不一樣。這裡的關鍵就是我們設定了enabled為false的activity要和其他的activity不在一個activity棧裡面就行了(我暫時沒明白這塊的原理,也是猜想加程式碼實踐後解決的)。

以上就是我在做這個功能的過程中總結出來的規則,目前沒有發現在其它的問題,有別的問題的朋友歡迎留言討論,還有,按照這些規則做的話,覆蓋安裝後的應用圖示也會是你上一次通過程式碼動態修改成功的圖示,因為手機的Launcher會有記錄,也就是我們通過程式碼會修改這個在Launcher中的記錄。

對了,我們在清單檔案中配置的Activity和Activity-alias的icon和label資訊在新的版本中都是可以換的,這些跟程式碼無關了,也就是跟我們平常換下app圖示名稱是一樣的操作,希望大家不要誤解了這裡 -_-!!!。

最後

最後,可能有的同學會想,我現在的應用入口就是預設的一個Activity,預設的enable也是true,也沒有配置任何的Activity-alias,而我在上面說的規則中都是建議清單檔案中的Activity的android:enabled="false”,那有人可能就會想我的新版本設定成false會不會導致我的圖示入口不見了呢?那麼我告訴你,如果按照我上面說的規則對你的新版本(可以動態切換圖示的版本)進行設定的話,是不會有以上情況產生的,這裡我給一個針對這種情況進行升級的版本的清單檔案的示例:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.wepon.switchicondemo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher_round"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <!--原Activity enabled固定為false,且不通過程式碼進行設定 -->
        <activity
            android:enabled="false"
            android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <!-- 固定設定一個預設的別名,用來替代原Activity-->
        <activity-alias
            android:name="DefaultAlias"
            android:enabled="true"
            android:label="@string/app_name"
            android:icon="@mipmap/ic_launcher_round"
            android:targetActivity=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <!--別名1  春節,雙11,雙12,51,國慶等等,都可以給配置一個別名在清單檔案,這裡我只示例了一個。-->
        <activity-alias
            android:name="NewActivity1"
            android:enabled="false"
            android:label="Alias1"
            android:icon="@mipmap/ic_launcher"
            android:targetActivity=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

    </application>

</manifest>
複製程式碼

簡單示例Demo

這裡放一個簡單的示例demo僅供參考

github.com/ywp0919/Swi…

相關文章