你的自定義View是否真的支持Margin

簡書 編程之樂
轉載請注明原創出處!

復習自定義View過程中我發現幾乎 很多人 都犯了一個細節上的錯誤,就是ViewGroup中的子View 不支持margin。

注: 關于自定義View的基礎教程 請參閱其他博客

先總結兩點

  1. 自定義View在onDraw里面需要處理padding的影響,widthMeasureSpec和heightMeasureSpec是包含padding大小的。
  2. 子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前綴起的不太好。

  1. measureChild減去了 ViewGroup的padding 保證child最大可用空間
  2. 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系統自帶控件的 源碼,畢竟這是最準確無誤的,閱讀他們文章加上自己的見解和懷疑,大牛也會有犯錯的時候。

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

推薦閱讀更多精彩內容