核心原理解析:开启B站少女心模式,探究APP换肤机制的设计与实现

本文主要是介绍核心原理解析:开启B站少女心模式,探究APP换肤机制的设计与实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

<TextView

android:layout_width=“wrap_content”

android:layout_height=“wrap_content”

android:text=“Hello World”

android:textColor="@color/skinPrimaryTextColor" />

二、构建产品化思维:皮肤包

=========================================================================

如何衡量一个开发人员的能力——对复杂功能快速、稳定的交付?

如果只是单纯的认可这个理念,那么对于换肤功能的实现反而简单了,以标题颜色skinPrimaryTextColor为例,我只需要声明两个color资源:

<?xml version="1.0" encoding="utf-8"?>

#000000

#FFFFFF

笔者成功摆脱了复杂的编码实现,在Activity中我只需2行代码即可:

public void initView() {

if (isLightMode) { // 日间模式

tv.setTextColor(R.color.skinPrimaryTextColor);

} else { // 夜间模式

tv.setTextColor(R.color.skinPrimaryTextColor_Dark);

}

}

这种实现并非一无是处,从实现的难度而言,至少能够保护开发者为数不多的发囊。

当然,这种方案有「优化空间」,比如提供封装的工具方法 看似摆脱 无尽的if-else:

/**

  • 获取当前皮肤下真正的color资源,所有color的获取都必须通过该方法。

**/

@ColorRes

public static int getColorRes(@ColorRes int colorRes) {

// 伪代码

if (isLightMode) { // 日间模式

return colorRes; // skinPrimaryTextColor

} else { // 夜间模式

return colorRes + “_Dark”; // skinPrimaryTextColor_Dark

}

}

// 代码中使用该方法,设置标题和次级标题颜色

tv.setTextColor(SkinUtil.getColorRes(R.color.skinPrimaryTextColor));

tvSubTitle.setTextColor(SkinUtil.getColorRes(R.color.skinSecondaryTextColor));

很明显,return colorRes + "_Dark"这行代码作为int类型的返回值是不成立的,读者无需关注具体实现,因为这种封装仍 未摆脱笨重的 if-else 实现 的本质。

可以预见,随着主题数量逐步增多,换肤相关的代码越来越臃肿,最关键的问题是,所有控件的相关颜色都强耦合于换肤相关代码本身,每个UI容器(Activity/Fragment/自定义View)等需要追加Java代码手动设置。

此外,当皮肤数量达到一定规模时,color资源的庞大势必影响到apk体积,因此主题资源的动态加载发势在必行,用户安装应用时默认只有一个主题,其它主题 按需下载和安装 ,比如淘宝:

到了这里,皮肤包的概念应运而出,开发者需要将单个主题的颜色资源视为一个皮肤包,在不同的主题下,对不同的皮肤包进行加载和资源替换:

#000000

#FFFFFF

这样,对于业务代码而言,开发者不再需要关注具体是哪个主题,只需要按常规的方式进行颜色的指定,系统会根据当前的颜色资源对View进行填充:

<TextView

android:layout_width=“wrap_content”

android:layout_height=“wrap_content”

android:text=“Hello World”

android:textColor="@color/skinPrimaryTextColor" />

回到本小节最初的问题,产品化思维也是一个优秀的开发者不可或缺的能力:先根据需求罗列不同的实现方案,做出对应的权衡,最后动手编码。

三、整合思路

==================================================================

目前为止,一切都还停留在需求提出和设计阶段,随着需求的明确,技术难点逐一罗列在开发者面前。

1.动态刷新机制

开发者面临的第一个问题:如何实现换肤后的动态刷新功能。

以微信注册页面为例,手动切换到深色模式后,微信进行了页面的刷新:

读者不禁会问,动态刷新的意义是什么 ,让当前页面重建或者APP重启不行吗?

当然可行,但是 不合理 ,因为页面重建意味着页面状态的丢失,用户无法接受一个表单页面已填信息被重置;而如果要弥补这个问题,对每个页面重建追加状态的保存(Activity.onSaveInstanceState()),在实现的角度来看,也是一个巨大的工程量。

因此动态刷新势在必行——用户无论是在应用内切换了皮肤包,还是手动切换了系统的深色模式,我们如何将这个通知进行下发,保证所有页面都完成对应的刷新呢?

2.保存所有页面的Activity

读者知道,我们可以通过Application.registerActivityLifecycleCallbacks()方法观察到应用内所有Activity的生命周期,这也意味着我们可以持有所有的Activity:

public class MyApp extends Application {

// 当前应用内的所有Activity

private List mPages = new ArrayList();

@Override

public void onCreate() {

super.onCreate();

registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {

@Override

public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {

mPages.add(activity);

}

@Override

public void onActivityDestroyed(@NonNull Activity activity) {

mPages.remove(activity);

}

// …省略其它生命周期

});

}

}

有了所有的Activity的引用,开发者就可以在接到换肤通知的时候,第一时间尝试让所有页面的所有View去更新换肤。

3.成本问题

但巨大的谜团随之映入眼帘,对于控件而言,更新换肤这个概念本身并不存在。

什么意思呢?当换肤通知到达时,我无法令TextView更新文字颜色,也无法令View更新背景颜色——它们都只是系统的控件,执行的都是最基础的逻辑,说白了,开发者根本无法进行编码。

有同学说,那我直接让整个页面的整个View树所有View都全部重新渲染可以吗?可以,但是又回到了最初的问题,那就是所有View本身的状态也被重置了(比如EditText的文字被清零),退一步讲,即使这一点可以被接受,那么整个View树的重新渲染也会极大影响性能。

那么,如何尽可能的节省页面动态刷新的成本 ?

开发者希望,换肤发生时,只对指定控件的指定属性进行动态更新,比如,TextView只关注更新background和textColor,ViewGroup只关注background,其他的属性不需要重置和修改,将设备的每一分性能都利用到极致:

public interface SkinSupportable {

void updateSkin();

}

class SkinCompatTextView extends TextView implements SkinSupportable {

public void updateSkin() {

// 使用当前最新的资源更新 background 和 textColor

}

}

class SkinCompatFrameLayout extends FrameLayout implements SkinSupportable {

public void updateSkin() {

// 使用当前最新的资源更新 background

}

}

如代码所示,SkinSupportable是一个接口,实现该接口的类意味着都支持动态刷新,当换肤发生时,我们只需要拿到当前的Activity,并通过遍历View树,让所有SkinSupportable的实现类都去执行updateSkin方法进行自身的刷新,那么整个页面也就完成了换肤的刷新,同时不会影响View本身当前其他的属性。

当然,这也意味着开发者需要将常规的控件进行一轮覆盖性的封装,并提供出对应的依赖:

implementation ‘skin.support:skin-support:1.0.0’ // 基础控件支持,比如SkinCompatTextView、SkinCompatFrameLayout等

implementation ‘skin.support:skin-support-cardview:1.0.0’ // 三方控件支持,比如SkinCompatCardView

implementation ‘skin.support:skin-support-constraint-layout:1.0.0’ // 三方控件支持,比如SkinCompatConstrain
tLayout

从长期来看,针对控件一一封装,提供可组合选择的依赖,对于换肤库的设计者而言,库本身的开发成本其实并不高。

4.牵一发而动全身

但负责业务开发的开发者叫苦不迭。

按照目前的设计,岂不是工程的xml文件中所有控件都需要重新进行替换?

<TextView

android:layout_width=“wrap_content”

android:layout_height=“wrap_content”

android:text=“Hello World”

android:textColor="@color/skinPrimaryTextColor" />

<skin.support.SkinCompatTextView

android:layout_width=“wrap_content”

android:layout_height=“wrap_content”

android:text=“Hello World”

android:textColor="@color/skinPrimaryTextColor" />

从另一个角度来看,这又是额外的成本,如果哪一天想要剔除或者替换换肤库,那么无异于一次新的重构。

因此设计者需要尽量避免类似 牵一发而动全身 的设计,最好是让开发者无感知的感受到换肤库的 动态更新。

5.着手点: LayoutInflater.Factory2

对 LayoutInflater 不了解的读者,可以参考 这篇文章 。

https://github.com/qingmei2/blogs/issues/25

了解LayoutInflater的读者应该知道,在解析xml文件并实例化View的过程中,LayoutInflater通过自身的Factory2接口,将基础控件拦截并创建成对应的AppCompatXXXView,既避免了反射创建View对性能的影响,也保证了向下的兼容性:

switch (name) {

// 解析xml,基础组件都通过new方式进行创建

case “TextView”:

view = new AppCompatTextView(context, attrs);

break;

case “ImageView”:

view = new AppCompatImageView(context, attrs);

break;

case “Button”:

view = new AppCompatButton(context, attrs);

break;

case “EditText”:

view = new AppCompatEditText(context, attrs);

break;

// …

default:

// 其他通过反射创建

}

一图以蔽之:

因此,LayoutInflater本身的实现思路为我们提供了一个非常好的着手点,我们只需要对这段逻辑进行拦截,将控件的实例化委托给换肤库即可:

如图所示,我们使用SkinCompatViewInflater拦截替换了系统LayoutInflater本身的逻辑,以CardView为例,解析标签时,将CardView生成的逻辑委托给下面的依赖库,如果工程中添加了对应的依赖,那么就能生成对应的SkinCompatCardView,其自然支持了动态换肤功能。

ompatEditText(context, attrs);

break;

// …

default:

// 其他通过反射创建

}

一图以蔽之:

因此,LayoutInflater本身的实现思路为我们提供了一个非常好的着手点,我们只需要对这段逻辑进行拦截,将控件的实例化委托给换肤库即可:

如图所示,我们使用SkinCompatViewInflater拦截替换了系统LayoutInflater本身的逻辑,以CardView为例,解析标签时,将CardView生成的逻辑委托给下面的依赖库,如果工程中添加了对应的依赖,那么就能生成对应的SkinCompatCardView,其自然支持了动态换肤功能。

这篇关于核心原理解析:开启B站少女心模式,探究APP换肤机制的设计与实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/739109

相关文章

如何使用C#串口通讯实现数据的发送和接收

《如何使用C#串口通讯实现数据的发送和接收》本文详细介绍了如何使用C#实现基于串口通讯的数据发送和接收,通过SerialPort类,我们可以轻松实现串口通讯,并结合事件机制实现数据的传递和处理,感兴趣... 目录1. 概述2. 关键技术点2.1 SerialPort类2.2 异步接收数据2.3 数据解析2.

mybatis-plus 实现查询表名动态修改的示例代码

《mybatis-plus实现查询表名动态修改的示例代码》通过MyBatis-Plus实现表名的动态替换,根据配置或入参选择不同的表,本文主要介绍了mybatis-plus实现查询表名动态修改的示... 目录实现数据库初始化依赖包配置读取类设置 myBATis-plus 插件测试通过 mybatis-plu

C#原型模式之如何通过克隆对象来优化创建过程

《C#原型模式之如何通过克隆对象来优化创建过程》原型模式是一种创建型设计模式,通过克隆现有对象来创建新对象,避免重复的创建成本和复杂的初始化过程,它适用于对象创建过程复杂、需要大量相似对象或避免重复初... 目录什么是原型模式?原型模式的工作原理C#中如何实现原型模式?1. 定义原型接口2. 实现原型接口3

Qt把文件夹从A移动到B的实现示例

《Qt把文件夹从A移动到B的实现示例》本文主要介绍了Qt把文件夹从A移动到B的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录如何移动一个文件? 如何移动文件夹(包含里面的全部内容):如何删除文件夹:QT 文件复制,移动(

Flask 验证码自动生成的实现示例

《Flask验证码自动生成的实现示例》本文主要介绍了Flask验证码自动生成的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习... 目录生成图片以及结果处理验证码蓝图html页面展示想必验证码大家都有所了解,但是可以自己定义图片验证码

VSCode配置Anaconda Python环境的实现

《VSCode配置AnacondaPython环境的实现》VisualStudioCode中可以使用Anaconda环境进行Python开发,本文主要介绍了VSCode配置AnacondaPytho... 目录前言一、安装 Visual Studio Code 和 Anaconda二、创建或激活 conda

使用mvn deploy命令上传jar包的实现

《使用mvndeploy命令上传jar包的实现》本文介绍了使用mvndeploy:deploy-file命令将本地仓库中的JAR包重新发布到Maven私服,文中通过示例代码介绍的非常详细,对大家的学... 目录一、背景二、环境三、配置nexus上传账号四、执行deploy命令上传包1. 首先需要把本地仓中要

JAVA封装多线程实现的方式及原理

《JAVA封装多线程实现的方式及原理》:本文主要介绍Java中封装多线程的原理和常见方式,通过封装可以简化多线程的使用,提高安全性,并增强代码的可维护性和可扩展性,需要的朋友可以参考下... 目录前言一、封装的目标二、常见的封装方式及原理总结前言在 Java 中,封装多线程的原理主要围绕着将多线程相关的操

MySQL中实现多表查询的操作方法(配sql+实操图+案例巩固 通俗易懂版)

《MySQL中实现多表查询的操作方法(配sql+实操图+案例巩固通俗易懂版)》本文主要讲解了MySQL中的多表查询,包括子查询、笛卡尔积、自连接、多表查询的实现方法以及多列子查询等,通过实际例子和操... 目录复合查询1. 回顾查询基本操作group by 分组having1. 显示部门号为10的部门名,员

Java进阶学习之如何开启远程调式

《Java进阶学习之如何开启远程调式》Java开发中的远程调试是一项至关重要的技能,特别是在处理生产环境的问题或者协作开发时,:本文主要介绍Java进阶学习之如何开启远程调式的相关资料,需要的朋友... 目录概述Java远程调试的开启与底层原理开启Java远程调试底层原理JVM参数总结&nbsMbKKXJx