由AppCompat主题中设置的colorAccent颜色对CheckBox无效问题想到的 - 浅析LayoutInflater

最近项目中遇到一个奇怪的问题,我在同一个界面里有两个CheckBox,xml文件写的一模一样,都是使用系统默认的样式,没有改button或drawable,但是最后出来的效果居然不一样。


其实再仔细一看不止颜色不一样,未选中的样式,触摸效果都不太一样,当时瞬间就懵逼了

这是怎么回事,显然靠右那个没有带文字的CheckBox才是正常的,而RecyclerView中的不正常,虽然我觉得这个不正常的反而更好看一些呢......

log打印一下这两个CheckBox吧,结果发现

我的天呐,我分明使用的AppCompat主题,怎么出现了一个不是AppCompatCheckBoX呢,在AppCompatCheckBox的注释中分明可以看到

* This will automatically be used when you use {@link CheckBox} in your layouts.

意思是只要我开启AppCompat主题,使用CheckBox就会自动给我转成AppCompatCheckBox了,为啥没其效果?后来经过好几天不吃不喝的(弥天大雾)的研究后,我才猛的发现,虽然这两个checkBox的xml一模一样,但是获取对象的方式还是不一样的,RecyclerView中的是通过LayoutInflater获取的对象,正常的那个是findViewById获取的,果然问题出在了LayoutInflater上,冒着可能被虐到死的觉悟,我还是打开了LayoutInflater的源码一探究竟。

那么直接从inflate(int resource, ViewGroup root, boolean attachToRoot)方法看起

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

代码比较简单,通过context创建了xml布局解析器,然后调用了inflate(parser, root, attachToRoot)方法,接着看往下看

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();
                
                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // 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);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // 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) {
                InflateException ex = new InflateException(e.getMessage());
                ex.initCause(e);
                throw ex;
            } catch (Exception e) {
                InflateException ex = new InflateException(
                        parser.getPositionDescription()
                                + ": " + e.getMessage());
                ex.initCause(e);
                throw ex;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);

            return result;
        }
    }

查看一下这个方法我简直是要放弃了(>﹏<),这么长......不过还好,我们很明显的会发现42行其中有这么一句final View temp = createViewFromTag(root, name, inflaterContext, attrs),很明显,view是在这里创建的,立刻转移到这里去

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

        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        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) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name);
            ie.initCause(e);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name);
            ie.initCause(e);
            throw ie;
        }
    }

在我们研究这个方法里面都做了什么之前请先看一下18行

// Let's party like it's 1995!

这是什么鬼,这是个什么梗么,谁来告诉我,这是啥

好了还是回来看这个方法吧,这个方法还好,这里会判断有没有设置Factory,会先尝试用Factory来创建view恩?这个Factory是个什么鬼?看了一眼代码它长这样

    public interface Factory {
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

恩?一个接口,里面就一个方法,我自己创建的LayoutInflater没有设置这个Factory肯定不会走这里了,那么在我们很明显的就会发现createViewFromTag里使用Factory没有创建出view时就会调用LayoutInflater的onCreateView去创建view了

隐隐的感觉问题就出在这了,这肯定就是导致为啥我的两个xml代码一模一样的CheckBox有一个不是AppCompatCheckBox,自然这个就没法响应AppCompat主题了,更不会对colorAccent中设置的颜色有反应

那么,第二问题来了,findViewById中获得的CheckBox对象会变成AppCompatCheckBox是因为在这过程中使用了Factory所以会这样么
答案是肯定的,查看AppCompatActivity会发现

 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
    }

在其onCreate过程中会调用一个AppCompatDelegate的installViewFactory(),打开后发现这是一个抽象方法

public abstract void installViewFactory();

这好办,找它的实现类,可以发现他有一个静态方法create

private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        final int sdk = Build.VERSION.SDK_INT;
        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 AppCompatDelegateImplV7(context, window, callback);
        }
    }

这里会根据不同的sdk版本返回不同的实现类,而AppCompatDelegateImplV23又是继承V14的,V14继承V11,V11继承V7,最终我们还是得跳到AppCompatDelegateImplV7里,打开其installViewFactory方法

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

哈哈,果然如此,我们再去看看他在FactoryonCreateView回调里做了什么

/**
     * From {@link android.support.v4.view.LayoutInflaterFactory}
     */
    @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);
    }

这里逻辑也很清楚,先调用callActivityOnCreateView创建view,如果没创建出来,再调用createView,先看callActivityOnCreateView

View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // Let the Activity's LayoutInflater.Factory try and handle it
        if (mOriginalWindowCallback instanceof LayoutInflater.Factory) {
            final View result = ((LayoutInflater.Factory) mOriginalWindowCallback)
                    .onCreateView(name, context, attrs);
            if (result != null) {
                return result;
            }
        }
        return null;
    }

这里逻辑依然很简单,会判断一个mOriginalWindowCallback的成员变量是不是LayoutInflater.Factory类型,如果不是就会返回null,那么上面的createView就会执行,那么这个mOriginalWindowCallback又是个什么鬼呢,我们再深入看一下它,先找到它的类型,发现它是

abstract class AppCompatDelegateImplBase extends AppCompatDelegate {

    final Context mContext;
    final Window mWindow;
    final Window.Callback mOriginalWindowCallback;
    final Window.Callback mAppCompatWindowCallback;

哦,在这里找到它了,那么看看他是怎么会实例化的

AppCompatDelegateImplBase(Context context, Window window, AppCompatCallback callback) {
        mContext = context;
        mWindow = window;
        mAppCompatCallback = callback;

        mOriginalWindowCallback = mWindow.getCallback();
        if (mOriginalWindowCallback instanceof AppCompatWindowCallbackBase) {
            throw new IllegalStateException(
                    "AppCompat has already installed itself into the Window");
        }
        mAppCompatWindowCallback = wrapWindowCallback(mOriginalWindowCallback);
        // Now install the new callback
        mWindow.setCallback(mAppCompatWindowCallback);
    }

这里一看就明白,主要的逻辑在wrapWindowCallback(mOriginalWindowCallback)这个方法中,我们接着看下去

Window.Callback wrapWindowCallback(Window.Callback callback) {
        return new AppCompatWindowCallbackBase(callback);
    }
class AppCompatWindowCallbackBase extends WindowCallbackWrapper {
        AppCompatWindowCallbackBase(Window.Callback callback) {
            super(callback);
        }

        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            return AppCompatDelegateImplBase.this.dispatchKeyEvent(event)
                    || super.dispatchKeyEvent(event);
        }
public class WindowCallbackWrapper implements Window.Callback {

    final Window.Callback mWrapped;

    public WindowCallbackWrapper(Window.Callback wrapped) {
        if (wrapped == null) {
            throw new IllegalArgumentException("Window callback may not be null");
        }
        mWrapped = wrapped;
    }

以上三段代码并不在同一处,这里为了方便一下全贴出来,可以看到最终通过这样的系列途径创建了一个callback,而AppCompatActivity最终继承自Activity,Activity实现了Window.Callback接口,最终可以说mOriginalWindowCallback应该就是AppCompatActivity对象,这里其实我也没搞太明白,纯属自己猜测,也懒得去翻官方的文档了,希望有懂的朋友指教一二

那我们再回到callActivityOnCreateView中去

View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // Let the Activity's LayoutInflater.Factory try and handle it
        if (mOriginalWindowCallback instanceof LayoutInflater.Factory) {
            final View result = ((LayoutInflater.Factory) mOriginalWindowCallback)
                    .onCreateView(name, context, attrs);
            if (result != null) {
                return result;
            }
        }
        return null;
    }

现在在回来看,这里会判断mOriginalWindowCallback instanceof LayoutInflater.Factory,那这句会不会是true呢?答案是肯定的,因为Activity同样实现了LayoutInflater.Factory这个接口,这样就会走到if里面去,并调用Activity的onCreateView回调来创建view

那么我们再来看一下Activity是怎么实现的onCreateView

@Nullable
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }

居然直接返回null,哈哈,我找了半天,进了这么多层他居然返回null,本宝宝不开森了(ToT)/~~~

既然他返回null,那么接下来代码就走到AppCompatDelegateImplV7的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 */
        );
    }

很显然这个方法里面创建了一个AppCompatViewInflater然后又跳到这个类的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;

        // 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;
        }

        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;
    }

哈哈,看到这是不是拨云见日真相大白了!

注意看那一堆switch case,我想到了这了就不用我再说了吧,至此,我们终于解开了为啥使用AppCompat主题,你创建的view就会变成AppCompat的view了,完全是通过LayoutInflaterFactory来办到的,不看源码还真是不理解这里面这么精妙

本文最后让我们来解决一下最初的问题,那怎么让自己用LayoutInflater创建出的CheckBox也能响应AppCompat主题变成AppCompatCheckBox呢,非常简单,在LayoutInflater.from(context)的时候传进去AppCompatActivity的对象就行了,而我之前传的是getApplicationContext()

最后,效果如下

感谢:http://blog.csdn.net/lmj623565791/article/details/51503977 鸿洋_ 大神

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容