??樓主最近在復習自定義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一下工程):
??這下就變得正常得多了!