1. 程式人生 > >自定義控制元件之-流式佈局FlowLayout

自定義控制元件之-流式佈局FlowLayout

前言

其實對於流式佈局控制元件,很多人並不陌生,專案中或多或少都會用到的.但是有多少人會寫一個流式佈局的控制元件這就不知道了,所以博主這裡對流式佈局進行一個講解,並且封裝一個比較完善的控制元件

效果圖

這裡寫圖片描述

看到的這個整個就是一個流式佈局,裡面是很多個TextView,博主使用了一個圓角的背景為了顯示的好看一點,當然了,流式佈局控制元件並不關心裡面的控制元件是什麼控制元件,任何控制元件在流式佈局內部都是可以顯示的

這裡寫圖片描述

效果圖上了,接下來就是教大家如何自定義了

分析

1.這是一個什麼型別的控制元件?
答:這是一個容器控制元件,那麼就需要繼承ViewGroup

2.流式佈局是什麼意思?
答:就是每一個控制元件從左往右排列,如果一行放不下啦就換一行顯示

3.流式佈局支援哪些屬性?
答:支援行間距,支援控制元件左右間的間距,整個流式佈局支援內邊距,支援做多顯示幾行(這個屬性有時候很好用的哦,至少博主專案中就用到了)

4.流式佈局有什麼特點?
答:這個特點是博主這個流式佈局的特點,並非所有的,博主實現的就是每一個流式佈局內部控制元件推薦的高度都是一樣的,但是如果出現大小不一的情況,高度小的會顯示在一行的中間

程式碼實現

1.支援的屬性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="XFlowLayout"
>
<!--橫縱向的間距--> <attr name="h_space" format="dimension" /> <attr name="v_space" format="dimension" /> <attr name="maxlines" format="integer" /> </declare-styleable> </resources>

2.繼承ViewGroup並且建立相應的建構函式

public class XFlowLayout
extends ViewGroup {
public XFlowLayout(Context context) { this(context, null); } public XFlowLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public XFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.XFlowLayout); //獲取橫縱向的間距 mHSpace = a.getDimensionPixelSize(R.styleable.XFlowLayout_h_space, dpToPx(10)); mVSpace = a.getDimensionPixelSize(R.styleable.XFlowLayout_v_space, dpToPx(10)); mMaxLines = a.getInt(R.styleable.XFlowLayout_maxlines, -1); a.recycle(); } }

控制元件內的屬性和支援的屬性對應

    /**
     * -1表示不限制,最多顯示幾行
     */
    private int mMaxLines = -1;

    /**
     * 孩子中最高的一個
     */
    private int mChildMaxHeight;

    /**
     * 每一個孩子的左右的間距
     * 20是預設值,單位是px
     */
    private int mHSpace = 20;

    /**
     * 每一行的上下的間距
     * 20是預設值,單位是px
     */
    private int mVSpace = 20;

測量方法

之前我們說過,博主這個流式佈局控制元件每一個內部的控制元件的高度都是一眼的,所以這裡測量孩子高度的時候,首先得讓所有孩子都測量一遍,然後找到高度最高的那個值,然後再讓孩子測量一遍(為什麼要再測量一遍是因為第一次測量只是為了知道每一個孩子的高度,而第二次測量是告訴所有孩子高度現在確定啦,這樣子每一個孩子內部才會根據這個值做一些計算,如果沒有再次測量這一步,比如TextView顯示文字的時候,你設定了居中顯示,可能就會失效了!)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 拿到父容器推薦的寬和高以及計算模式

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

        //測量孩子的大小
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        //尋找孩子中最高的一個孩子,找到的值會放在mChildMaxHeight變數中
        findMaxChildMaxHeight();

        //初始化值
        int left = getPaddingLeft(), top = getPaddingTop();

        // 幾行
        int lines = 1;

        //迴圈所有的孩子
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);

            if (left != getPaddingLeft()) { //是否是一行的開頭
                if ((left + view.getMeasuredWidth()) > sizeWidth - getPaddingRight()) { //需要換行了,因為放不下啦
                    // 如果到了最大的行數,就跳出,top就是當前的
                    if (mMaxLines != -1 && mMaxLines <= lines) {
                        break;
                    }
                    //計算出下一行的top
                    top += mChildMaxHeight + mVSpace;
                    left = getPaddingLeft();
                    lines++;
                }
            }

            left += view.getMeasuredWidth() + mHSpace;

        }

        if (modeHeight == MeasureSpec.EXACTLY) {
            //直接使用父類推薦的寬和高
            setMeasuredDimension(sizeWidth, sizeHeight);
        } else if (modeHeight == MeasureSpec.AT_MOST) {
            setMeasuredDimension(sizeWidth, (top + mChildMaxHeight + getPaddingBottom()) > sizeHeight ? sizeHeight : top + mChildMaxHeight + getPaddingBottom());
        } else {
            setMeasuredDimension(sizeWidth, top + mChildMaxHeight + getPaddingBottom());
        }

    }
    /**
     * 尋找孩子中最高的一個孩子
     */
    private void findMaxChildMaxHeight() {
        mChildMaxHeight = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            if (view.getMeasuredHeight() > mChildMaxHeight) {
                mChildMaxHeight = view.getMeasuredHeight();
            }
        }
    }

這裡的程式碼分幾步:
1.測量所有孩子,找到高度最高的值
2.然後我們模擬排列每一個孩子(View),計算出自身需要的高度
3.然後根據父容器推薦的高度的計算模式,來決定,自身高度是直接採用父容器給的高度還是採用自身的高度
4.最後設定自己的寬和高,把內邊距考慮進去
5.這裡有一個需要注意的是,如果設定了最高顯示的行數,那麼如果模擬排列的時候行數超過了最大的行數,那麼高度就以最高行數的高度為準

安排孩子的位置

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

        findMaxChildMaxHeight();

        //開始安排孩子的位置

        //初始化值
        int left = getPaddingLeft(), top = getPaddingTop();

        // 幾行
        int lines = 1;

        //迴圈所有的孩子
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);

            if (left != getPaddingLeft()) { //是否是一行的開頭
                if ((left + view.getMeasuredWidth()) > getWidth() - getPaddingRight()) { //需要換行了,因為放不下啦
                    // 如果到了最大的行數,就跳出,top就是當前的
                    if (mMaxLines != -1 && mMaxLines <= lines) {
                        break;
                    }
                    //計算出下一行的top
                    top += mChildMaxHeight + mVSpace;
                    left = getPaddingLeft();
                    lines++;
                }
            }

            //安排孩子的位置
            view.layout(left, top, left + view.getMeasuredWidth(), top + mChildMaxHeight);

            left += view.getMeasuredWidth() + mHSpace;

        }

    }

這次的安排和測量onMeasure方法中的模擬排列的邏輯是一樣的.
不再贅述

到這裡整個自定義就完工了,其實很簡單,並沒有什麼難的地方,程式碼量也很少

原始碼下載: