簡書 編程之樂
轉載請注明原創出處!
復習自定義View過程中我發現幾乎 很多人 都犯了一個細節上的錯誤,就是ViewGroup中的子View 不支持margin。
注: 關于自定義View的基礎教程 請參閱其他博客
先總結兩點
- 自定義View在onDraw里面需要處理padding的影響,widthMeasureSpec和heightMeasureSpec是包含padding大小的。
- 子View的margin屬性是由ViewGroup處理的,ViewGroup在onMeasure和onLayout時一定要考慮 ViewGroup自己的padding和子View的margin的影響。
你可能遇到過下面這樣的錯誤。
java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
下面我們分析為什么會遇到這種錯誤以及解決方法。
你可能見過很多人在自定義ViewGroup的
onMeasure()
中使用
measureChildren(widthMeasureSpec, heightMeasureSpec);
來測量所有子View的尺寸。
ViewGroup.measureChildren的源碼如下:
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);
}
}
measureChild是不是不太合適呢,查閱了FrameLayout和LinearLayout等都沒有用過這個measureChildren呢,幾乎全部都重寫了,我們的自定義ViewGroup的measureChildren是不是應該是改成下面這樣才對。
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) {
// ******************* 注意這里 ********************
measureChildWithMargins(child, widthMeasureSpec, heightMeasureSpec);
}
}
你應該看到了區別,measureChild和measureChildWithMargins區別就是
測量child尺寸時,保證child的 最大可用尺寸,感覺這個with前綴起的不太好。
- measureChild減去了 ViewGroup的padding 保證child最大可用空間
- measureChildWithMargins減去了ViewGroup的padding和子View的margin 保證child最大可用空間
至于 measureChild和measureChildWithMargins中是如何**生成child的MeasureSpec,并最終調用child.measure() -- > child.onMeasure()的,這里就不貼源碼了。
總結 : ViewGroup中測量child一定要用measureChildWithMargins而不是measureChild
使用measureChildWithMargins后卻產生異常
終于改成measureChildWithMargins了,卻突然產生了異常,這是為什么?
找到異常產生的位置,追蹤到ViewGroup.addView()方法,源碼如下:
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);
}
異常信息是 ClassCastException
cannot be cast to android.view.ViewGroup$MarginLayoutParams
而addView中,如果child.getLayoutParams();獲取不到,則默認生成一個
generateDefaultLayoutParams();
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
這個默認生成的肯定不能強制轉換為MarginLayoutParams了。
再來看addView中的其他方法
private void addViewInner(View child, int index, LayoutParams params,boolean preventRequestLayout) {
if (!checkLayoutParams(params)) {
// **************** 注意這里 ****************
params = generateLayoutParams(params);
}
if (preventRequestLayout) {
child.mLayoutParams = params;
} else {
child.setLayoutParams(params);
}
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
................
................
}
里面還有檢測這個child的LayoutParams 是不是為空的,干脆全部重寫得了。
在你的自定義ViewGroup中加入如下代碼即可令 子View 的margin生效。
public class MyViewGroup extends ViewGroup {
// ..................... 其他代碼省略 .....................
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MyLayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new MyLayoutParams(lp);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MyLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
public static class MyLayoutParams extends MarginLayoutParams {
public MyLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public MyLayoutParams(int width, int height) {
super(width, height);
}
public MyLayoutParams(LayoutParams lp) {
super(lp);
}
}
}
另外在ViewGroup.onLayout()時中千萬別忘記根據 ViewGroup的padding和子View的margin 靈活給子View布局。
關于自定義View和自定義ViewGroup的其他細節就參閱其他文章吧。最好參考Android系統自帶控件的 源碼,畢竟這是最準確無誤的,閱讀他們文章加上自己的見解和懷疑,大牛也會有犯錯的時候。