美文网首页android
Android View 填充方式那么多,我到底该用哪一种?

Android View 填充方式那么多,我到底该用哪一种?

作者: maxcion | 来源:发表于2020-06-20 16:54 被阅读0次

    现有的方式

    var rootNull = LayoutInflater.from(this).inflate(R.layout.item, null, false)
    

    上面这段代码是使用了LayoutInflater进行填充,但是第二个参数root 设置为null, 第三个参数 attachRoot 设置为 false,从字面意思来看是:我要解析一个xml 布局, 我的root 是null,并且我不想把**给attach到 root 里面去

    var rootNotNull = LayoutInflater.from(this).inflate(R.layout.item, container, false)
    

    这是第二种填充方式,和上面差不多,只是参数不同,root 不为null了,但是 第三个参数还是false, 意思难道是:就算你root 不是null, 我也不想把**attach到root上?

    var rootNullNotAttach = LayoutInflater.from(this).inflate(R.layout.item, null, true)
    

    这是第三种填充方式, root 为null,但是attach 为ture, 意思是不是指: 不管你root 怎么样,反正我要把**attach 到root 上

    var rootNotNullAttach = LayoutInflater.from(this).inflate(R.layout.item, container, true)
    

    这是第四种填充方式,最喜欢这种完美参数调用(每个参数都不是null,也不是0,不是负数,不是false,都是积极向上的)。这个意思可能就是:刚好你root 不是null, 我也刚好想把**attch到你root上

    val fuckWay = View.inflate(this, R.layout.item, container)
    

    这是第五种填充方式,不用LayoutInflater, 我最终目的是要获取一个View, 我找你LayoutInflater干嘛,直接找View 不香吗?直接通过调用View的方法来解析xml。(这个方法看起来香,但实际运用起来还是有坑的)


    用哪一个.png

    此时此刻我只想说,你给我这么多,到底是让我用哪一个来解析布局啊,你搞得我好乱啊!!

    效果展示

    都说要“透过表象看本质”,我觉得他们说的对,但是这次我还是要通过表象去看本质,我们直接看效果。

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
        <RelativeLayout
            android:id="@+id/ll"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#f00" />
    
    </LinearLayout>
    

    这是我们MainActivity的布局文件,这个布局中id 是ll 的RelativeLayout将会被当做第二个参数root来使用

    <?xml version="1.0" encoding="utf-8"?>
    <Button xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="#ff0"/>
    

    这个布局的文件名是item.xml, 这个也就是最终我们要被解析的xml布局。

    第一种方式的 root为null ,attach 为false 的情况

    val container = findViewById<RelativeLayout>(R.id.ll)
    var rootNull = LayoutInflater.from(this).inflate(R.layout.item, null, false)
    

    一共两行代码,第一行是找到我们id为ll viewGroup, 第二行代码直接LayoutInflater.from(this).inflate(R.layout.item, null, false)将xml解析为view,这个方法返回了一个view,我们怎么将这个被解析出来的view 显示出来? 很简单直接通过addView 加到ViewGroup里面

    val container = findViewById<RelativeLayout>(R.id.ll)
    var rootNull = LayoutInflater.from(this).inflate(R.layout.item, null, false)
    container.addView(item)
    
    rootNullAttachFalse.png

    是的,你没看错,我也没有上传错效果图,上面三行代码的效果就是这样的,你的疑惑是,明明给Button设置的宽高是200dp,显示出来却不是,要是说是宽高写死了,在不同设备上显示不同还好,但是我特么明明要的是正方形的Button,你特么给我的确实长方形的。是不是有点说不过去了?就暂且当他是系统bug把,后面会说明为什么会出现这个原因。

    第二种方式 root 不为null , attach 为false

    val container = findViewById<RelativeLayout>(R.id.ll)
    var rootNotNull = LayoutInflater.from(this).inflate(R.layout.item, container, false)
    
    container.addView(rootNotNull)
    
    rootNotNull.png

    是不是感觉这次很完美,尺寸对了,形状也对了,是的这次很完美,但是有一点不好的就是,竟然让我们写了三行代码猜得到想要的结果,鉴于“能坐着绝不站着,能躺着绝不坐着”,这次只能中评。

    第三种方式 root 为null, attach true

    val container = findViewById<RelativeLayout>(R.id.ll)
    var rootNullNotAttach = LayoutInflater.from(this).inflate(R.layout.item, null, true)
    container.addView(rootNullNotAttach)
    
    rootNullAttachTrue.png

    是的你没看错,我保证我绝对没有贴错图片,效果就是和第一种是一样的。

    第四种 root 不为空, attach 为true

    val container = findViewById<RelativeLayout>(R.id.ll)
    var rootNotNullAttach = LayoutInflater.from(this).inflate(R.layout.item, container, true)
    container.addView(rootNotNullAttach)
    

    很遗憾这里,不是效果图,因为这样写会导致崩溃


    crash.png

    从奔溃日志上可以看到崩溃原因是 你的子View 已经有parent的,谷歌就不让你再给他找一个parent了,言外之意就是你已经把这个View 添加到一个ViewGroup里面了,不能再把他添加到其他ViewGroup里面。一共就三行代码,哪来的添加两次的逻辑?行吧,听你的都听你的,你说我重复添加了,但是我不添加了我把我addView 的代码删掉试试?

    val container = findViewById<RelativeLayout>(R.id.ll)
    var rootNotNullAttach = LayoutInflater.from(this).inflate(R.layout.item, container, true)
    
    rootNotNullAttach.png

    去掉之后就完美运行了。惊不惊喜意不意外? 其实起到影响的就是LayoutInflater.from(this).inflate()的第三个参数,true代表,我要把解析出来的view add到root 里面去, false 代表,解析出来的view 我暂时不打算add 到root 里面。

    好了现在LayoutInflater.from(this).inflate()三个参数,我们已经知道其中两个参数的作用了,第一个参数指定要被解析的xml布局,第三个参数attach 表示是否要把被解析出来的View add 到第二个参数root 里面去。 那么问题来了!! 第二个参数root 是干嘛的,我该怎么传入,不传行不行?

    我们android 现有的几大ViewGroup : LinearLayout、RelativeLayout、FrameLayout他们都有自己特有的布局属性比如:
    LinearLayout : android:layout_weight=""
    RelativeLayout: android:layout_above="" 等等,
    如果你的父布局是LinearLayout,你在子布局中设置了android:layout_above,那就是没有效果的,同样的如果你父布局是RelativeLayout,但是你给子布局设置了 android:layout_weight,同样也是没效果的, 那我们是不是可以大胆的猜想一下,子View的layout属性都是交给他的parent(父布局)来解析的呢?如果是的就能走通上面四种解析方式效果出乎我们意料的问题了。

    现在假设我们上面的猜想是对的,我们来总结一下

    1. LayoutInflater.from(this).inflate 的第一个参数是来指定被解析的xml布局
    2. LayoutInflater.from(this).inflate 的第二个参数是用来解析 待解析的xml布局根视图的layout_ 属性
    3. LayoutInflater.from(this).inflate 的第三个参数是用来设置是否要把解析出来的View 添加到root View 中。

    按照上面三个结论我们来反推上面四张效果图是否正确

    第一种 root 为null ,attach 为false

    root 为null ,也就是不解析xml布局 根视图的layout_属性,自然而然的 待解析布局中设置android:layout_width="200dp"和android:layout_height="200dp" 是无效的, 又因为第三个参数是false 代表不自动把解析出来的view add 到viewGroup里面,所以我们需用第三行代码把解析出来的view add到VuewGroup里面
    推理完美的与表象结合了有木有

    第二种 root 不为 null ,attach 为 false

    按照上面的推理来说就是, 要解析 待解析xml布局根视图的layout_属性, 但是不自动添加在root 中。
    有一次完美了有木有?尺寸正确,形状正确

    第三种 root 为null, attach 为true

    按照上面的结论来说就是 ,不解析 待解析xml布局跟视图的layout_属性,但是我要把解析出来的view add 到root 中(很不合理有木有,root都为null了,你还要往null里面add 东西? 所以 roor 为 null 的时候 ,第三个参数没有意义, 与false 效果一致)
    再次与上面的表象一致了

    第四种 root不为null, attach 为true

    意思就是: 我既要解析 待解析xml布局跟视图的layout_ 属性,我又要自动把解析出来的view add 到 root 中
    很好,完美的与上面四种表象都吻合了

    要不看看源码

    当然,我知道如果这样就想说服你们相信我我说的是对的,肯定是不可能的, 因为这就像,一个人和你说 凡是喝了白开水的人最后都死了,劝你不要喝白开水一样(白开水是真的香)

    我们先来看看LayouInflater.inflate(@LayoutRes int resource, @Nullable 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) + ")");
            }
    
            View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
            if (view != null) {
                return view;
            }
            XmlResourceParser parser = res.getLayout(resource);
            try {
                return inflate(parser, root, attachToRoot);
            } finally {
                parser.close();
            }
        }
    

    最终返回的结果时 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 {
                    advanceToRootNode(parser);
                    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) {
                    final InflateException ie = new InflateException(e.getMessage(), e);
                    ie.setStackTrace(EMPTY_STACK_TRACE);
                    throw ie;
                } catch (Exception e) {
                    final InflateException ie = new InflateException(
                            getParserStateDescription(inflaterContext, attrs)
                            + ": " + e.getMessage(), e);
                    ie.setStackTrace(EMPTY_STACK_TRACE);
                    throw ie;
                } finally {
                    // Don't retain static reference on context.
                    mConstructorArgs[0] = lastContext;
                    mConstructorArgs[1] = null;
    
                    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
                }
    
                return result;
            }
        }
    

    这段代码很长,虽然我很想删除一些不必要的代码,但是里面几乎每行代码都很有用

    • 第一处:这一处代码重要不重要?和今天的主体没有关系,但是和面试很有关系有没有? 只要面试面试个view 有关百分之七十都会问 你能给我解释一下<merge >这个标签吗?
      道理我都懂:merge 只能是根视图,并且LayoutInflater.infale()的第三个参数必须是true,因为代码上写的很清楚,你要是不符合就直接扔一个Exception给你(喜欢吗?)

    • 第二处: 首先我们从英文注释里面可以看到

    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    

    这行代码的作用是从xml中找到根布局

    上面的代码先截取一段

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

    这段代码是不是很清晰? 首先从xml 布局中找到跟视图, 然后创建一个null 的ViewGroup.LayoutParams, 如果root不为空(我们在调用LayoutInfalter.inflate()第二个参数root 不为空),他就干嘛了?干嘛了?干嘛了?

    // Create layout params that match root, if supplied
    params = root.generateLayoutParams(attrs);
    

    他去生成LayoutParams了,他去生成LayoutParams了,他去生成LayoutParams了!!!(是不是和我们上面的猜想吻合了)然后呢?然后呢?然后呢?

    然后他竟然去判断attachToRoot 是否是false 了, 如果是false,他就会把解析出来的layoutParams 设置给rootView(只是设置layoutParams,只是设置,不是add啊!注意了没有add),如果不是false 呢?如果不是false!!!

    if (root != null && attachToRoot) {
        root.addView(temp, params);
    }
    

    如果不是false而且root 是null,他就解析出来的view add 到root 里面了。

    总结一下

    通过源码的查看我们发现我们的猜想完全正确有没有?

    LayoutInflater.from(this).inflate(R.layout.item, null, true)
    

    第一个参数 指定 要解析那个xml 布局
    第二个参数 root 来确定要不要解析 待解析xml布局跟视图的layout_ 属性
    第三个参数 来确定是否要把解析出来的View add 到root 中。

    好吧其实上面文章已经结束了从中午12点一直敲到下午4点,没吃饭优点饿,就像结束文章了。但是文章里有一个瑕疵就是有一个地方我没解释,由于了半个小时,我决定先爬起来花半个小时把这个瑕疵给补上, 但是犒劳自己去吃顿肯德基。 晚上再去跑会步,惩罚自己一天没下床。

    瑕疵就是,文章开篇我们的猜想中有说过,第二个参数的作用是用来 确定是否要解析 待解析的xml布局中 layout-属性, 喜欢追根究底的朋友在这里肯定优点不舒服, “layout-属性”是什么意思,从没在别人那里停过这个词,别想用高大上的名字忽悠群众。 好的这里解释一下,layout-属性 就是只每个ViewGroup 里特有“layout_”开头的属性,在LinearLayout里面有 android:layout_weight="" 权重属性, 在RelativeLayout中有android:layout_above=""等等

    因为待解析xml布局的跟视图的layoutParams的生成是通过

    params = root.generateLayoutParams(attrs);
    

    这个,root 就是你传进来第二个参数。

    RelativeLayout.java
    
    
     @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new RelativeLayout.LayoutParams(getContext(), attrs);
        }
    
    LinearLayout.java
     @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new LinearLayout.LayoutParams(getContext(), attrs);
        }
    

    可以看到这个函数虽然每个ViewGroup都有,但是没有具体的实现类返回的结果是不一样的, 其中具体实现也是不一样的。

    问题

    1. 开篇说了五种解析方式: 但是我只说了4种,第五种View.inflate() 大家还是自己看吧, 内部其实调用还是LayoutInflater.infalte()

    2. 在使用RecyclerView 重写Adapter的onCreateViewHolder 函数时,到底应该使用哪种方式解析view。如果看懂了这篇文章就能知道为什么在创建ViewHolder 的时候不能使用View.infalte()创建而必须使用LayoutInflater.infalte()来解析了

    相关文章

      网友评论

        本文标题:Android View 填充方式那么多,我到底该用哪一种?

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