?不錯!實現(xiàn)動態(tài)切換應(yīng)用圖標
大家好,我是皇叔,最近開了一個安卓進階漲薪訓(xùn)練營,可以幫助大家突破技術(shù)&職場瓶頸,從而度過難關(guān),進入心儀的公司。
詳情見文章:沒錯!皇叔開了個訓(xùn)練營
作者:下午吃早餐同學(xué)
https://juejin.cn/post/7018100275336462372
注:經(jīng)常聽到大家討論類似的需求,懷疑大廠是不是用了此方案,據(jù)我個人了解,多數(shù)頭部 app 其實都是發(fā)版來更新節(jié)假日的 icon。
當然本方案也是一種可選的方案,以前我也調(diào)研過,存在問題和作者所述差不多,此外原文鏈接作者也回復(fù)了很多疑問,可以同時了解。
開始了解吧...
注意:我公司已采用此方案投入生產(chǎn),此方案存在缺陷并非完美方案,采用前請評估是否接受缺陷,具體缺陷見文末。
1.效果圖

2.產(chǎn)品需求
市面上很多App能根據(jù)特定活動,動態(tài)切換應(yīng)用圖標達到宣傳目的,例如淘寶雙十一,國慶節(jié)等等。那么我們怎樣才能在不發(fā)新版本的情況下,動態(tài)切換應(yīng)用圖標呢?
3.具體方案
1.圖標更換:在AndroidManifest設(shè)置應(yīng)用入口Activity的別名,然后通過setComponentEnabledSetting動態(tài)啟用或禁用別名進行圖標切換。
2.控制圖標顯示:冷啟動App時,調(diào)用接口判斷是否需要切換icon。
3.觸發(fā)時機:監(jiān)聽App前后臺切換,當App處于后臺時切換圖標,使得用戶無感知。
4.代碼實現(xiàn)
在AndroidManifest.xml中給入口Activity設(shè)置activity-alias
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.SwitchIcon">
<!-- 原MainActivity -->
<activity android:name=".MainActivity" />
<!-- 固定設(shè)置一個默認的別名,用來替代原MainActivity -->
<activity-alias
android:name=".DefaultAliasActivity"
android:enabled="true"
android:icon="@mipmap/ic_launcher"
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>
<!-- 別名1,特定活動需要的圖標如:雙11,國慶節(jié)等 -->
<activity-alias
android:name=".Alias1Activity"
android:enabled="false"
android:icon="@mipmap/ic_launcher_show"
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>
</application>
activity-alias標簽中的屬性如下:
| 標簽 | 作用 |
|---|---|
| android:name | 別名,命名規(guī)則同Actively |
| android:enabled | 是否啟用別名,這里的主要作用的控制顯示應(yīng)用圖標 |
| android:icon | 應(yīng)用圖標 |
| android:label | 應(yīng)用名 |
| android:targetActivity | 必須指向原入口Activity |
在MainActivity中,通過啟用或禁用別名進行圖標切換
/**
* 設(shè)置默認的別名為啟動入口
*/
public void setDefaultAlias() {
PackageManager packageManager = getPackageManager();
ComponentName name1 = new ComponentName(this, "com.fengfeibiao.switchicon.DefaultAliasActivity");
packageManager.setComponentEnabledSetting(name1, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
ComponentName name2 = new ComponentName(this, "com.fengfeibiao.switchicon.Alias1Activity");
packageManager.setComponentEnabledSetting(name2, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}
/**
* 設(shè)置別名1為啟動入口
*/
public void setAlias1() {
PackageManager packageManager = getPackageManager();
ComponentName name1 = new ComponentName(this, "com.fengfeibiao.switchicon.DefaultAliasActivity");
packageManager.setComponentEnabledSetting(name1, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
ComponentName name2 = new ComponentName(this, "com.fengfeibiao.switchicon.Alias1Activity");
packageManager.setComponentEnabledSetting(name2, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}
ForegroundCallbacks監(jiān)聽App前后臺切換
/**
* 監(jiān)聽App前后臺切換
*/
public class ForegroundCallbacks implements Application.ActivityLifecycleCallbacks {
public static final long CHECK_DELAY = 500;
public static final String TAG = ForegroundCallbacks.class.getName();
public interface Listener {
void onForeground();
void onBackground();
}
private static ForegroundCallbacks instance;
private boolean foreground = false, paused = true;
private Handler handler = new Handler();
private List<Listener> listeners = new CopyOnWriteArrayList<Listener>();
private Runnable check;
public static ForegroundCallbacks init(Application application) {
if (instance == null) {
instance = new ForegroundCallbacks();
application.registerActivityLifecycleCallbacks(instance);
}
return instance;
}
public static ForegroundCallbacks get(Application application) {
if (instance == null) {
init(application);
}
return instance;
}
public static ForegroundCallbacks get(Context ctx) {
if (instance == null) {
Context appCtx = ctx.getApplicationContext();
if (appCtx instanceof Application) {
init((Application) appCtx);
}
throw new IllegalStateException(
"Foreground is not initialised and " +
"cannot obtain the Application object");
}
return instance;
}
public static ForegroundCallbacks get() {
if (instance == null) {
throw new IllegalStateException(
"Foreground is not initialised - invoke " +
"at least once with parameterised init/get");
}
return instance;
}
public boolean isForeground() {
return foreground;
}
public boolean isBackground() {
return !foreground;
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
@Override
public void onActivityResumed(Activity activity) {
paused = false;
boolean wasBackground = !foreground;
foreground = true;
if (check != null)
handler.removeCallbacks(check);
if (wasBackground) {
Log.d(TAG, "went foreground");
for (Listener l : listeners) {
try {
l.onForeground();
} catch (Exception exc) {
Log.d(TAG, "Listener threw exception!:" + exc.toString());
}
}
} else {
Log.d(TAG, "still foreground");
}
}
@Override
public void onActivityPaused(Activity activity) {
paused = true;
if (check != null)
handler.removeCallbacks(check);
handler.postDelayed(check = new Runnable() {
@Override
public void run() {
if (foreground && paused) {
foreground = false;
Log.d(TAG, "went background");
for (Listener l : listeners) {
try {
l.onBackground();
} catch (Exception exc) {
Log.d(TAG, "Listener threw exception!:" + exc.toString());
}
}
} else {
Log.d(TAG, "still foreground");
}
}
}, CHECK_DELAY);
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
}
需要在Application中調(diào)用ForegroundCallbacks.init(this)進行初始化。
在MainActivity中實現(xiàn)ForegroundCallbacks.Listener對App進行監(jiān)聽,當處于后臺判斷是否切換應(yīng)用圖標
完整的MainActivity代碼:
public class MainActivity extends AppCompatActivity implements ForegroundCallbacks.Listener {
private int position = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//添加app前后臺監(jiān)聽
ForegroundCallbacks.get(this).addListener(this);
findViewById(R.id.tv_default).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
position = 0;
}
});
findViewById(R.id.tv_alias1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
position = 1;
}
});
}
@Override
protected void onDestroy() {
// 移除app前后臺監(jiān)聽
ForegroundCallbacks.get(this).removeListener(this);
super.onDestroy();
}
@Override
public void onForeground() {
}
@Override
public void onBackground() {
//根據(jù)具體業(yè)務(wù)需求設(shè)置切換條件,我公司采用接口控制icon切換
if (position == 0) {
setDefaultAlias();
} else {
setAlias1();
}
}
/**
* 設(shè)置默認的別名為啟動入口
*/
public void setDefaultAlias() {
PackageManager packageManager = getPackageManager();
ComponentName name1 = new ComponentName(this, "com.fengfeibiao.switchicon.DefaultAliasActivity");
packageManager.setComponentEnabledSetting(name1, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
ComponentName name2 = new ComponentName(this, "com.fengfeibiao.switchicon.Alias1Activity");
packageManager.setComponentEnabledSetting(name2, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}
/**
* 設(shè)置別名1為啟動入口
*/
public void setAlias1() {
PackageManager packageManager = getPackageManager();
ComponentName name1 = new ComponentName(this, "com.fengfeibiao.switchicon.DefaultAliasActivity");
packageManager.setComponentEnabledSetting(name1, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
ComponentName name2 = new ComponentName(this, "com.fengfeibiao.switchicon.Alias1Activity");
packageManager.setComponentEnabledSetting(name2, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}
}5.具體缺陷
具體缺陷如下:
1. 切換icon會關(guān)閉應(yīng)用進程,不是崩潰所以不會上報bugly。
2. 切換icon需要時間,部分華為機型要10s左右,之后能正常打開。
3. 切換icon過程中,部分機型點擊圖標無法打開應(yīng)用,提示應(yīng)用未安裝。
2021.11.15更新 魅族機型 16S 不能動態(tài)切換icon
我公司已采用本方案投入生產(chǎn),后續(xù)發(fā)現(xiàn)有缺陷及解決辦法會及時更新。如采用本方案有其他問題,歡迎留言。
Demo的github地址
https://github.com/FengFeiBiao/SwitchIcon
參考文章
【監(jiān)聽App進入前后臺】
https://blog.csdn.net/mapboo/article/details/104073789
為了防止失聯(lián),歡迎關(guān)注我防備的小號
微信改了推送機制,真愛請星標本公號??



