不错!实现动态切换应用图标
大家好,我是皇叔,最近开了一个安卓进阶涨薪训练营,可以帮助大家突破技术&职场瓶颈,从而度过难关,进入心仪的公司。
详情见文章:没错!皇叔开了个训练营
作者:下午吃早餐同学
https://juejin.cn/post/7018100275336462372
注:经常听到大家讨论类似的需求,怀疑大厂是不是用了此方案,据我个人了解,多数头部 app 其实都是发版来更新节假日的 icon。
当然本方案也是一种可选的方案,以前我也调研过,存在问题和作者所述差不多,此外原文链接作者也回复了很多疑问,可以同时了解。
开始了解吧...
注意:我公司已采用此方案投入生产,此方案存在缺陷并非完美方案,采用前请评估是否接受缺陷,具体缺陷见文末。
1.效果图

2.产品需求
市面上很多App能根据特定活动,动态切换应用图标达到宣传目的,例如淘宝双十一,国庆节等等。那么我们怎样才能在不发新版本的情况下,动态切换应用图标呢?
3.具体方案
1.图标更换:在AndroidManifest设置应用入口Activity的别名,然后通过setComponentEnabledSetting动态启用或禁用别名进行图标切换。
2.控制图标显示:冷启动App时,调用接口判断是否需要切换icon。
3.触发时机:监听App前后台切换,当App处于后台时切换图标,使得用户无感知。
4.代码实现
在AndroidManifest.xml中给入口Activity设置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" />
    <!-- 固定设置一个默认的别名,用来替代原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,国庆节等 -->
    <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 | 别名,命名规则同Actively | 
| android:enabled | 是否启用别名,这里的主要作用的控制显示应用图标 | 
| android:icon | 应用图标 | 
| android:label | 应用名 | 
| android:targetActivity | 必须指向原入口Activity | 
在MainActivity中,通过启用或禁用别名进行图标切换
/**
 * 设置默认的别名为启动入口
 */
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);
    }
/**
 * 设置别名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监听App前后台切换
/**
 * 监听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中调用ForegroundCallbacks.init(this)进行初始化。
在MainActivity中实现ForegroundCallbacks.Listener对App进行监听,当处于后台判断是否切换应用图标
完整的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前后台监听
        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前后台监听
        ForegroundCallbacks.get(this).removeListener(this);
        super.onDestroy();
    }
    @Override
    public void onForeground() {
    }
    @Override
    public void onBackground() {
        //根据具体业务需求设置切换条件,我公司采用接口控制icon切换
        if (position == 0) {
            setDefaultAlias();
        } else {
            setAlias1();
        }
    }
    /**
     * 设置默认的别名为启动入口
     */
    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);
    }
    /**
     * 设置别名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会关闭应用进程,不是崩溃所以不会上报bugly。
2. 切换icon需要时间,部分华为机型要10s左右,之后能正常打开。
3. 切换icon过程中,部分机型点击图标无法打开应用,提示应用未安装。
2021.11.15更新 魅族机型 16S 不能动态切换icon
我公司已采用本方案投入生产,后续发现有缺陷及解决办法会及时更新。如采用本方案有其他问题,欢迎留言。
Demo的github地址
https://github.com/FengFeiBiao/SwitchIcon
参考文章
【监听App进入前后台】
https://blog.csdn.net/mapboo/article/details/104073789
为了防止失联,欢迎关注我防备的小号
微信改了推送机制,真爱请星标本公号👇



