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

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