IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android换肤逻辑 -> 正文阅读

[移动开发]Android换肤逻辑

换肤逻辑

来归纳一下换肤的相关原理吧!有这么一种说法,对于一个成熟的控件,其实核心逻辑代码仅仅占总代码的10%,并且处理了90%的需求,然后剩下的90%的代码,是用于解决剩下的10%的需求的。本文当然也只是为了梳理换肤的核心逻辑,但这绝对是远远不够的,毕竟优化是无止境的。

思维导图

在这里插入图片描述

核心逻辑

先来看一看换肤的核心逻辑,我可以打包票,看懂了如下的流程图,换肤的核心的逻辑也已经理解的差不多了。

动态换肤的核心逻辑就是维护一份从Activity到其中需要换肤子视图列表的Map,然后在触发换肤操作的时候遍历Map里面的换肤子视图,为其设置对应的主题属性即可。专门维护Activity到子视图列表的Map而不是直接维护子视图列表的目的是我们需要区分哪个子视图属于哪个Activity从而在对应Activity退出之后,我们能释放对应的引用从而避免内存泄漏。另外,后续进行一些优化的操作的时候,这样设计其实有大用处。
在这里插入图片描述

实现思路

设置切入点

既然需要维护一份Activity和换肤视图的对应关系的Map,那么就必须对视图的生成进行把控。我们平时在Activity中是通过**setContentView(Int)来设置当前Activity中所需要显示的视图资源Id。这个方法经过一系列的方法调用,最后会调用LayoutInflater#createViewFromTag(View,String,Context,AttributeSet,boolean)**用于以创建每个视图。来看一下逻辑,我们可以看到如果定义了mFactory和mFactory2的情况下,其实就直接基于他俩来生成视图,因为他俩也实现了相同参数类型的createView和onCreateView方法。这里就不展示具体的源码了,有兴趣的可以自行欣赏:LayoutInflater源码

在这里插入图片描述

所以我们的切入点也非常明确了,就拿这两个factory开刀就行了。这就需要替换每个Activity中LayoutInflater的mFactory,mFactory2变量为自定义LayoutInflaterFactory。这个时候可以通过美国狗狗(此处指代Google,美国人喜欢把热爱的东西称为狗嘛= =)所提供的**LayoutInflaterCompat.setFactory(LayoutInflater,Factory)**方法来设置LayoutInflater中的factory变量。

切入点1:ActivityLifecycleCallback

优点:侵入性低,可拓展性更好

Android在Application中提供了这样一个API用于对于Activity各个生命周期的统一回调:Application.registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks),通过实现自定义ActivityLifecycleCallbacks我们就可以对Activity的生命周期设置回调。

public interface ActivityLifecycleCallbacks {
    void onActivityCreated(Activity activity, Bundle savedInstanceState);
    void onActivityStarted(Activity activity);
    void onActivityResumed(Activity activity);
    void onActivityPaused(Activity activity);
    void onActivityStopped(Activity activity);
    void onActivitySaveInstanceState(Activity activity, Bundle outState);
    void onActivityDestroyed(Activity activity);
}

在onActivityCreated方法中我们就可以通过activity参数获取LayoutInflater,进而更换factory。

来看看来onActivityCreated(Activity,Bundle)的回调时机,实际上onActivityCreated(Activity,Bundle)其实是在Activity#onCreate(Bundle)中被调用的,差点被方法名误导。这个回调方法执行的位置在super.onCreate(Bundle),也就是还并没有执行setContentView(int)以设置视图。因此这部分逻辑就没有问题,先替换factory,再基于替换后的factory生成视图。

在这里插入图片描述

可以来看一下Activity的onCreate方法,我们可以看见Activity会尝试去调用Application中所有已注册的onActivityCreated回调。

Activity.java
protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...可怜的被忽略代码...
        dispatchActivityCreated(savedInstanceState);
    ...可怜的被忽略代码...
}

Activity.java
private void dispatchActivityCreated(@Nullable Bundle savedInstanceState) {
    getApplication().dispatchActivityCreated(this, savedInstanceState);
    ...可怜的被忽略代码...
}

Application.java
@UnsupportedAppUsage
/* package */ void dispatchActivityCreated(@NonNull Activity activity,
                                           @Nullable Bundle savedInstanceState) {
    Object[] callbacks = collectActivityLifecycleCallbacks();
    if (callbacks != null) {
        //对当前Activity执行每一个回调
        for (int i=0; i<callbacks.length; i++) {
            ((ActivityLifecycleCallbacks)callbacks[i]).onActivityCreated(activity,
                                                                         savedInstanceState);
        }
    }
}

切入点2:BaseActivity

缺点:必须要依赖一个开放的BaseActivity

在BaseActivity中进行切入其实非常简单,因为是在Activity内直接调用的原因,所以非常容易根据Activity获取到LayoutInflater进而更换factory。

在这里插入图片描述

维护Activity和换肤视图的关系

整个动态换肤的核心思路就是维护一份从Activity到换肤视图的映射关系(Map),这就需要我们来通过自定义Factory的方式来用将视图生成逻辑交由自己,而并非官方API来控制,流程如下:
在这里插入图片描述

创建视图

因为是自定义Factory,所以需要自定义视图还是需要由就需要自己生成,这里我们就通过反射获取到LayoutInflater.createViewFromTag(View,String,Context,AttributeSet,boolean)方法用于创建视图。这里强调一点,在动态换肤的需求中我们对于视图创建本身并没有改动,我们实际上只是在其基础上增加了一层视图变换的逻辑。

既然获取到了createViewFromTag方法,在我们自定义的Factory中直接通过反射获取到的Method来创建视图即可。也不用担心缺少参数,因为自定义的LayoutInflaterFactory所实现的方法onCreateView的参数类型和LayoutInflater#createViewFromTag的参数类型是一样的。

public interface LayoutInflaterFactory {
     View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
    ...被忽略的无辜代码...
}

基于规则过滤出换肤子视图

目前我所了解到的主要有两种基本的过滤规则来过滤出我们实际上需要进行动态换肤的子视图。

基于接口

这种过滤规则适用于自定义视图的换肤操作,在换肤组件中,我们往往会定义一个接口用来标识某个自定义视图为支持换肤的视图。所以当我们创建完视图后,就直接可以通过判定该视图是否继承该接口从而完成视图过滤。

基于属性

在上一个步骤中我们可以看到createViewFromTag方法所传入的参数中包含了AttributeSet,这个参数顾名思义存储的就是在XML中定义该视图的所有属性。我们只需要遍历AttributeSet中的所有属性信息,基于其中的换肤属性来完成视图过滤。

在这里插入图片描述

来看看这部分功能具体是怎么实现的,我们在XML中定义属性的时候,实质上就是定义了一对key-value。其实无论是从key还是从value的都是能够达成过滤出视图的目的,只要换肤属性能和一般的属性区分开来就行了。我们这里为了节省篇幅,就只讲讲怎么从value的角度上从众多属性中过滤出我们所感兴趣的换肤属性。

这里介绍一种比较常见的过滤规则:根据资源名的后缀进行换肤,过滤规则有很多,只要属性的在不同皮肤下的资源不会造成冲突就没有什么大的问题。如果有机会,我会另外写一篇如何通过将不同皮肤的资源打到不同的皮肤包中从而使用的方式,这种方式也是非常的有趣。

所谓根据资源名的后缀进行换肤是一种相对直观,但与此同时也是比较死板的一种换肤方式,这种方式是通过对不同主题命名不同后缀的方式来达成属性的索引的。什么意思呢?

假设我们当前支持两种主题,白天和夜晚模式,那么对于需要换肤的属性,就需要为这两套主题分别准备一套资源,为了与普通的资源区分开来,换肤资源的资源名有专有后缀,白天为**_day**,夜晚为**_night**,为了避免歧义,从原则上我们是不允许一般的资源名使用这两种后缀的。然后,当我们实际上遍历属性的时候,如果发现所使用的资源名为_day,那么就意味着该属性有换肤的需求,在进行实际换肤的时候将属性的属性值在_day和_night之间进行切换就行了。

我们所实现的LayoutInflaterFactory实现了onCreateView(View parent, String name, Context context,AttributeSet attrs)方法,然后每次通过我们自定义的LayoutInflaterFactory创建一个新的视图时我们都能通过AttributeSet参数来获取到在xml中给当前视图所设置的所有参数,并通过遍历的方式过滤我们感兴趣的属性。

new LayoutInflaterFactory() {
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        final int attributeSetLength = attrs.getAttributeCount();
        for (int i = 0; i < attributeSetLength; ++i) {
            //获取属性名称
            String attributeName = attrs.getAttributeName(i);
            //获取属性值
            String attributeValue = attrs.getAttributeValue(i);
			//如果是引用,获取不带后缀的属性值字符串
            //如果属性符合后缀规则,去除第一位@符号获取id
            if(attributeValue.startsWith("@")&&attributeValue.endsWith("_day")) {
                int attributeId = Integer.parseInt(attributeValue.substring(1));
                String attributeType = context.getResources().getResourceTypeName(attributeId);
            }
        }
        return null;
    }
};

AttributeSet提供了AttributeSet.getAttributeValue(int)方法来获取当前遍历序号属性所对应的属性值。属性值分为三种,一直是具体的数值,一种是系统引用资源,另外一种是普通资源引用。三者的区别在于,具体的数值,常见是以下的形式android:background="#777777";系统引用资源以?开头,常见是以下的形式android:background="?android:attr/windowBackground";普通资源引用是以@开头,@之后的就是AAPT为该资源所分配的id,通过这个id我们就能通过**Resources#getResourceEntryName(int redId)**来获取到该id引用的具体资源,根据这个资源的资源名后缀,我们就能知道我们是否想要对其动态的换肤。如果该资源的属性值满足我们所定义的后缀规则,那么我们就通过其引用的id来获取该资源的类型。因为对于一个属性,可能使用不同类型的资源,而对于不同类型的资源,处理方式可能也有区别。

也就是说,对于需要换肤的属性,我们会获取三个信息并储存:

class SkinAttr{
    //属性名称
    private String attrName;
    //属性类型
    private String attrType;
    //属性值(去后缀)
    private String attrValue;
}

建立Activity和换肤视图的关系

在上一个步骤中我们已经获取到了所有存在换肤需求的子视图以及这些子视图需要进行动态换肤的属性。

接下来就是构建子视图,换肤属性以及Activity之间的联系。比较简单暴力的做法就是创建如下的存储结构,分别存储activity到其中的子视图的映射关系以及子视图到其中的属性的映射关系:

//Activity到其中的子视图的映射关系
Map<Activity, List<View>> activity2ChildViewMap = new HashMap<>();
//子视图到其中的属性列表的映射关系,属性Pair分别存储属性的名称和属性的去后缀名称
Map<View,List<Pair<String,String>>> childView2AttributePairMap = new HashMap<>(); 

首先来说说为什么特地需要构建这三者之间的联系而不是仅仅是子视图和换肤属性之间的联系。其实这里的Activity并不是单单指Activity,它更多的泛指所有带有像Activity这样多层视图的带有生命周期的视图结构。用人话说,就是像Activity一样,一个新的Activity显示出来后,之前显示的Activity就不再活跃。既然不再活跃,那么我们完全不需要在其完全不显示的情况下他里面的视图进行视图的操作,这无疑是对性能的强烈浪费。

所以,按照如上所说,我们创建Activity,Activity中的子视图以及子视图之中的换肤属性三者之间的互相对应关系就非常有必要了。

在这里插入图片描述

如上图,Activity和ChildView,ChildView和Attribute都是一对多的关系。每当我们通过点击换肤按钮或者其他操作触发换肤操作的时候,我们只根据上述的对应关系对当前所显示的Activity进行换肤的操作,而对于其他Activity,等到具体需要显示的时候再去设置其属性就行了。

这样做其实还有一个好处,在反复点击换肤按钮的时候(总有人会这么做,特别是测试= =),性能损耗相比遍历全体换肤视图更低,效率明显更高!

触发换肤

需要触发换肤,根据上一个步骤所说,我们首先需要知道当前正在显示的Activity的信息(如果有Activity不完全覆盖屏幕的情况就需要获取多个Activity信息),但是因为我们是通过ActivityLifecycleCallback或者BaseActivity来进行子视图的捕获的,所以想要知道当前显示的Activity并不困难。

在获取到正在显示的Activity列表之后,我们就按图索骥,根据上一个步骤所建立的Activity,子视图,换肤属性三者关系就能找出正在显示视图中所有需要换肤的子视图以及相应的换肤属性。

到这里,材料都已经拿到了,接下来就是怎么处理这些材料从而达到换肤的目的。我们定义一个接口,在这个接口中定义如何根据特定的视图和换肤属性来为视图换肤的方法。接口中定义了三个方法,用于获取视图名称,获取属性名称,以及到底怎么基于视图,属性名称,属性值以及当前的主题来进行换肤操作。

interface IChangeSkin<T extends View>{
    //获取属性名称
    String getAttributeName();
    //获取属性类型
    String getAttributeType();
    /**
      * @param childView 换肤视图
      * @param attributeValue 属性值
      */
    void changeSkin(T childView, String attributeValue);
}

来看一个实现,这个实现就定义了对于TextView的drawable类型的背景换肤逻辑,就是我们根据当前应用的报名,换肤属性的类型名称,以及需要换肤的资源名称来获取到实际的资源id并设置到对应视图:

new IChangeSkin<TextView>(){
    @Override
    public String getAttributeName() {
        return "background";
    }
    @Override
    public String getAttributeType() {
        return "drawable";
    }
    @Override
    public void changeSkin(TextView childView, String attributeValue) {
       childView.setBackgroundResource(childView.getContext().getResources().getIdentifier(attributeValue+"当前主题后缀",getAttributeType(),context.getPackageName()));
    }
};

然后我们统一定义一个管理类,用于专门存储对于这个接口在各种视图,各种属性下的实现。这样一来我们在对一个视图进行换肤操作的时候,就只需要直接在管理类中比对属性名称和属性类型是否相同来寻找材料中视图和换肤属性所对应的换肤操作接口的实现,进而进行换肤操作就行了。

解除Activity和换肤视图的关系

既然我们是通过容器来存储Activity,换肤子视图以及换肤属性的对应关系的,就意味着如果不及时释放就可能会产生内存泄漏的问题。所幸我们通过ActivityLifecycleCallback或BaseActivity对Activity的生命周期进行着监控,所以我们只需要在Activity被销毁,也就是onDestroy()的时候将容器内该Activity对应的内容释放就行了。

肤操作接口的实现,进而进行换肤操作就行了。

解除Activity和换肤视图的关系

既然我们是通过容器来存储Activity,换肤子视图以及换肤属性的对应关系的,就意味着如果不及时释放就可能会产生内存泄漏的问题。所幸我们通过ActivityLifecycleCallback或BaseActivity对Activity的生命周期进行着监控,所以我们只需要在Activity被销毁,也就是onDestroy()的时候将容器内该Activity对应的内容释放就行了。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-07-17 12:03:47  更:2021-07-17 12:06:05 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 13:15:14-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码