经常听到大家讨论类似的需求,怀疑大厂是不是用了此方案,据我个人了解,多数头部 app 其实都是发版来更新节假日的 icon。当然本方案也是一种可选的方案,以前我也调研过,存在问题和作者所述差不多,此外原文链接作者也回复了很多疑问,可以同时了解。
效果图
产品需求
市面上很多App能根据特定活动,动态切换应用图标达到宣传目的,例如淘宝双十一,国庆节等等。那么我们怎样才能在不发新版本的情况下,动态切换应用图标呢?
具体方案
1.图标更换:在AndroidManifest设置应用入口Activity的别名,然后通过setComponentEnabledSetting动态启用或禁用别名进行图标切换。
2.控制图标显示:冷启动App时,调用接口判断是否需要切换icon。
3.触发时机:监听App前后台切换,当App处于后台时切换图标,使得用户无感知。
代码实现
在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);
}
}
具体缺陷
具体缺陷如下:
1. 切换icon会关闭应用进程,不是崩溃所以不会上报bugly。
2. 切换icon需要时间,部分华为机型要10s左右,之后能正常打开。
3. 切换icon过程中,部分机型点击图标无法打开应用,提示应用未安装。