自定義ViewGroup原來如此簡單?手把手帶你寫一個流式佈局!

​ Android開發中,總會遇到這樣和那樣的需求。雖然官方已經給咱們提供了豐富的ViewGroupView的實現,可是總有無法知足需求的時候。這個時候咱們該怎麼辦呢? 首先遇事不決能夠先Google一下,看看有無現成的輪子。若是有輪子,那麼恭喜,扒來改改就好啦。若是沒有輪子,那能咋辦,只能本身造輪子咯。其實使用輪子更多時候是追求穩定和節約時間,咱們仍是須要對輪子的原理有必定的瞭解的。java

流式佈局在Android開發中使用的場景應該仍是比較多的,好比標籤展現搜索歷史記錄展現等等。這種樣式的佈局Android目前是沒有原生的ViewGroup的,固然你要找輪子確定也是很容易找到的,不過今天我仍是想以自定義ViewGroup的方式來實現這麼一個容器。web

什麼是ViewGroup

​ 首先咱們得弄清楚ViewGroup是什麼,還有它的職責。markdown

image-20201227155705094

ViewGroup繼承自View,並實現了ViewManagerViewParent接口。按照官方的定義,ViewGroup是一個特別的View,它能夠容納其餘的View,它實現了一系列添加和刪除View的方法。同時ViewGroup還定義了LayoutParamsLayoutParams會影響ViewViewGroup的位置和大小相關屬性。app

ViewGroup也是個抽象類,須要咱們重寫onLayout方法,固然僅僅重寫這麼一個方法是不夠的。ViewGroup自己只是實現了容納View能力,實現一個ViewGroup咱們須要完成對自身的測量、對child的測量、child的佈局等一系列的操做。ide

onMeasure

​ 這是自定義View實現的一個很是重要的方法,無論咱們是自定義View也好,仍是自定義ViewGroup都須要實現它。這個方法來自於ViewViewGroup自己沒有去處理這個方法。這個方法會傳遞兩個參數,分別是widthMeasureSpecheightMeasureSpec。這兩個數值實際上是個混合的信息,他們包含了具體的寬高數值和寬高的模式。這裏須要說一下MeasureSpecoop

MeasureSpec

MeasureSpecView的內部類,他是父容器給孩子傳遞的佈局信息的一個壓縮體。上文提到的傳遞的數值,實際上是經過MeasureSpecmakeMeasureSpec方法生成的:佈局

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
  //...
複製代碼

​ 其實MeasureSpec表明一個32位的int值,高2位表示SpecMode,低30位表示SpecSize,咱們能夠分別經過getModegetSize獲取對應的信息。表示什麼信息算是搞清楚了,那麼這些信息又是如何確認的呢?this

​ 在ViewGroup中有個getChildMeasureSpec方法,這個方法的實現基本能夠解答咱們的疑問google

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    int size = Math.max(0, specSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼

​ 代碼長度仍是有點長,可是邏輯並不複雜。spec參數爲ViewGroup的相關信息,padding則爲ViewGroup的leftPadding+rightPadding+childLeftMargin+childRightMargin+usedWidth,childDimension爲child的LayoutParams中指定的寬高信息。spa

​ child的具體的MeasureSpec會受到父容器的影響,也和自身的佈局信息有關,具體以下:

  • 若是child的LayoutParams指定了固定的寬高,如100dp,則最終onMeasure被傳遞的size就是指定的寬高,mode則是MeasureSpec.EXACTLY
  • 若是child的寬高信息爲MATCH_PARENT,這時候傳遞的size一般爲父容器的寬高,mode則會和父容器的mode保持一致。
  • 若是child的寬高信息爲WRAP_CONTENT,這時候傳遞的size也同樣是父容器的寬高,若是父容器的mode是MeasureSpec.UNSPECIFIED,則傳遞的mode是MeasureSpec.UNSPECIFIED,不然爲MeasureSpec.AT_MOST。

​ 這個specMode,簡單的來講EXACTLY就表明寬高信息是比較確認的,AT_MOST則是會告訴你一個最大寬度,實際寬度由你本身確認,UNSPECIFIED也是會告訴你一個父容器寬度,你也能夠設置爲任意高度。

onMeasure方法裏應該作什麼

​ 上面說了一堆關於MeasureSpec的,如今再來講一下onMeasure方法裏應該作什麼。

​ 若是是自定義View,咱們須要根據父容器傳遞的MeasureSpec來確認自身的寬高。若是是MeasureMode是EXACTLY,則這個View的寬高就是傳遞過來的size,若是是AT_MOST和UNSPECIFIED,則須要咱們自行處理了。在咱們計算獲得了一個想要的寬高信息後,須要調用setMeasuredDimension的方法來保存信息。

​ 若是是自定義ViewGroup,那咱們須要作的事情可能就要多一點了,首先咱們也仍是同樣,須要確認ViewGroup自身的寬高信息,若是都是EXACTLY拿很好辦,直接設置對應的size便可。若是想要支持WRAP_CONTENT,這時候可能就會比較麻煩一點了。首先咱們得想好一點,這個ViewGroup是如何爲child佈局的。這很重要,由於不一樣的佈局方式,child的排布不一樣,都會影響實際佔用的空間。

​ 仍是以LinearLayout舉例吧,LinearLayout支持橫向排列和縱向排列,他們須要執行的測量邏輯都是不同的。若是是縱向排列,則須要遍歷child,測量child,並累加他們的高度和margin,最後還要加上自身高度,這樣累加出來的數值就是WRAP_CONTENT下,自身應該佔用的高度。若是是橫向排列,則須要遍歷和累加child,並累加他們的寬度和margin等,原理都是差很少的。

​ 總結一下,onMeasure方法須要ViewGroup結合父容器傳遞的MeasureSpec測量child,配合child的排布方式,確認自身的寬高

onLayout

onLayout方法傳遞了5個參數,changed表示自身的位置或大小是否發生了改變,剩下的分別爲left,top,right,bottom,決定了他在父容器的位置。這是一個相對座標,起點並非屏幕的左上角。

​ 那在這個方法裏咱們應該作什麼呢?若是是自定義View的時候,咱們能夠不用管這個方法。由於View自己沒有容納child的能力,若是是ViewGroup,這時候咱們就須要爲child執行佈局操做了。咱們須要遍歷child,執行它們的layout方法。經過調用layout方法,咱們能夠傳遞left,top,right,bottom,肯定child在ViewGroup中的位置。一樣的,這也是一個相對座標,是依賴於父容器的。

​ 事實上,onLayout方法是在自身的layout方法被調用後調用的。Android總體的佈局體系自上而下一層層的調用,傳遞佈局信息,最終確認了各個View在屏幕上的位置。

onDraw

​ 一般來講,自定義ViewGroup並不須要重寫這個方法。這個方法用來作一些繪製操做,若是是自定義View,那咱們則須要重寫這個方法,實現一些繪製邏輯。

Padding和Margin

​ 這兩個概念仍是要說一下,理解一下它們的做用和實現原理。

  • Padding是相對於自身而言的,它影響了自身的繪製和child的佈局,是View自身的屬性。若是須要讓這個屬性生效,在繪製和佈局時候,咱們須要基於這個屬性的數值作必定的偏移,在測量的時候,咱們也須要考慮它的數值,爲最終測量結果添加上。
  • Margin是相對於父容器而言的,它影響了ViewViewGroup中的佈局,它一般是由LayoutParams所定義的。有這個屬性的時候,咱們在測量時候須要考慮到它,而且累加上,在佈局的時候,須要根據響應的屬性,進行必定的偏移。

實現一個流式佈局

​ 道理都理清楚了,寫代碼就會簡單不少了。流式佈局大概的效果就是添加的VIew按一行或者一列有序排列,若是一行或者一列放不下了,則換到下一行排列。下面就簡單實現一個流式佈局來加深一下理解。

​ 首先須要定義一個類,繼承自ViewGroup:

public class FlowLayout extends ViewGroup {
    public FlowLayout(Context context) {
        this(context, null);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      //todo 實現測量邏輯
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
			//todo 實現child的佈局邏輯
    }
}
複製代碼

​ 由於咱們須要支持margin屬性,因此咱們還須要這樣一個LayoutParamsViewGroup中已經定義了這樣一個MarginLayoutParams,咱們建立一個內部類,繼承此類實現:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
}
複製代碼

LayoutParams中還能夠本身去定義一些個性化的佈局參數,這裏就簡單處理了。同時咱們還得注意如下幾個方法:

/** * 直接調用 {@link #addView(View view)}的時候 用來生成默認的LayoutParams * * @return */
@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(-2, -2);
}

/** * {@link #addView(View child, ViewGroup.LayoutParams params)}時候,用來檢查佈局參數是否正確 * * @param p * @return */
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof LayoutParams;
}

/** * 若是{@link #checkLayoutParams(ViewGroup.LayoutParams p)}返回false,會調用此方法生成LayoutParams * * @param p * @return */
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    if (p == null) {
        return generateDefaultLayoutParams();
    }
    return new LayoutParams(p);
}

/** * 若是xml中的child,會調用此方法生成佈局參數 * @param attrs * @return */
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}
複製代碼

​ 註釋我都寫了,主要是用來用戶addView時候的默認佈局信息生成和檢測,若是沒處理好,可能會引發崩潰啥的。

​ 接下來是測量方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    Log.d(TAG, "onMeasure");
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    if (widthMode == MeasureSpec.EXACTLY) {
        //橫向寬度固定
        int lineMaxHeight = 0;//當前行最高的行高
        int currentLeft = getPaddingLeft();//當前child的起點left
        int currentTop = getPaddingTop();//當前child的起點top
      	//去除paddingLeft 和 paddingRight即爲可用寬度
        int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE) {//gone的child 不處理
                continue;
            }
            //測量child
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            int decoratedWidth = getDecoratedWidth(child);
            int decoratedHeight = getDecoratedHeight(child);
            if (currentLeft + decoratedWidth > availableWidth) {
                //寬度超了 換行
                currentLeft = decoratedWidth + getPaddingLeft();
                currentTop += lineMaxHeight;//高度加上以前的最大高度
                lineMaxHeight = decoratedHeight;
            } else {
                //若是不須要換行 只記錄當前的最大高度。
                currentLeft += decoratedWidth;
                lineMaxHeight = Math.max(lineMaxHeight, decoratedHeight);
            }
            if (i == getChildCount() - 1) {
                //最後一個元素了 咱們須要累加高度
                currentTop += lineMaxHeight;
            }
        }
      	//保存寬高信息
        setMeasuredDimension(widthSize, currentTop + getPaddingBottom());
    } else if (heightMode == MeasureSpec.EXACTLY) {
        //todo 實現縱向固定的流式佈局

    } else {
        //todo 實現寬高都固定的流式佈局
        
    }
}
複製代碼

​ 測量邏輯並不複雜,首先判斷ViewGroup的寬高模式,這裏實現了寬度固定的流式佈局的處理邏輯。咱們須要遍歷全部的child,並調用測量方法肯定他們的寬高。同時要注意的是child若是不可見則須要跳過。由於寬度是固定的,因此咱們須要計算出自身的高度。getDecoratedWidth獲取的是child自身的寬度與自身的左右的margin的和。遍歷過程當中依此排列child,若是一行排不下了,則執行換行邏輯,並累加高度,最後得出高度,保存。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    Log.d(TAG, "onLayout l :" + l + " t :" + t + " r :" + r + " b :" + b);
    int lineMaxHeight = 0;
    int currentLeft = getPaddingLeft();//當前child的起點left
    int currentTop = getPaddingTop();//當前child的起點top
    int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        if (child.getVisibility() == GONE) {//gone的child 不處理
            continue;
        }
        int decoratedWidth = getDecoratedWidth(child);
        int decoratedHeight = getDecoratedHeight(child);
        LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
        int childLeft, childTop;
        if (currentLeft + decoratedWidth > availableWidth) {
            //寬度超了 換行
            currentLeft = decoratedWidth + getPaddingLeft();
            currentTop += lineMaxHeight;//高度加上以前的最大高度
            lineMaxHeight = decoratedHeight;
            childLeft = getPaddingLeft() + +layoutParams.leftMargin;
            childTop = currentTop + layoutParams.topMargin;
        } else {
            //若是不須要換行 只記錄當前的最大高度。
            childLeft = currentLeft + layoutParams.leftMargin;
            childTop = currentTop + layoutParams.topMargin;
            currentLeft += decoratedWidth;
            lineMaxHeight = Math.max(lineMaxHeight, decoratedHeight);
        }
        child.layout(childLeft, childTop,
                childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
    }
}
複製代碼

​ onLayout方法裏我也只是實現了寬度固定下的邏輯。邏輯和測量時候的思路同樣,在測量的時候咱們已經爲每一個child確認了自身的寬高,在這裏咱們就只須要調用layout方法爲每一個child執行佈局邏輯便可。

​ 最後上運行效果,由於是demo因此樣式比較隨意,不要在乎這些細節(#^.^#)image-20201227182155002

自定義ViewGroup大體的流程就是這樣了,若是還有什麼困惑還不解能夠留言,我會用心解答。

相關文章
相關標籤/搜索