Android 动态切换应用图标方案

news2024/9/28 9:19:28

经常听到大家讨论类似的需求,怀疑大厂是不是用了此方案,据我个人了解,多数头部 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过程中,部分机型点击图标无法打开应用,提示应用未安装。

Demo的github地址

https://github.com/FengFeiBiao/SwitchIcon

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/371346.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Pyparsing为嵌入式开发定义自己的脚本语言

Python在嵌入式开发中也很流行生成实用脚本。Pyparsing还允许你轻松地定义在Python上下文中运行的定制脚本语言。Python实现的系统旨在能够独立执行用户传递的一系列命令。你希望系统以脚本的形式接收命令。用户应该能够定义条件。这种对通信中逻辑元素的最初简单的声音要求&am…

【Hello Linux】初识操作系统

作者&#xff1a;小萌新 专栏&#xff1a;Linux 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;简单介绍下操作系统的概念 操作系统 操作系统是什么&#xff1f; 操作系统是管理软硬件资源的软件 为什么要设计操作系统 为什么要设…

认识html

1.html的特点先看一段简单的html代码<html><head></head><body>hello world</body> </html>如果将这段带有这段代码的.html文件拉进浏览器中,就会出现一个页面,内容就是hello world,如下图:由上面的代码,我们可以了解到一些html代码的特点…

Java - 数据结构,队列

一、什么是队列 普通队列&#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出FIFO(FirstIn First Out) 入队列&#xff1a;进行插入操作的一端称为队尾&#xff08;Tail/Rear&#xff09; 出队列&#xf…

【华为OD机试模拟题】用 C++ 实现 - 对称美学(2023.Q1)

最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 获得完美走位(2023.Q1) 文章目录 最近更新的博客使用说明对称美学题目输入输出示例一输入输出说明示例二输入输出说明备注Code使用说明 参加华为od机试,一定要注意不要完全背诵代码࿰

五、DeepWalk、Node2Vec论文精读与代码实战【CS224W】(Datawhale组队学习)

开源内容&#xff1a;https://github.com/TommyZihao/zihao_course/tree/main/CS224W 子豪兄B 站视频&#xff1a;https://space.bilibili.com/1900783/channel/collectiondetail?sid915098 斯坦福官方课程主页&#xff1a;https://web.stanford.edu/class/cs224w 文章目录D…

拿下域控后,我还是对大佬的操作念念不忘

历来攻防演练中&#xff0c;我都笃信一个道理——吃饱了才有力气干活。所以&#xff0c;在清晨的客户现场&#xff0c;当看到大佬满意地吃完了我带来的煎饺&#xff0c;我知道这一战&#xff0c;我们作为攻击队&#xff0c;基本已经拿下了。 虽然说的每一句话都带着一股醋味儿…

Android 高工分享一波性能优化的总结~

随着 Android 开发越来越规范&#xff0c;国内工程师的素质&#xff0c;以及用户对产品的要求也越来越高。这也间接导致我们对研发项目的质量要求到了近乎苛刻的地步&#xff0c;**内存优化、UI 卡顿优化、App 崩溃监控等性能调优也逐渐成了人手必备的技能。**工作之余&#xf…

Boost资料整理备忘

Boost资料整理备忘 网络资源 书籍: The Boost C Libraries官方文档 Boost Library Documentation random boost.randomBoost随机库的简单使用&#xff1a;Boost.Random(STL通用)tutorialstd::random boost::asio Boost.Asio 网络编程 - 基本原理Boost.Asio DocBoost定时器 网…

理光M2701复印机载体初始化方法

理光M2701基本参数&#xff1a; 产品类型&#xff1a;数码复合机 颜色类型&#xff1a;黑白 复印速度&#xff1a;单面&#xff1a;27cpm 双面&#xff1a;16cpm 涵盖功能&#xff1a;复印、打印、扫描 网络功能&#xff1a;支持无线、有线网络打印 接口类型&#xff1a;USB2.0…

如何建立项目标准化评价体系?【锦狸】

PMO团队面临着管理多个项目&#xff0c;甚至是多个项目集&#xff0c;多个产品集的问题&#xff0c;那么如何对项目们进行标准化评价体系的建设&#xff0c;就是PMO需要首先思考的问题。 首先我们要关注项目的背景&#xff0c;了解了项目背景之后&#xff0c;我们才可以明确项…

CPython解释器性能分析与优化

原文来自微信公众号“编程语言Lab”&#xff1a;CPython 解释器性能分析与优化 搜索关注 “编程语言Lab”公众号&#xff08;HW-PLLab&#xff09;获取更多技术内容&#xff01; 欢迎加入 编程语言社区 SIG-元编程 参与交流讨论&#xff08;加入方式&#xff1a;添加文末小助手…

【Linux】使用U盘自动化安装Linux(VMware虚拟机)

文章目录前言一、准备二、新建虚拟机2.1 创建虚拟机2.2 新增硬盘2.3 系统启动项三、加电运行四、EFI方式五、总结前言 一、准备 基于之前的基础【Linux】Kickstart 配置U盘自动化安装Linux系统&#xff0c;现在我们可以在虚拟机中尝试自动化安装Linux系统。 二、新建虚拟机 …

POI导入导出、EasyExcel批量导入和分页导出

文件导入导出POI、EasyExcel POI&#xff1a;消耗内存非常大&#xff0c;在线上发生过堆内存溢出OOM&#xff1b;在导出大数据量的记录的时候也会造成堆溢出甚至宕机&#xff0c;如果导入导出数据量小的话还是考虑的&#xff0c;下面简单介绍POI怎么使用 POI导入 首先拿到文…

Java:如何选择一个Java API框架

Java编程语言是一种高级的、面向对象的语言&#xff0c;它使开发人员能够创建健壮的、可重用的代码。Java以其可移植性和平台独立性而闻名&#xff0c;这意味着Java代码可以在任何支持Java运行时环境(JRE)的系统上运行。Java和Node js一样&#xff0c;是一种功能强大的通用编程…

机试指南

文章目录零、绪论和IDE安装int取值范围常犯的编程小错误一、枚举和模拟 (暴力求解)(一) 枚举1.Reverse函数 求 反序数2.程序出错的原因1.编译错误 (compile)&#xff1a;基本语法错误2.链接错误 (link)&#xff1a;函数名写错了3.运行错误 (run)&#xff1a;结果与预期不符&…

前后端分离开发Springboot+VUE学习笔记

学习内容来源&#xff1a;传送门 目录前后端分离实现技术创建vue项目在idea中打开新建页面创建SpringBoot应用创建实体对象与数据库表元素绑定创建实体类接口前端调用数据跨域传输在springboot中解决总结前后端分离 前后端分离就是将一个应用的前端和后端代码分开写&#xff0…

前端:分享JS中7个高频的工具函数

目录 ◆1、将数字转换为货币 ◆2、将 HTML 字符串转换为 DOM 对象 ◆3、防抖 ◆4、日期验证 ◆5、将 FormData&#xff08;表单数据&#xff09;转换为 JSON ◆6、衡量一个函数的性能 ◆7、从数组中删除重复项 JavaScript 实用函数是有用的、可重复使用的片段&#xff0…

STM32开发(14)----CubeMX配置ADC

CubeMX配置ADC前言一、什么是ADC&#xff1f;二、实验过程1.单通道ADC采集STM32CubeMX配置代码实现2.多通道ADC采样(非DMA)STM32CubeMX配置代码实现3.多通道ADC采样&#xff08;DMA&#xff09;STM32CubeMX配置代码实现总结前言 本章介绍使用STM32CubeMX对ADC进行配置的方法&a…

SpringCloud之Seata(二)

4.Seata如何应用于项目&#xff1f; 安装seata及修改配置 4.1 官网下载Seata安装包 4.2 修改seata/config.txt 4.2.1 修改存储方式 store.db.dbTypemysql store.db.driverClassNamecom.mysql.jdbc.Driver store.db.urljdbc:mysql://你的IP:3306/seata?useUnicodetrue sto…