Coder Social home page Coder Social logo

nestaticskinpeeler's Introduction

静态换肤实现以及原理分析

[TOC]

静态换肤,也称内置换肤,默认使用系统的(白天/黑夜)模式,通过res/valuesres/values-night目录下不同的颜色文件来起到换肤的效果,不需要皮肤包。

一、实现效果图

image

二、实现过程浅析

2.1 总体思路

实现一键全局换肤的整体思路是重写在ActivityonCreate()方法中setContentView(...)之前设置工厂Factory2,然后重写onCreateView()方法(布局文件从xml渲染到屏幕的过程中会先调用tryCreateView()方法,该方法中调用Factory2onCreateView()方法),在此方法中将布局文件中的系统控件替换为自定义控件,在自定义控件中实现换肤接口,在需要换肤的时候递归遍历decorView中实现了换肤接口的控件,调用其换肤方法实现换肤。

2.2 详细步骤

2.2.1 自定义NeTextView等控件

自定义NeTextView等控件,此类控件继承原生控件,并实现了ViewsChange接口。控件在初始化时,保存其自定义属性(主要为backgroundtextColor等)和其对应的资源idAttrsBeanresourcesMap中。换肤时,通过ViewsChange接口的实现方法,经AttrsBeanresourcesMap中的资源id获取对应资源文件夹(res/valuesres/values-night)下相应的资源:background drawabletextColor,然后设置给该自定义控件即可完成换肤。

2.2.2 实现ActionBarUtils等工具类

实现ActionBarUtils等工具类,该类中的方法获取主题颜色属性资源,即res/valuesres/values-night目录下colors.xml中颜色资源,设置给ActionBar,每次换肤时调用一次,即可完成换肤。

2.2.3 所有需要换肤的Activity都需继承SkinActivity

该基类的核心在于三点:

  • onCreate()方法中提前设置工厂

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        //我们要抢先一步,比系统还有早,拿到主动权
        LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), this);
        super.onCreate(savedInstanceState);
    }
  • onCreateView()方法中将原生控件替换为自定义控件

    //此函数 比系统的 onCreateView 函数更早执行,我们就能够采集布局里面所有的View
    //把xml中的系统控件替换成你自己的控件
    @Nullable
    @Override
    public View onCreateView(@Nullable View parent, @NonNull String name,
                             @NonNull Context context, @NonNull AttributeSet attrs) {
    	if(openSkin()) {
    		if(null == viewInflater) {
    			viewInflater = new MyAppCompatViewInflater(context);
    		}
    		viewInflater.setName(name); //TextView
    		viewInflater.setAttrs(attrs); //TextView所有的属性集
    		return viewInflater.createViewAction(); // 如果返回null,系统判断时 null, 就会走系统的,不影响
      }
      return super.onCreateView(parent, name, context, attrs); //继续正常走系统的
    }
  • 设置白天/黑夜模式时,递归遍历View,对于实现了ViewsChange接口的View,调用其接口方法,实现换肤操作

       /**
         * 暴露给使用者使用,换肤操作(内置换肤,没有皮肤包,只有黑夜和白天)
         * @param mode
         */
        public void setDayNightMode(int mode) {
            getDelegate().setLocalNightMode(mode); //兼容包,提供了黑夜,白天模式
    
            final boolean isPost21 = Build.VERSION.SDK_INT >= 21;
            if(isPost21) {
                StatusBarUtils.forStatusBar(this); //改变状态栏颜色
                ActionBarUtils.forActionBar(this); //标题栏颜色
                NavigationUtils.forNavigation(this); //虚拟按钮栏颜色
            }
            View decorView = getWindow().getDecorView();
            changeSkinAction(decorView);
        }
    
        /**
         * 使用递归 + 接口调用方式 换肤
         * @param decorView
         */
        private void changeSkinAction(View decorView) {
            if(decorView instanceof ViewsChange) { //只有成为了ViewsChange标准,才有资格换肤
                ViewsChange viewsChange = (ViewsChange) decorView;
                viewsChange.skinChangeAction();
            }
    
            if(decorView instanceof ViewGroup) {
                ViewGroup viewGroup = (ViewGroup) decorView;
                for (int i = 0; i < viewGroup.getChildCount(); i++) {
                    View childAt = viewGroup.getChildAt(i);
                    changeSkinAction(childAt);
                }
            }
        }

2.3 需要注意的点

AndroidManifest.xml文件中的activity标签中设置android:confiChanges可以避免换肤时屏幕闪烁。

<activity android:name=".MainActivity"
    android:configChanges="uiMode"> <!-- 如果不加这个模式,换肤时屏幕会抖,看起来不爽-->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

2.4 本文换肤与网上换肤框架的比较

2.4.1 网上换肤框架缺点

没有搞清楚 系统流程,各种暴力反射,增加了无需的代码;没有做兼容,没有考虑后续维护性;使用几个集合去存储 所有的控件,当焕肤的时候,多重for去遍历集合 (性能很低);作死的遍,非常暴力,自己去解析布局文件,用多个集合存储,多重for去遍历集合 ,去焕肤(崩溃)。

  1. 收集所有的控件里面的属性集

  2. 没有考虑到V7 V4兼容

  3. 用两套集合去装控件+属性集合

  4. 换肤时,多重for循环,性能损耗

  5. 代码量太多,写了很多无用代码

  6. 暴力的方式 mFactorySet

2.4.2 本文优点

  1. 知道系统流程,可以节省很多代码,利用系统的功能
  2. 时时刻刻考虑兼容
  3. 性能高
  4. 可扩展性,维护性 高
  5. 看系统源码,去拦截,灵活的操作

三、原理分析

3.1 setContentView(resID)分析

为什么通过这句代码把布局ID丢进去之后就能把xml中的布局加载到Activity上呢?

点击setContentView()跟进源码,一路追踪AppcompatActivity-->AppCompatDelegate-->AppCompatDelegateImpl,在AppCompatDelegateImpl中,我们发现resId传给了LayoutInflater中的inflate()方法。

set_content_view1

继续查看inflate()方法

inflate2

inflate3

我们发现resId最终通过ResourcesgetLayout()方法将int型的资源id转换成了一个XmlResourceParser对象,该对象是一个xml的解析工具,具体用法可参考这里:使用XmlResourceParser动态解析XML

然后再一次调用inflate()方法,并将转换后的XmlResourceParser对象传入。我们继续查看inflate()方法:

inflate4

inflate5

在该方法中,如果xml的根布局为<merge/>标签的话,调用rInflate()方法递归创建View,否则的话就调用createViewFromTag()方法和rInflateChildren()方法,该方法又会调用rInflate()方法。我们先看一下rInflate()方法做了什么:

rinflate6

可以看到该方法先是做了一些检查,然后同样调用了createViewFromTag()方法和rInflateChildren()方法,我们先进入createViewFromTag()方法:

create_view_from_tag7

这里我们关注两点,首先是调用tryCreateView()方法创建View;其次如果为空的话,会调用onCreateView()createView()方法来创建View

这里可以看到一个知识点是系统如何区分这个View到底是系统控件还是自定义控件呢?关键在于-1 == name.indexOf('.')这个判断条件,由此我们可以联想到系统控件是不需要加完整包名的,如TextView,而我们的自定义控件则必须使用完整包名,如com.xxx.xxx.MyView。这里没有.的就是系统控件,有.的就是自定义控件。对于系统控件,调用onCreateView()来创建View,对于自定义控件,则调用createView()方法来创建View

我们先看一下tryCreateView()方法做了什么:

try_create_view8

tryCreateView()中用到了几个工厂mFactorymFactory2mPrivateFactory,我们来看一下这几个工厂:

    @UnsupportedAppUsage
    private Factory mFactory;
    @UnsupportedAppUsage
    private Factory2 mFactory2;
    @UnsupportedAppUsage
    private Factory2 mPrivateFactory;

    public interface Factory {
        /**
         * Hook you can supply that is called when inflating from a LayoutInflater.
         * You can use this to customize the tag names available in your XML
         * layout files.
         *
         * <p>
         * Note that it is good practice to prefix these custom names with your
         * package (i.e., com.coolcompany.apps) to avoid conflicts with system
         * names.
         *
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         *
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        @Nullable
        View onCreateView(@NonNull String name, @NonNull Context context,
                @NonNull AttributeSet attrs);
    }

    public interface Factory2 extends Factory {
        /**
         * Version of {@link #onCreateView(String, Context, AttributeSet)}
         * that also supplies the parent that the view created view will be
         * placed in.
         *
         * @param parent The parent that the created view will be placed
         * in; <em>note that this may be null</em>.
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         *
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        @Nullable
        View onCreateView(@Nullable View parent, @NonNull String name,
                @NonNull Context context, @NonNull AttributeSet attrs);
    }

这两个工厂其实就是接口,而且Factory2是继承自Factory的,里面都有一个onCreateView()方法。

回过头来继续看tryCreateView(),我们发现如果这几个工厂有不为空的话,就调用它的onCreateView()方法来创建View,否则的话就返回空的View。即tryCreateView()方法尝试用工厂接口来创建View,如果创建的View不为空的话,就不会走系统的创建View的过程了,否则走系统的创建View的过程。

假如我们没有设置工厂的话,继续分析系统创建View的流程,先看创建系统控件ViewonCreateView()方法:

on_create_view9

on_create_view10

on_create_view11

这里我们可以看到在createView()方法中传入了android.view.字符串参数,然后看一下在这个方法里面做了什么:

create_view12

create_view13

create_view14

我们可以看到在最终的createView()方法中传进来的prefix参数android.view.name参数拼接成系统View的完整路径名,如android.view.TextView,并通过反射拿到实例对象,并在854行将通过反射创建出来的View返回出去,这样就完成了系统自定义控件从XMLView的创建流程。

再看看自定义View和系统控件View的区别:

create_view_from_tag15

可以看到自定义View和系统View的创建流程其实是一样的,不过这里没有传前缀prefix参数,这样通过反射创建View的时候就不用拼接前缀了,直接用完整路径就可以拿到对象实例,直接创建View了。

总结:

resID  --> XmlResourceParser ---> while遍历当前布局里面所有的控件  
{  
    要添加进去的View view = createViewFromTag() {  
        1.区分是否是自定义控件  
        不是自定义控件: android.view.TextView  
        无论是系统控件还是自定义控件,最终通过反射实例化  
        return view;  
    }  
    根布局.addView(view);  
}  

3.2 Factory2的分析

从3.1的分析中有这样一个疑问:为什么会有tryCreateView()这个方法呢,直接走onCreateView()创建View不就行了吗?

因为android为开发者提供了一个可以自行决定如何创建View的接口,即LayoutInflater.Factory2,在tryCreateView()中当mFactory不为空的时候就会走Factory中的onCreateView()方法,开发者重写onCreateView()方法就可以决定View的创建方式,以及创建什么样的View

通过查看Activity源码我们可以发现Activity是实现了LayoutInflater.Factory2接口的,所以在SkinActivity中可以直接重写onCreateView()方法。

activity_factory2_16

不过重写onCreateView()还不够,还需要通过:

LayoutInflater.from(this).setFactory2(this);

将自己实现的LayoutInflater.Factory2接口设置给LayoutInflater才能让自己的onCreateView()方法代替系统的onCreateView()方法。

需要注意的是setFactory2()方法需要写在super.onCreate(saveInstanceState)之前:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        //我们要抢先一步,比系统还有早,拿到主动权
        LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), this);
        super.onCreate(savedInstanceState);
    }

因为ActivityonCreate()方法中会取设置一遍factory:

install_factory17

install_factory18

可见,如果我们提前设置了factory,系统就不会再设置了。

set_factory19

set_factory20

可以看到368行有一个mFactorySet标记,这个标记的初始值为false,一旦设置过一次factory,这个标记会在374行设置为true,下次再设置时,会在369行抛出异常。所以要在Activitysuper.onCreate()方法之前调用setFactory()方法。

这就解释了2.2.3中为什么要在onCreate()方法中提前设置工厂,并且为什么要复写onCreateView()方法,而且在该方法中可以替换原生控件为自定义控件了。

本文参考:
android暗黑模式学习记录

nestaticskinpeeler's People

Contributors

tianyalu avatar

Watchers

James Cloos avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.