View默认的LayoutParams是何时生成的,默认值是什么
View#mLayoutParams属性:
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_width
和android: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();
}
这里通过Context
和attrs
获取R.styleable.ViewGroup_Layout
属性集合,接着通过setBaseAttributes
方法读取
资源文件中的layout_width
和layout_height
属性,接着设置给LayoutParams
的width
和height
属性。具体如下:
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
setBaseAttributes
方法将布局文件中的layout_width
和layout_height
属性值分别赋值给了LayoutParams
的width
和height
属性,这样就完成了子View对应的LayoutParams
的构建。
好了,通过LayoutInflater#innflate
将xml转换成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
属性,它的属性值主要来源于子布局的width
和height
属性。
2、通过ViewGroup#addView()方法添加的View,如果View没有LayoutParams
属性,默认会给添加LayoutParams
属性,它的属性值默认都是wrap_content
。