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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章