Android 自定義ViewGroup(一)

自定義viewgroup,這個東西可以說簡單也簡單,說復雜也復雜。主要是因為用到所以復習了一下,那就順便做個筆記。

暫時只講簡單的用法

一.重要方法

(1)onMeasure 設置viewgroup的大小
(2)onLayout 設置如何擺放子View
(3)generateLayoutParams 設置LayoutParams

最重要的是前面兩個方法,所以說viewgroup很簡單,你只需要知道在onMeasure 和 onLayout中寫什么內容就行。

注:這里說的自定義viewgroup是值直接繼承ViewGroup,而不是繼承各種Layout之類已封裝好的ViewGroup。

1.確定自己要做的viewgroup是怎么樣的

首先要確認自己要做出怎樣的viewgroup,因為onMeasure和onLayout 可以說是關聯很小的,他們是配合使用才能出現自己想要的效果,如果不注意細節的話很容易弄錯,所以要先確定自己想做出來的viewgroup是怎樣的,才開始做。

2.onMeasure

首先要記好自定義onMeasure的流程,他會先調用onMeasure再調用onLayout,而onMeasure會調用多次,這個以后講。

(1)onMeasure做的事很簡單,就是測量ViewGroup的大小,準確來說是根據子View來測量viewgroup的大小。所以在onMeasure方法里面一般會用measureChildren去測量子view的大小。
(2)onMeasure方法中一般要分兩種情況去測量,viewgroup在xml中定義時,寬高是不是wrap_content
你想想,如果viewgroup固定寬高或者填充父布局的話,那實際中的寬高肯定是你定義的,但是如果是wrap_content的話,你就需要自己去設置寬高讓它包裹所有子View,所以自定義viewgroup的onMeasure中會分兩種情況去setMeasuredDimension寬高

2.onLayout

設置完viewgroup的寬高之后,就要去擺放子view。

(1)擺放子View的規則是,設置這個view的左上角的點在viewgroup的位置:
child.layout(left, top, right,bottom);
而根據這個坐標點和寬高,我們就能在viewgroup中正確的擺放子view
(2)需要注意的是如果子view超出了viewgroup所onMeasure(設置好大小)的部分,那部分不會顯示出來。
(3)獲取子view的方法View child = getChildAt(i); 得到的view一般是addview時添加view的順序,但是還有特殊情況,這個過后再解釋。

3.generateLayoutParams

設置LayoutParams,那么LayoutParams是什么東西,一般我們給viewgroup添加view都會用到LayoutParams。
翻譯過來就是布局參數,通俗點說就是能獲取到布局一些特定的屬性,比如說布局的邊距什么的。反正你正著想,在創建view時LayoutParams設置的屬性,在自定義Viewgroup中都能拿到。

這個類系統有很多子類,包括如果你牛逼的話你可以依照谷歌的這種做法,可以自定義LayoutParams,所以具體情況再說。

二.demo

逼逼了這么多,還是應該拿個例子來說,比如說流式布局
流式布局可以用自定義viewgroup來實現,雖然它也可以用recyclerview來實現,但是它的性質和RelativeLayout這些布局一樣,應該是一個viewgroup。

我就找了網上一個來說啊,因為我懶得寫算法。

public class BerFlowLayout extends ViewGroup {

    //存儲所有子View
    private List<List<View>> mAllChildViews = new ArrayList<>();
    //每一行的高度
    private List<Integer> mLineHeight = new ArrayList<>();

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

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //父控件傳進來的寬度和高度以及對應的測量模式
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        //如果當前ViewGroup的寬高為wrap_content的情況
        int width = 0;//自己測量的 寬度
        int height = 0;//自己測量的高度
        //記錄每一行的寬度和高度
        int lineWidth = 0;
        int lineHeight = 0;

        //獲取子view的個數
        int childCount = getChildCount();
        for(int i = 0;i < childCount; i ++){
            View child = getChildAt(i);
            //測量子View的寬和高
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //得到LayoutParams
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            //子View占據的寬度
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            //子View占據的高度
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            //換行時候
            if(lineWidth + childWidth > sizeWidth){
                //對比得到最大的寬度
                width = Math.max(width, lineWidth);
                //重置lineWidth
                lineWidth = childWidth;
                //記錄行高
                height += lineHeight;
                lineHeight = childHeight;
            }else{//不換行情況
                //疊加行寬
                lineWidth += childWidth;
                //得到最大行高
                lineHeight = Math.max(lineHeight, childHeight);
            }
            //處理最后一個子View的情況
            if(i == childCount -1){
                width = Math.max(width, lineWidth);
                height += lineHeight;
            }
        }
        //wrap_content
        setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width,
                modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mAllChildViews.clear();
        mLineHeight.clear();
        //獲取當前ViewGroup的寬度
        int width = getWidth();

        int lineWidth = 0;
        int lineHeight = 0;
        //記錄當前行的view
        List<View> lineViews = new ArrayList<View>();
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            //如果需要換行
            if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width) {
                //記錄LineHeight
                mLineHeight.add(lineHeight);
                //記錄當前行的Views
                mAllChildViews.add(lineViews);
                //重置行的寬高
                lineWidth = 0;
                lineHeight = childHeight + lp.topMargin + lp.bottomMargin;
                //重置view的集合
                lineViews = new ArrayList();
            }
            lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
            lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin);
            lineViews.add(child);
        }
        //處理最后一行
        mLineHeight.add(lineHeight);
        mAllChildViews.add(lineViews);

        //設置子View的位置
        int left = 0;
        int top = 0;
        //獲取行數
        int lineCount = mAllChildViews.size();
        for (int i = 0; i < lineCount; i++) {
            //當前行的views和高度
            lineViews = mAllChildViews.get(i);
            lineHeight = mLineHeight.get(i);
            for (int j = 0; j < lineViews.size(); j++) {
                View child = lineViews.get(j);
                //判斷是否顯示
                if (child.getVisibility() == View.GONE) {
                    continue;
                }
                MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                int cLeft = left + lp.leftMargin;
                int cTop = top + lp.topMargin;
                int cRight = cLeft + child.getMeasuredWidth();
                int cBottom = cTop + child.getMeasuredHeight();
                //進行子View進行布局
                child.layout(cLeft, cTop, cRight, cBottom);
                left += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            }
            left = 0;
            top += lineHeight;
        }
    }

    /**
     * 與當前ViewGroup對應的LayoutParams
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

}

1.onMeasure

先看onMeasure,看看它怎么測量整體父布局的。這里循環處理子view,先獲取到子view的大小

            //子View占據的寬度
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            //子View占據的高度
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;

然后判斷換不換行,如果這個子view的寬度加上前面累計起來的比父布局的寬度寬,那就加一行。

其實這里的onMeasure意圖很容易看懂,它的作用就是根據子view來決定高度,所以為什么我之前說要先弄清楚你想做怎么樣的效果,比如這里,我想做的效果就是流式布局的效果,那這個布局的高度肯定是根據有多少行來動態決定的吧,而這里的計算就是覺得這個高度的過程。

1.onLayout

這個他這里寫得有點麻煩,應該是可以再縮短一些的。

如果累加的寬度+當前子view的寬度+間距 > 一行的寬度,則換行

if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width) {
}

換行就設置累計寬度為0: lineWidth = 0;
mAllChildViews就是它存儲的第一個裝view的二維數組。

然后再對每一行進行操作 for (int i = 0; i < lineCount; i++) {...},然后對left 和top 進行疊加操作。

其實我覺得這里可以在最上面的循環中就直接child.layout對子View進行布局,不用兩次循環。他這里的思路是第一次大循環來獲取行數并保存二維數組,第二次大循環再設置子view位置。

3.MarginLayoutParams

這里寫的

/**
     * 與當前ViewGroup對應的LayoutParams
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

是為了設置子view和子view間通過margin設置的間距。

4.調用

如果在xml中寫子布局,可以直接用,這時設置generateLayoutParams會默認調用3個種的這個方法,你不用去關系LayoutParams。

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

但是如果是動態去添加view,你就需要自己去寫MarginLayoutParams,那么可以這樣寫。

                ViewGroup.LayoutParams lp = new ViewGroup.
                        LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                ViewGroup.MarginLayoutParams mlp = new ViewGroup.MarginLayoutParams(lp);
                mlp.setMargins(10,10,10,10);

                textView.setLayoutParams(mlp);

三.總結

最后做個小結吧。你可以看成自定義ViewGroup不難,它就要求你會用兩個方法去測量和擺放,難的是什么呢?難的是你要怎么去寫算法來完成你這個viewgroup的實現,也就是兩個方法中具體的代碼實現,還有就是這兩個方法連起來的效果和與LayoutParams配合的效果,主要是onMeasure和onLayout的配合,要非常的注意細節,比如你在onLayout設置間距,但是在onMeasure沒有去開辟這個間距所需要的空間,那就會出問題,這種寫多就會清楚了。

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

推薦閱讀更多精彩內容