自定義ViewGroup的一個綜合實踐 FlowLayout

自定義控件系列:
秒懂OnMeasure
秒懂OnLayout
讓自定義ViewGroup裏的子控件支持Margin
讓自定義ViewGroup支持Padding
自定義ViewGroup的一個綜合實踐 FlowLayout

效果

在這裏插入圖片描述

直接上源碼

關鍵是onMeasure裏caculateAtMostSize()方法和onLayout的邏輯,自定義View就是這樣小邏輯比較繞,需要仔細推敲,一定能寫得出來
其他的都是模板代碼
我認爲有了大的框架思維後,再去推敲細微部分的邏輯,會比較輕鬆,最怕大框架也沒有,細微部分的那種很繞的邏輯還不會寫,這就很難受

package com.view.custom.dosometest.view;

import android.content.Context;
import android.graphics.Point;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
 * 描述當前版本功能
 *
 * @Project: DoSomeTest
 * @author: cjx
 * @date: 2019-12-01 10:06  星期日
 */
public class FlowLayout extends ViewGroup {


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

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

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


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 首先計算一下在AtMost模式下,這個自定義view的寬高,
        // 這裏把計算出來的寬高封裝在了一個Point裏,x爲寬,y爲高
        Point point = caculateAtMostSize(widthMeasureSpec, heightMeasureSpec);

        // 根據 默認寬高、AtMost下的寬高、MeasureSpec測量規格,計算出最終這個view的寬高
        int width = measureSize(0, point.x, widthMeasureSpec);
        int height = measureSize(0, point.y, heightMeasureSpec);

        // 把上面計算出來的寬高作爲參數設置給setMeasuredDimension就ok了
        setMeasuredDimension(width, height);
    }


    /**
     * 通過widthMeasureSpec計算出這個View最終的寬高
     *
     * @param defalut     這個view的默認值,僅僅是爲了支持下UNSPECIFIED模式,但是這個模式其實用不到
     * @param atMostSize  AT_MOST下的尺寸
     * @param measureSpec 測量規格(包含了模式+尺寸)
     * @return
     */
    private int measureSize(int defalut, int atMostSize, int measureSpec) {


        int result = defalut;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = defalut;
                break;
            case MeasureSpec.AT_MOST:
                //在AT_MOST模式下,系統傳來的specSize是一個父容器所能容納的最大值,你這個自定義view計算的尺寸不能大於這個值
                result = Math.min(atMostSize, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }


        return result;
    }


//    ↓↓↓↓↓↓↓↓支持Margin的固定寫法,下面照抄就行了,至於爲什麼,可以去看源碼,但是我覺得直接記住就ok了↓↓↓↓↓↓↓↓

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

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

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }
//    ↑↑↑↑↑↑↑↑支持Margin的固定寫法,下面照抄就行了,至於爲什麼,可以去看源碼,但是我覺得直接記住就ok了↑↑↑↑↑↑↑↑

    /**
     * 計算本View在AtMost模式下的寬高
     * 其他代碼都是不用動的,在這裏寫下你特有的邏輯就可以
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     * @return
     */
    private Point caculateAtMostSize(int widthMeasureSpec, int heightMeasureSpec) {

        int lineWidth = 0;//當前行的寬度
        int lineHeight = 0;//當前行的高度
        int totalWidth = 0;//自定義ViewGroup的總寬度
        int totalHeight = 0;//自定義ViewGroup的總高度


        int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            // 測量一下子控件的寬高
            measureChild(child, widthMeasureSpec, heightMeasureSpec);

            // 得到MarginLayoutParams,margin就在這裏保存着
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            // 獲得子控件的寬高(需要加上對應的margin,讓控件的寬高包含margin,
            // 這樣才能讓自定義的viewgroup在計算自身在AtMost模式的尺寸時候考慮到這些margin)
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;

            // 前面說過,如果是wrap_content或match_parent,getMeasuredWidth()得到的是父容器的最大值
            if (lineWidth + childWidth > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
                // 換行,總寬度更新,總高度更新
                totalWidth = Math.max(lineWidth, totalWidth);
                totalHeight += lineHeight;
                //新的一行,初始化lineWidth,lineHeight
                lineWidth = childWidth - lp.leftMargin;

                lineHeight = childHeight;
            } else {
                // 在這行接着放子控件
                lineWidth += childWidth;//行寬度增加
                lineHeight = Math.max(lineHeight, childHeight);//本行高度爲本行最高的那個
            }

            // 上面的代碼裏只有在換行時候,才計算總寬高,
            // 如果沒有換行的話,我們只是單純地累加計算了本行的高度和寬度
            // 這樣會導致,沒有換行的的哪一行的寬高是沒計算到總寬高裏的,
            // 那麼這樣的行其實只有一個,就是最後一行(如果只有一行的話,沒經過換行,這唯一的一行也相當於最後一行)
            if (i == count - 1) {
                totalWidth = Math.max(lineWidth, totalWidth);
                totalHeight += lineHeight;
            }


        }

        totalWidth += (getPaddingLeft() + getPaddingRight());
        totalHeight += (getPaddingTop() + getPaddingBottom());
        Log.e("ccc", totalWidth + "totalWidth");
        Log.e("ccc", totalHeight + "totalHeight");
        return new Point(totalWidth, totalHeight);
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int lineWidth = 0;//當前行的寬度
        int lineHeight = 0;//當前行的高度
        int top = getPaddingTop();
        int left = getPaddingLeft();

        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            boolean isChangeLine = false;
            View child = getChildAt(i);

            // 得到MarginLayoutParams,margin就在這裏保存着
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();


            // 獲得子控件的寬高(需要加上對應的margin,讓控件的寬高包含margin)
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;

            // 前面說過,如果是wrap_content或match_parent,getMeasuredWidth()得到的是父容器的最大值
            if (lineWidth + childWidth > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
                isChangeLine = true;
                top += lineHeight;
                left = getPaddingLeft();

                //新的一行,初始化lineWidth,lineHeight
                lineWidth = childWidth - lp.leftMargin;
                lineHeight = childHeight;
            } else {
                isChangeLine = false;
                lineHeight = Math.max(lineHeight, childHeight);
                lineWidth += childWidth;

            }


            //如果換行了,那麼這個子控件的左邊距不生效(爲了保證左對齊,因爲使用的時候把所有的子控件都設置了左margin,第一個view別設置左margin)
            int childLeft = isChangeLine ? left : left + lp.leftMargin;
            int childTop = top + lp.topMargin;
            int childRight = childLeft + child.getMeasuredWidth();
            int childBottom = childTop + child.getMeasuredHeight();

            child.layout(childLeft, childTop, childRight, childBottom);
            // 佈局了一個子控件後,left往後移動一個子控件的寬度
            left += childWidth;

        }

    }
}

github源碼之 FlowLayout

參考:
《Android自定義開發入門與實踐》感謝大神的著作,對其中的實例做了部分修改

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章