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

簡(jiǎn)書(shū) 編程之樂(lè)
轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處!

復(fù)習(xí)自定義View過(guò)程中我發(fā)現(xiàn)幾乎 很多人 都犯了一個(gè)細(xì)節(jié)上的錯(cuò)誤,就是ViewGroup中的子View 不支持margin。

注: 關(guān)于自定義View的基礎(chǔ)教程 請(qǐng)參閱其他博客

先總結(jié)兩點(diǎn)

  1. 自定義View在onDraw里面需要處理padding的影響,widthMeasureSpec和heightMeasureSpec是包含padding大小的。
  2. 子View的margin屬性是由ViewGroup處理的,ViewGroup在onMeasure和onLayout時(shí)一定要考慮 ViewGroup自己的padding和子View的margin的影響。

你可能遇到過(guò)下面這樣的錯(cuò)誤。

java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams

下面我們分析為什么會(huì)遇到這種錯(cuò)誤以及解決方法。

你可能見(jiàn)過(guò)很多人在自定義ViewGroup的
onMeasure()中使用
measureChildren(widthMeasureSpec, heightMeasureSpec); 來(lái)測(cè)量所有子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等都沒(méi)有用過(guò)這個(gè)measureChildren呢,幾乎全部都重寫(xiě)了,我們的自定義ViewGroup的measureChildren是不是應(yīng)該是改成下面這樣才對(duì)。

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);
    }
}

你應(yīng)該看到了區(qū)別,measureChild和measureChildWithMargins區(qū)別就是
測(cè)量child尺寸時(shí),保證child的 最大可用尺寸,感覺(jué)這個(gè)with前綴起的不太好。

  1. measureChild減去了 ViewGroup的padding 保證child最大可用空間
  2. measureChildWithMargins減去了ViewGroup的padding子View的margin 保證child最大可用空間

至于 measureChild和measureChildWithMargins中是如何**生成child的MeasureSpec,并最終調(diào)用child.measure() -- > child.onMeasure()的,這里就不貼源碼了。

總結(jié) : ViewGroup中測(cè)量child一定要用measureChildWithMargins而不是measureChild

使用measureChildWithMargins后卻產(chǎn)生異常
終于改成measureChildWithMargins了,卻突然產(chǎn)生了異常,這是為什么?
找到異常產(chǎn)生的位置,追蹤到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();獲取不到,則默認(rèn)生成一個(gè)
generateDefaultLayoutParams();

protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

這個(gè)默認(rèn)生成的肯定不能強(qiáng)制轉(zhuǎn)換為MarginLayoutParams了。

再來(lái)看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);
        ................
        ................
}

里面還有檢測(cè)這個(gè)child的LayoutParams 是不是為空的,干脆全部重寫(xiě)得了。

在你的自定義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()時(shí)中千萬(wàn)別忘記根據(jù) ViewGroup的padding和子View的margin 靈活給子View布局。

關(guān)于自定義View和自定義ViewGroup的其他細(xì)節(jié)就參閱其他文章吧。最好參考Android系統(tǒng)自帶控件的 源碼,畢竟這是最準(zhǔn)確無(wú)誤的,閱讀他們文章加上自己的見(jiàn)解和懷疑,大牛也會(huì)有犯錯(cuò)的時(shí)候。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容