Android 擴展-ViewGroup的子View真正實現Margin屬性

??樓主最近在復習自定義View,在復習到自定義ViewGroup這個知識點時,發現了一個問題--就是我們之前的定義ViewGroup在考慮Margin屬性可能有問題。本文在解決該問題給出建議性的意見,但是不一定是正確的,如果有錯誤或者不當的地方,希望指正。
??本文參考文章:
??1.Android 手把手教您自定義ViewGroup(一)
??2.你的自定義View是否真的支持Margin

1.提出問題

??這里我舉一個簡單的例子來說,假設我們需要定義一個ViewGroup放置一個子View,同時這個子View支持Padding和Margin屬性。
??這里我先貼出一個常規的寫法:

public class CustomViewGroup02 extends ViewGroup {

    public CustomViewGroup02(Context context) {
        super(context);
    }

    public CustomViewGroup02(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomViewGroup02(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //這里假設只有一個子View
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        View view = getChildAt(0);
        MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
        int width = view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + getPaddingLeft() + getPaddingRight();
        int height = view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin + getPaddingTop() + getPaddingBottom();

        setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize:width, (heightMode == MeasureSpec.EXACTLY) ? heightSize:height);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        View view = getChildAt(0);

        MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
        int left = getPaddingLeft() + lp.leftMargin;
        int top = getPaddingTop() + lp.topMargin;
        view.layout(left, top, left + view.getMeasuredWidth(), top + view.getMeasuredHeight());

    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

}

??在代碼中,我們考慮到了padding屬性和Margin屬性,同時我們可以在xml代碼測試一下效果
??xml中這樣寫:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.apple.android_demo08.CustomViewGroup02
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginLeft="20dp"
            android:background="#FFDAB9" />
    </com.example.apple.android_demo08.CustomViewGroup02>
</android.support.constraint.ConstraintLayout>

??模擬器上展示的效果圖:



??看上去似乎是沒有問題的,我們給TextView設置了marginLeft為20dp,在手機上也能正常顯示出來margin屬性。但是,如果TextView的layout_width設置為match_parent會怎么樣呢?
??xml代碼:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.apple.android_demo08.CustomViewGroup02
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_marginLeft="20dp"
            android:background="#FFDAB9" />
    </com.example.apple.android_demo08.CustomViewGroup02>
</android.support.constraint.ConstraintLayout>

??此時我們在Android studio右側的預覽界面來看看此時效果:



??我們發現雖然TextVeiw向左移動了20dp,但是我們發現了一個問題,就是TextView右側超出了屏幕,也就是說,TextView的layout_marginLeft 屬性根本沒有影響到它的width,只是單純將TextView向右移動了20dp。這個是有問題的,我們去看看系統的LinearLayout布局,margin屬性會影響View的寬和高的。從而得知,我們這里支持的Margin屬性是假的!那怎么才能真正的支持Margin屬性呢?

2.解決問題

??要想解決問題,必須先知道問題出現在哪里。這個問題就出現在onMeasure方法中measureChildren方法。
??我們先來看看measureChildren方法的源碼:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

??這個方法表達的意思非常簡單,就是循環測量每個子View。然后我們再來看看measureChild方法:

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

??在measureChild方法里面,先利用父布局的XXXXMeasureSpec、padding值和子View向父布局申請的大小來生成子View的寬和高。這里我們就看出問題了,我們發現系統在測量子View的width和height時,只是考慮了padding的影響,沒有考慮Margin對View的width和height的影響。
??看到這里,我們明白了,為什么之前我們給TextView設置了marginLeft,同時設置TextView的layout_width為match_parent時,TextView只是單純的向右移動了,而沒有調整TextView的大小。因為我們通過measureChild方法來測量每個子View是不會考慮Margin屬性對View的大小的影響。
??知道的問題所在,解決問題就非常的容易。解決的問題的辦法就是重寫measureChildren方法,在測量每個View時,考慮到margin的影響。其實在ViewGroup還有一個方法那就是measureChildWidthMargins方法,這個方法測量每個View時,考慮到了每個View的margin屬性的影響。我們來看看measureChildWidthMargins方法的源代碼:

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

??我們發現在這個方法里面,將Margin屬性的影響也考慮到的。那么我們就來重寫measureChildren方法:

    @Override
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View view = getChildAt(i);
            if (view != null && view.getVisibility() != GONE){
                measureChildWithMargins(view, widthMeasureSpec, 0, heightMeasureSpec, 0);
            }
        }
    }

??在這個重寫的代碼中,我們需要主要兩點:

??1.在原來的measureChildren方法的if判斷條件是:(child.mViewFlags & VISIBILITY_MASK) != GONE,而我們這里是:view != null && view.getVisibility() != GONE。我們這里的依據是LinearLayout,系統的LinearLayout也重寫了measureChildren方法的,它的判斷條件就是:view != null && view.getVisibility() != GONE。
??2.measureChildrenWithMargins方法多出兩個參數,分別是:widthUsed,heightUsed,這里傳入的是兩個0,這里的依據還是LinearLayout,LinearLayout調用measureChildrenWithMargins傳入就是兩個0。

??重寫之后,我們來看看之前的match_parent的情況(記得Rebuild一下工程):



??這下就變得正常得多了!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容