源码原理知识点

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

2020-05-27  本文已影响0人  tinyvampirepudg

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

上一篇下一篇

猜你喜欢

热点阅读