美文网首页源码原理知识点
View默认的LayoutParams是何时生成的,默认值是什么

View默认的LayoutParams是何时生成的,默认值是什么

作者: tinyvampirepudg | 来源:发表于2020-05-27 19:44 被阅读0次

    View默认的LayoutParams是何时生成的,默认值是什么

    View#mLayoutParams属性:

    /**
     * The layout parameters associated with this view and used by the parent
     * {@link android.view.ViewGroup} to determine how this view should be
     * laid out.
     * {@hide}
     */
    protected ViewGroup.LayoutParams mLayoutParams;
    

    它唯一的可以修改的地方是View#setLayoutParams(ViewGroup.LayoutParams params)方法.

    如果我们不手动给View设置ViewGroup.LayoutParams属性,那它会有默认的值么?答案是有的。

    添加View的两种方式

    添加View一般有两种方式,一种是xml中添加,我们再通过View#findViewById()获取View;另一种是通过ViewGroup#addView()的一系列重载方法来添加。

    xml添加

    xml添加代码,一种是直接写到activity的xml布局文件中,通过Activity#setContentView()方法设置布局文件;另一种是将某个xml文件通过LayoutInflater#inflate方法解析成View,我们给Fragment设置布局文件或者自定义View时用的就是这种方式。

    需说明的是,我们常用的View.inflate(Context context, int resource, ViewGroup root)方法,内部也是调用的LayoutInflater#inflate(int resource, ViewGroup root, boolean attachToRoot)方法。

    LayoutInflater#inflate方法

    我们先看下LayoutInflater#inflate方法:

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

    这里主要分两步走,第一步根据布局文件生成XmlResourceParser对象,第二步调用inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法把parser对象转换成View对象。

    接着看inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法,简单起见,删除了不必要的代码:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            
        View result = root;
    
        // Look for the root node.
        int type;
        // 寻找根节点
        while ((type = parser.next()) != XmlPullParser.START_TAG &&
                type != XmlPullParser.END_DOCUMENT) {
            // Empty
        }
    
        final String name = parser.getName();
        if (TAG_MERGE.equals(name)) {
            rInflate(parser, root, inflaterContext, attrs, false);
        } else {
            // 1
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);
            ViewGroup.LayoutParams params = null;
            // 2、3
            if (root != null) {
                // Create layout params that match root, if supplied
                params = root.generateLayoutParams(attrs);
                if (!attachToRoot) {
                    temp.setLayoutParams(params);
                }
            }
    
            // Inflate all children under temp against its context.
            // 4
            rInflateChildren(parser, temp, attrs, true);
    
            if (root != null && attachToRoot) {
                root.addView(temp, params);
            }
            if (root == null || !attachToRoot) {
                result = temp;
            }
        }
        // 5
        return result;
    }
    

    这个方法很明确,穿入参数XmlPullParser和ViewGroup对象root(可为空),然后返回一个创建好的View。我们的任务是找到给新创建的View设置LayoutParams的地方。

    我们只看我们关心的逻辑:

    1、先通过createViewFromTag方法创建一个根View对象temp出来
    2、如果root不为空,就通过root.generateLayoutParams(attrs)方法将temp的width和height属性转化成LayoutParams设置给temp。
    3、如果root为空,表示temp的父布局不确定,这里也没有必要给设置LayoutParams了,等到它添加进别的布局时,就会设置LayoutParams参数了。
    4、通过rInflateChildren方法,将temp的子View都添加进来
    5、返回根view(temp是必定包含在根view中的)

    接下来我们看下添加子View的rInflateChildren方法,它最终会调用到rInflate方法,老规矩,删除无关代码,只看关心的:

    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    
        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;
    
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
    
            final String name = parser.getName();
    
            if (TAG_REQUEST_FOCUS.equals(name)) {
                ...
            } else {
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }
    
        if (finishInflate) {
            parent.onFinishInflate();
        }
    }
    

    1、开启while循环,根据获取到的属性,调用createViewFromTag方法生成View。createViewFromTag方法里面会通过反射,调用包含两个参数的构造器(形如View(Context context, @Nullable AttributeSet attrs))生成View对象。

    2、通过ViewGroup#generateLayoutParams方法获取子View对应的attrs里面的宽高,也就是我们在布局中给View设置的android:layout_widthandroid:layout_height属性。根据这个宽高生成对应的LayoutParams参数,接着将view添加给对应的parent,添加过程中会将这个LayoutParams参数设置给生成的View对象(后面会讲解)。

    3、在添加View之前,会递归调用rInflateChildren方法,完成当前View的子View的添加。

    需要说明的是,这里的采用的是深度优先遍历的方式进行的创建。

    我们再重点看下ViewGroup#generateLayoutParams方法是如何将子View的宽高生成LayoutParams参数的。

    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }
    

    它调用了ViewGroup的内部类LayoutParams的构造方法,我们接着看:

    public LayoutParams(Context c, AttributeSet attrs) {
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
        setBaseAttributes(a,
                R.styleable.ViewGroup_Layout_layout_width,
                R.styleable.ViewGroup_Layout_layout_height);
        a.recycle();
    }
    

    这里通过Contextattrs获取R.styleable.ViewGroup_Layout属性集合,接着通过setBaseAttributes方法读取资源文件中的layout_widthlayout_height属性,接着设置给LayoutParamswidthheight属性。具体如下:

    /**
     * Extracts the layout parameters from the supplied attributes.
     *
     * @param a the style attributes to extract the parameters from
     * @param widthAttr the identifier of the width attribute
     * @param heightAttr the identifier of the height attribute
     */
    protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
        width = a.getLayoutDimension(widthAttr, "layout_width");
        height = a.getLayoutDimension(heightAttr, "layout_height");
    }
    

    setBaseAttributes方法将布局文件中的layout_widthlayout_height属性值分别赋值给了LayoutParamswidthheight属性,这样就完成了子View对应的LayoutParams的构建。

    好了,通过LayoutInflater#innflatexml转换成View的流程我们分析完了,每个子View在创建时都会设置LayoutParams属性,并且该属性都来源与子View的width和height属性。

    Activity#setContentView方法

    接下来我们研究下Activity#setContentView方法设置的xml,是如何转化成View对象的?转化过程中是如何添加LayoutParams属性的。

    Activity#setContentView源码如下:

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
    

    这里的getWindow()的具体实现是PhoneWindow,我们看下PhoneWindow#setContentView(int layoutResID)的实现:

    public void setContentView(int layoutResID) {
        ...
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ...
    }
    

    可以看到最终还是调用了LayoutInflater#inflate方法将xml解析成View,并添加进到mContentParent中。LayoutInflater#inflate的具体实现可以参照上面的分析。

    ViewGroup#addView()

    我们看下ViewGroup#addView的几个重载方法:

    addView(View child)
    addView(View child, int index)
    addView(View child, int width, int height)
    addView(View child, LayoutParams params)
    addView(View child, int index, LayoutParams params)
    

    具体可以两类,一类是入参里面包含LayoutParams参数的,一类是不包含的。

    入参包含LayoutParams的方法直接将LayoutParams设置给view即可;入参不包含LayoutParams需要生成一个默认的LayoutParams,这里以addView(View child, int index)方法为例,我们看下它的实现:

    public void addView(View child, int index) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams();
            if (params == null) {
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }
    

    可以看出,如果view没有设置过LayoutParams,就通过generateDefaultLayoutParams()方法生成一个,我们看下默认生成的LayoutParams是什么样的:

    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }
    

    可以看出,默认的LayoutParams中宽高给的都是wrap_content

    总结

    通过上面的分析,可以得出结论:
    1、通过xml布局文件生成的View对象,会默认添加LayoutParams属性,它的属性值主要来源于子布局的widthheight属性。
    2、通过ViewGroup#addView()方法添加的View,如果View没有LayoutParams属性,默认会给添加LayoutParams属性,它的属性值默认都是wrap_content

    相关文章

      网友评论

        本文标题:View默认的LayoutParams是何时生成的,默认值是什么

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