美文网首页
一键换肤

一键换肤

作者: David_zhou | 来源:发表于2020-08-20 09:08 被阅读0次

常见的APP很多都要切换皮肤的功能,到了晚上,就换成黑色背景,另外用户也可以手动切换皮肤。一直好奇是怎么实现的,后面知道了大概原理后觉得挺有意思的。换肤思路有好几种,这里先看下通过Factory自己创建View的这种。大概流程就是接管系统创建view的过程,然后找出view颜色相关的属性,替换成皮肤包中的属性值,替换完成之后系统会自动重新进行界面的绘制。下面简单介绍下具体的实现原理,首先从View的绘制流程开始,以android28源码为基础。

View绘制原理

首先从Activity的setContentView()开始,一般都是在这里将布局文件设置进去,这个方法的代码如下:

// Activity.java
public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar(); // 初始化ActionBar
    }

getWindow()返回一个Window。这个类是抽象类,唯一的实现类是PhoneWindow,所以直接看PhoneWindow的setContentView方法即可。

// PhoneWindow.java
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
        if (mContentParent == null) { 
            installDecor();// 重点1 
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
                // 一般流程。重点2
        mLayoutInflater.inflate(layoutResID, mContentParent);
        
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback(); 
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();// activity可以监听到这个状态变化
        }
        mContentParentExplicitlySet = true;
    }

mContentParent初始化的为空,因此就直接会调用installDecor(),这个方法中会通过generateLayout()创建mContentParent。generateLayout() 也是通过 findViewById 的方式找到 R.id.content 这个Id对应的ViewGroup。接下来看重点2, inflate 这个方法会返回一个view,重点代码如下:

// LayoutInflater.java
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) { 
            View result = root;

            try {
                final String name = parser.getName();
                if (TAG_MERGE.equals(name)) {
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    // 创建view的地方,重点1  
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;
                    if (root != null) {
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    // Inflate all children under temp against its context.
                    // 遍历children,重点2
                    rInflateChildren(parser, temp, attrs, true);

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            } catch (XmlPullParserException e) {
            }
            return result;
        }
    }

我们先看重点方法createViewFromTag()这个方法的返回结果是View,我们跟进去看下源码:

// LayoutInflater.java
 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        try {
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }
            return view;
        } catch (Exception e) { 
        }
    }

这个方法首先会判断mFactory2这个值是否为空,如果不为空,则调用Factory2.onCreateView去创建View。如果mFactory2为空,则判断mFactory是否为空,如果不为空,则调用Factory.onCreateView()创建View。如果创建出来的View为空,则调用onCreateView或者createView创建,最后将View返回。Factory2和Factory是两个接口,代码如下:

// LayoutInflater.java
public interface Factory {
            public View onCreateView(String name, Context context, AttributeSet attrs);
    }
    
  public interface Factory2 extends Factory {
      public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}

mFactory2和mFactory这两个成员变量都是在LayoutInflater的构造函数中赋值,默认情况下都是为空,另外可以通过setFactory2()和setFactory()两个方法分别设置。因为默认情况下都是为空,所以这种情况下是使用onCreateView()或createView()来进行View的创建。

换肤思路

​ 此处就有一种换肤的思路,如果手动设置mFactory2或mFactory,然后自己实现onCreateView,那么就不会走系统的onCreateView()或createView()。也就是说我们可以通过自己设置mFactory2或mFactory,来接管View的创建。自己接管创建过程,就意味着View属性可以自定义。即View颜色相关的属性值可以从apk中获取,也可以从其他文件中获取。我们只是改变View颜色相关等属性,不会去改变View的创建流程,因此自己实现的onCreateView()实际上可以使用系统的onCreateView()或createView(),只是需要接管部分属性的赋值。

相关文章

  • 原来Android换肤如此简单

    这是一个Android换肤的库,代码量极少,支持换肤的情况还比较多,提供了以下功能: 无需重启,一键换肤效率高 支...

  • 一键换肤

    常见的APP很多都要切换皮肤的功能,到了晚上,就换成黑色背景,另外用户也可以手动切换皮肤。一直好奇是怎么实现的,后...

  • 让你的App,一键换肤吧

    此框架无需启动应用即可一键换肤(支持background,string ,color,dimen),使用简单只需4...

  • iOS - 换肤初体验

    简单的一个小思路 ..利用 [XX appearance]; 属性 代码 : 说明 :一键换肤 如果将所有控件...

  • ios 一键换肤

    首页感谢Draveness大神的开源!!本demo基于DKNightVersion的一键换主题,只用于学习。 以下...

  • vue 一键换肤

    该一键换肤只是定义好几个颜色,并进行简单的切换。 在src下的assets文件下面定义一个css文件夹,在对应的文...

  • vue 一键换肤

    转自 https://www.jianshu.com/p/53e4a1c0bd62 。。。 该一键换肤只是定义好几...

  • iOS Block 实现类似通知、一键换肤的功能

    需求: 比如我们一键换肤功能, 一键切换字体的时候, 这一类都属于执行一个动作,发出通知一样。 我们先自己定义一个...

  • sketch首款主题管理插件-主题大师

    首款 SKETCH 换肤及主题管理软件。(3+1)套主题选择,一键切换;多种图标库、让你sketch与众不同;个性...

  • 一键换肤框架分享

    这几天写了一个一键换肤的框架,刚传到github上。想分享一下使用方法很简单,把我上传的资源copy到自己的项目中...

网友评论

      本文标题:一键换肤

      本文链接:https://www.haomeiwen.com/subject/htxsjktx.html