View默认的LayoutParams是何时生成的,默认值是什么。layout_width和layout_height属性在哪里生效

简介: View默认的LayoutParams是何时生成的,默认值是什么。layout_width和layout_height属性在哪里生效

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

相关文章
|
8月前
|
Android开发
Android 中setMargins和setPadding的区别
Android 中setMargins和setPadding的区别
59 0
|
Android开发
Android 动态修改layout_weight
Android 动态修改layout_weight
637 0
Android 动态修改layout_weight
|
机器学习/深度学习 数据建模 vr&ar
为什么都是ViewGroup的LayoutParams,也会报cannot be cast to android.view.ViewGroup$MarginLayoutParams?
为什么都是ViewGroup的LayoutParams,也会报cannot be cast to android.view.ViewGroup$MarginLayoutParams?
335 0
为什么relativelayout.layoutParams的width为-1
源码里看下就知道了。。 -1不代表宽度,代表MATCH_PARENT常量的值public static final int FILL_PARENT = -1;public static final int MATCH_PARENT = -1;public static final int WRA...
965 0
|
Android开发 Windows
android布局 - fill_parent/match_paren/wrap_content的区别
三个属性都用来适应视图的水平或垂直大小,一个以视图的内容或尺寸为基础的布局比精确地指定视图范围更加方便。 1)fill_parent 设置一个构件的布局为fill_parent将强制性地使构件扩展,以填充布局单元内尽可能多的空间。
823 0