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_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
屬性。具體如下:
/**
* 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_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
。