View的绘制(4)-换肤框架(一)之Support v7库解析

作者: ZJ_Rocky | 来源:发表于2017-09-10 14:22 被阅读289次

    主目录见:Android高级进阶知识(这是总目录索引)

    一.目标

    秉承一贯的原则,不以目的为出发点的源码解析都是耍流氓。所以我们来说明下今天的目的:
    1)复习《setContentView源码分析》的知识点。
    2)为下一篇的换肤框架打一个基础.
    3)当然也是为《小红书的效果》做一个铺垫.

    二.support v7库解析

    1.回顾setContentView的一个知识点

    为了省去大家去查找前面文章的麻烦(当然我建议有时间还是可以去看下《setContentView源码分析》这一定不会浪费你时间),我这个地方重新贴一遍这一段关键代码,这也是原则,重要的知识点提醒!提醒!提醒!

      View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) {
    //此处省略一些代码,减少眼疲劳
    ............
                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 (InflateException e) {
                throw e;
    
            } catch (ClassNotFoundException e) {
    ..........
            }
        }
    

    这个地方我们看到下面这句代码 :

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

    这句代码非常关键,也是我们接下来换肤框架要用到的知识点,我们看到这个地方创建view的时候会先用factory来创建即这边的mFactory2,mFactory,这两个东西是什么呢?这两个其实是Factory的子类,LayoutInflater其中有个方法:

    public void setFactory(Factory factory) {
    }
    

    这个方法可以设置factory,也就是说只要我们设置进来我们自己的factory,那么系统就会用我们的factory来创建view,这也就达到拦截view创建过程的作用。Fantastic Resource Code!!!

    fatastic

    2.AppCompatActivity onCreate

    我们知道我们现在创建Activity的时候会继承兼容包里面的AppCompatActivity来达到使用高版本控件的能力。那我们的源码之旅就从onCreate开始。

     protected void onCreate(@Nullable Bundle savedInstanceState) {
     //我们看到接下来这三句,主要就是得到delegate,然后执行delegate里面的onCreate方法
            final AppCompatDelegate delegate = getDelegate();
            delegate.installViewFactory();
            delegate.onCreate(savedInstanceState);
            if (delegate.applyDayNight() && mThemeId != 0) {
                // If DayNight has been applied, we need to re-apply the theme for
                // the changes to take effect. On API 23+, we should bypass
                // setTheme(), which will no-op if the theme ID is identical to the
                // current theme ID.
                if (Build.VERSION.SDK_INT >= 23) {
                    onApplyThemeResource(getTheme(), mThemeId, false);
                } else {
                    setTheme(mThemeId);
                }
            }
            super.onCreate(savedInstanceState);
        }
    

    看到这个方法,我们看到第一句getDelegate()方法,后面的操作都是在这个delegate对象里面,那么我们看看这个delegate到底是哪里的孙猴子=》列车直通花果山:

        @NonNull
        public AppCompatDelegate getDelegate() {
            if (mDelegate == null) {
                mDelegate = AppCompatDelegate.create(this, this);
            }
            return mDelegate;
        }
    

    so easy!有没有,就是调用的AppCompatDelegate的create方法,顺其代码直接进入AppCompatDelegate的这个方法:

       private static AppCompatDelegate create(Context context, Window window,
                AppCompatCallback callback) {
            final int sdk = Build.VERSION.SDK_INT;
            if (BuildCompat.isAtLeastN()) {
                return new AppCompatDelegateImplN(context, window, callback);
            } else if (sdk >= 23) {
                return new AppCompatDelegateImplV23(context, window, callback);
            } else if (sdk >= 14) {
                return new AppCompatDelegateImplV14(context, window, callback);
            } else if (sdk >= 11) {
                return new AppCompatDelegateImplV11(context, window, callback);
            } else {
                return new AppCompatDelegateImplV9(context, window, callback);
            }
        }
    

    我去。。。。童话里都是骗人的,刚说的so easy呢?这里竟然不同版本返回的还是不一样,但是这有个有趣的地方,待我给大家揭露:

    class AppCompatDelegateImplN extends AppCompatDelegateImplV23 {
    }
    class AppCompatDelegateImplV23 extends AppCompatDelegateImplV14 {
    }
    class AppCompatDelegateImplV14 extends AppCompatDelegateImplV11 {
    }
    class AppCompatDelegateImplV11 extends AppCompatDelegateImplV9 {
    }
    class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
            implements MenuBuilder.Callback, LayoutInflaterFactory {
    }
    

    这里看到了没有,各个的类其实最终都是从AppCompatDelegateImplV9 出来的,那为什么要弄出几个呢?其实只是为了兼容后面版本一些夜间模式功能而已,所以这里返回的对象我们就当做AppCompatDelegateImplV9 。

    3.delegate installViewFactory

    我们到这里已经找到我们delegate(AppCompatDelegateImplV9 )了,所以我们直接进入AppCompatDelegateImplV9 的installViewFactory()方法:

       @Override
        public void installViewFactory() {
            LayoutInflater layoutInflater = LayoutInflater.from(mContext);
            if (layoutInflater.getFactory() == null) {
                LayoutInflaterCompat.setFactory(layoutInflater, this);
            } else {
                if (!(LayoutInflaterCompat.getFactory(layoutInflater)
                        instanceof AppCompatDelegateImplV9)) {
                    Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                            + " so we can not install AppCompat's");
                }
            }
        }
    

    其实这句代码也很简单有没有,就是得到layoutInflater对象,然后判断factory存不存在,不存在则将factory设置成AppCompatDelegateImplV9。这个地方是不是和开头我们说的想吻合了。我们通过设置factory来拦截创建view的过程。

    4.delegate onCreateView

    既然已经将我们的factory设置进去了,那我们知道创建view的时候会调用onCreateView,这也是我们开篇说过的,所以我们这里进入AppCompatDelegateImplV9这个方法看有什么不同:

       @Override
        public final View onCreateView(View parent, String name,
                Context context, AttributeSet attrs) {
            // First let the Activity's Factory try and inflate the view
            final View view = callActivityOnCreateView(parent, name, context, attrs);
            if (view != null) {
                return view;
            }
    
            // If the Factory didn't handle it, let our createView() method try
            return createView(parent, name, context, attrs);
        }
    

    其实从英文注释也可以看出,首先会调用Activity的onCreateView进行尝试创建View,如果没有创建成功则调用我们的createView(),我们知道,如果用到高级控件的话,有些属性是低版本没有的所以如果用到低版本没有的属性的话那么肯定会创建失败即这里会调用到我们的createView()方法。

        @Override
        public View createView(View parent, final String name, @NonNull Context context,
                @NonNull AttributeSet attrs) {
            final boolean isPre21 = Build.VERSION.SDK_INT < 21;
    
            if (mAppCompatViewInflater == null) {
                mAppCompatViewInflater = new AppCompatViewInflater();
            }
    
            // We only want the View to inherit its context if we're running pre-v21
            final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
    
            return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                    isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
                    true, /* Read read app:theme as a fallback at all times for legacy reasons */
                    VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
            );
        }
    

    这个方法其实没有什么内容就是创建一个mAppCompatViewInflater 对象,然后调用他的createView()方法,所以我们直接跳到这个方法:

     public final View createView(View parent, final String name, @NonNull Context context,
                @NonNull AttributeSet attrs, boolean inheritContext,
                boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
            final Context originalContext = context;
    
            // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
            // by using the parent's context
            if (inheritContext && parent != null) {
                context = parent.getContext();
            }
            if (readAndroidTheme || readAppTheme) {
                // We then apply the theme on the context, if specified
                context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
            }
            if (wrapContext) {
                context = TintContextWrapper.wrap(context);
            }
    
            View view = null;
    //关键代码看这里,这下面的控件就是我们要兼容的控件,也是support V7要拦截的view
            // We need to 'inject' our tint aware Views in place of the standard framework versions
            switch (name) {
                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;
                case "Spinner":
                    view = new AppCompatSpinner(context, attrs);
                    break;
                case "ImageButton":
                    view = new AppCompatImageButton(context, attrs);
                    break;
                case "CheckBox":
                    view = new AppCompatCheckBox(context, attrs);
                    break;
                case "RadioButton":
                    view = new AppCompatRadioButton(context, attrs);
                    break;
                case "CheckedTextView":
                    view = new AppCompatCheckedTextView(context, attrs);
                    break;
                case "AutoCompleteTextView":
                    view = new AppCompatAutoCompleteTextView(context, attrs);
                    break;
                case "MultiAutoCompleteTextView":
                    view = new AppCompatMultiAutoCompleteTextView(context, attrs);
                    break;
                case "RatingBar":
                    view = new AppCompatRatingBar(context, attrs);
                    break;
                case "SeekBar":
                    view = new AppCompatSeekBar(context, attrs);
                    break;
            }
    
    //当然如果不是上面的控件则会尝试用其他方法创建view
            if (view == null && originalContext != context) {
                // If the original context does not equal our themed context, then we need to manually
                // inflate it using the name so that android:theme takes effect.
                view = createViewFromTag(context, name, attrs);
            }
    
            if (view != null) {
                // If we have created a view, check it's android:onClick
                checkOnClickListener(view, attrs);
            }
    
            return view;
        }
    

    这个方法很长,但是其实很简单,就是根据name(这个name就是我们控件的名字),如果发现name对应的控件则new出对应的控件。我们这个地方挑一个控件来看,那就挑AppCompatTextView吧。

    5.AppCompatTextView

    到这里我们的讲解就快完成了哈,大家坚持一下,马上就见到曙光了。

    坚持.png

    我们直接打开AppCompatTextView类,然后看到构造方法,我们都知道,自定义有好几个构造函数,但是一个参数的构造函数会调用两个参数的构造函数,两个参数的构造函数会调用三个参数的构造函数,所以我们看三个参数的构造函数。

        public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
    
            mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
            mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
    
            mTextHelper = AppCompatTextHelper.create(this);
            mTextHelper.loadFromAttributes(attrs, defStyleAttr);
            mTextHelper.applyCompoundDrawablesTints();
        }
    

    这个方法其实也很简单,我们可以看到主要有AppCompatBackgroundHelper和AppCompatTextHelper,那这两个是干什么的呢?我们先来看AppCompatBackgroundHelper:

    5.1.AppCompatBackgroundHelper loadFromAttributes

    我们看到这个方法里面做了啥:

        void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
            TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
                    R.styleable.ViewBackgroundHelper, defStyleAttr, 0);
            try {
                if (a.hasValue(R.styleable.ViewBackgroundHelper_android_background)) {
                    mBackgroundResId = a.getResourceId(
                            R.styleable.ViewBackgroundHelper_android_background, -1);
                    ColorStateList tint = mDrawableManager
                            .getTintList(mView.getContext(), mBackgroundResId);
                    if (tint != null) {
                        setInternalBackgroundTint(tint);
                    }
                }
                if (a.hasValue(R.styleable.ViewBackgroundHelper_backgroundTint)) {
                    ViewCompat.setBackgroundTintList(mView,
                            a.getColorStateList(R.styleable.ViewBackgroundHelper_backgroundTint));
                }
                if (a.hasValue(R.styleable.ViewBackgroundHelper_backgroundTintMode)) {
                    ViewCompat.setBackgroundTintMode(mView,
                            DrawableUtils.parseTintMode(
                                    a.getInt(R.styleable.ViewBackgroundHelper_backgroundTintMode, -1),
                                    null));
                }
            } finally {
                a.recycle();
            }
        }
    

    看到这个是不是很熟悉,其实就是得到背景,背景着色器等等属性,然后进行设置进TextView。

    5.1.AppCompatTextHelper loadFromAttributes

    这是方法其实也是获取对应属性的值,然后分别进行设置,由于这个地方属性太多了,代码也会比较多所以我就贴出部分代码,因为这不是重点,主要给大家一个思路,可以通过这种方式来自定义属性:

      void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
            final Context context = mView.getContext();
            final AppCompatDrawableManager drawableManager = AppCompatDrawableManager.get();
    
            // First read the TextAppearance style id
            TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
                    R.styleable.AppCompatTextHelper, defStyleAttr, 0);
            final int ap = a.getResourceId(R.styleable.AppCompatTextHelper_android_textAppearance, -1);
            // Now read the compound drawable and grab any tints
            if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableLeft)) {
                mDrawableLeftTint = createTintInfo(context, drawableManager,
                        a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableLeft, 0));
            }
            if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableTop)) {
                mDrawableTopTint = createTintInfo(context, drawableManager,
                        a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableTop, 0));
            }
            if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableRight)) {
                mDrawableRightTint = createTintInfo(context, drawableManager,
                        a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableRight, 0));
            }
            if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableBottom)) {
                mDrawableBottomTint = createTintInfo(context, drawableManager,
                        a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableBottom, 0));
            }
            a.recycle();
    //底下省略部分代码,跟上面这段类似
    ...........
        }
    

    所以我们知道其实AppCompatTextView这个控件也好,还是其他控件也好,这个地方就是获取我们自定义的属性然后进行设置。这样我们的TextView就有了一些高级属性,达到了自定义的作用.到这里我们的源码分析就已经完成了,希望大家有enjoy这段旅程。

    enjoy-more
    总结:我们的support v7库就是设置LayoutInflater的Factory然后拦截onCreateView方法来创建View,原理特别简单,但是这是一个很有用的技能。。。

    相关文章

      网友评论

      本文标题:View的绘制(4)-换肤框架(一)之Support v7库解析

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