自定義 FlowLayout

自定義 FlowLayout

在這里什么是一個 FlowLayout ,就是我們在很多軟件的搜索頁面看到的一些搜索標簽,先上一張圖吧。


FlowLayout使用例子

就是根據(jù)子 View 的長度自動換行,當然 Android 9.0 也出了一個類似的控件,但是考慮到之前的版本,我這里自己擼一個,也順便熟悉熟悉整個流程。
這里想寫一個 FlowLayout 也就是重寫一個 ViewGroup,我自己覺得寫一個
ViewGroup 要比寫一個 View 要麻煩,不僅要考慮適配 WRAP_CONTENT,同時還要處理自身的長度和子 View 的擺放,但是我們可以對 Android 的一些 ViewGroup 的實現(xiàn)有所了解。當然有時候可以曲線救國,有時候我覺得有些太底層的沒必要去自己實現(xiàn),而且自己寫的肯定沒有 Android 原生的考慮的周全,我就可以直接繼承已有的 ViewGroup 比如 LinearLayout、RelativieLayout 之類的,曲線救國~~

自定義 FlowLayout 的基本步驟

  1. 重寫OnMeasure
  2. 重寫OnLayout
    這里分別是 父ViewGroup 對我們 measure 和 layout 的方法的回調方法,我們在這里獲取到我們測量的大小和位置,接著可以繼續(xù)去測量我們的子 Item 的大小和位置。

重寫OnMeasure

空的onMeasure如下所示

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    }

首先第一步要先獲取測量模式和大小

int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

這個上面的 widthMeasureSpec 和 heightMeasureSpec 變量是OnMeasure的回調參數(shù),在這里我們獲取到了寬度和高度的大小和分別的測量模式,測量模式分成三種,但是我們在這里只討論常見的兩種 EXACTLY 和 AT_MOST, 前者對應著指定寬度和 MATCH_PARENT,后者對應著 WRAP_CONTENT。我們在這個方法里最想確定自己在 MATCH_PARENT 和 WRAP_CONTENT 下長寬如何,這里還要注意一點就是如果你重寫一個 ViewGroup,如果沒有針對 WRAP_CONTENT 重寫 ,那么你的長寬和 MATCH_PARENT 模式下沒有區(qū)別。

我們在 onMeasure 主要是測量子 View 所有長度加起來的和,當然不是盲目的加,廢話不多說先上代碼。

        int wrapWidth = 0;
        int wrapHeight = 0;

        int lineWidth = 0;
        int lineHeight = 0;

        int cCount = getChildCount();

        for (int i = 0; i < cCount; i++) {

            // 得到第 i 個 View
            View child = getChildAt(i);

            // 這里要十分注意,如果這里不先 measureChild 是無法得到 child 的寬度和高度
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();


            // 獲得子 View 所占的寬度和高度的時候要加上他自身的 margin
            int childWidth = child.getMeasuredWidth() + lp.leftMargin
                    + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin
                    + lp.bottomMargin;

            // 判斷是否需要換行 要減去左右的 padding
            if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {

                // 需要換行的時候
                wrapWidth = Math.max(wrapWidth, lineWidth);
                lineWidth = childWidth;
                wrapHeight += lineHeight;
                lineHeight = childHeight;

            } else {

                // 不需要換行的時候
                lineWidth += childWidth;
                lineHeight = Math.max(lineHeight, childHeight);

            }

            // 特殊考慮,要考慮到獲得 wrapWidth 和 wrapHeight
            if (i == cCount - 1) {
                wrapWidth = Math.max(lineWidth, wrapWidth);
                wrapHeight += lineHeight;
            }

        }

這上面有兩個變量要關注下,wrapWidth 和 wrapHeight 記錄著 WRAP_CONTENT 時的長和寬,原因也知道了,WRAP_CONTENT 的效果需要我們自己實現(xiàn),要不然就和 MATCH_PARENT 一模一樣。另一個需要注意的點就是上面調用了很重要的函數(shù)就是 measureChild 這是 ViewGroup 自帶的函數(shù),作用就是測量子 View 的長和寬,這個很好理解,我作為一個父 ViewGroup,在WRAP_CONTENT 的模式下,連自己的子 View 的長寬都不清楚,我怎么確定自己的長寬大小,最后還需要注意最后一個要另外記錄 wrapWidth 和 wrapHeight ,其它的大家自己可以通過閱讀代碼理解。

最后我們當然要調用設置自己寬高的函數(shù) setMeasuredDimension ,代碼如下,包括不同的測量模式下,大小不一樣。

setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth : wrapWidth + getPaddingLeft() + getPaddingRight(), modeHeight == MeasureSpec.EXACTLY ? sizeHeight : wrapHeight + getPaddingTop() + getPaddingBottom());

重寫 OnLayout

我們在這個函數(shù)里面通過調用子 View 的layout來確定子 View 的位置。這個方法的思路和上面的有相似的也有不同的,我們需要通過兩個變量來儲存每行有哪些 View 和每一行的高度,同時我們還得注意考慮一種情況,如果這個子 View 的狀態(tài)是 GONE, 我們就需要跳過它,同時在計算間距的時候還別忘了加上 父 ViewGroup 的padding 和自身的 margin ,按照這個思路我們需要遍歷兩遍所有的子 View,其它的跟上面的也很類似,關鍵就是控制哪些 View 是一行的,以及什么時候換行,這都又自己想要達到的效果決定的。這里因為跟上面的類似,我這里就不一一講述了,下面直接給出所有的代碼。

所有代碼

public class FlowLayout extends ViewGroup {

    private static final String TAG = "FlowLayout";

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

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

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


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        Log.d(TAG, "" + sizeWidth);

        // 如果是warp_content情況下,記錄寬和高
        int width = 0;
        int height = 0;

        // 記錄每一行的寬度與高度
        int lineWidth = 0;
        int lineHeight = 0;

        // 得到內部元素的個數(shù)
        int count = getChildCount();

        for (int i = 0; i < count; i++) {

            // 通過索引拿到每一個子view
            View child = getChildAt(i);

            // 測量子View的寬和高,系統(tǒng)提供的measureChild
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            // 得到LayoutParams
            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();

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

            // 換行 判斷 當前的寬度大于 開辟新行
            if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {

                // 對比得到最大的寬度
                width = Math.max(width, lineWidth);
                // 重置lineWidth
                lineWidth = childWidth;
                // 記錄行高
                height += lineHeight;
                lineHeight = childHeight;

            } else
            // 未換行
            {
                // 疊加行寬
                lineWidth += childWidth;
                // 得到當前行最大的高度
                lineHeight = Math.max(lineHeight, childHeight);
            }

            // 特殊情況,最后一個控件
            if (i == count - 1) {
                width = Math.max(lineWidth, width);
                height += lineHeight;
            }

        }

        setMeasuredDimension(
                modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
                modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()//
        );


    }

    /**
     * 存儲所有的View
     */
    private List<List<View>> allViews = new ArrayList<List<View>>();

    /**
     * 每一行的高度
     */
    private List<Integer> mLineHeight = new ArrayList<Integer>();


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {


        allViews.clear();
        mLineHeight.clear();

        // 當前ViewGroup的寬度
        int width = getWidth();

        int lineWidth = 0;
        int lineHeight = 0;

        // 存放每一行的子view
        List<View> lineViews = new ArrayList<>();

        int count = getChildCount();

        for (int i = 0; i < count; 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 - getPaddingLeft() - getPaddingRight()) {
                // 記錄LineHeight
                mLineHeight.add(lineHeight);
                // 記錄當前行的Views
                allViews.add(lineViews);

                // 重置我們的行寬和行高
                lineWidth = 0;
                lineHeight = childHeight + lp.topMargin + lp.bottomMargin;
                // 重置我們的View集合
                lineViews = new ArrayList<View>();
            }
            lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
            lineHeight = Math.max(lineHeight, childHeight + lp.topMargin
                    + lp.bottomMargin);
            lineViews.add(child);

        }// for end

        // 處理最后一行
        mLineHeight.add(lineHeight);
        allViews.add(lineViews);

        // 設置子View的位置

        int left = getPaddingLeft();
        int top = getPaddingTop();

        // 行數(shù)
        int lineNum = allViews.size();

        for (int i = 0; i < lineNum; i++) {

            // 當前行的所有的View
            lineViews = allViews.get(i);
            lineHeight = mLineHeight.get(i);

            for (int j = 0; j < lineViews.size(); j++) {
                View child = lineViews.get(j);
                // 判斷child的狀態(tài)
                if (child.getVisibility() == View.GONE) {
                    continue;
                }

                MarginLayoutParams lp = (MarginLayoutParams) child
                        .getLayoutParams();

                int lc = left + lp.leftMargin;
                int tc = top + lp.topMargin;
                int rc = lc + child.getMeasuredWidth();
                int bc = tc + child.getMeasuredHeight();

                // 為子View進行布局
                child.layout(lc, tc, rc, bc);

                left += child.getMeasuredWidth() + lp.leftMargin
                        + lp.rightMargin;
            }
            left = getPaddingLeft();
            top += lineHeight;
        }

    }

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


}

效果圖如下

在布局里面如何使用
效果圖

最后的總結

自定義一個 ViewGroup 最少需要重寫這兩個方法,當然也可以重寫他自己的 onDraw方法,我們這里最需要注意的就是 onMeasure 里面的測量模式,而且要記住,要自己來控制 WRAP_CONTENT 下自身的大小。

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容