流式佈局初探

背景

最近項目中用到了流式佈局,最初就決定自己寫一個,發現一時竟然沒有思路。雖然自定義控件的博客看了不少,也寫過簡單的自定義控件,但是真正自己獨立寫出一個流式佈局,還是有些考驗的。查找了幾篇博客,思路大同小異,理清思路,自己開幹寫了一下。中間改了幾個問題,覺得可以正常使用後,這纔有了這篇博客。

我想說,會寫流式佈局了,表示你對ViewGroup的測量(onMeasure)和佈局(onLayout)有了一個較爲深入的理解。流式佈局主要涉及ViewGroup對子View的測量和擺放(佈局)。

效果圖流失佈局效果圖

參考佈局

參考佈局,尺寸和顏色根據自己需求修改:


<com.istarshine.views.MFlowLayout
    android:id="@+id/flow_search_history"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="@dimen/common_margin"
    android:layout_marginTop="2dp"
    android:layout_marginRight="18dp">


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="6dp"
        android:layout_marginTop="6dp"
        android:background="@drawable/shape_solid_ff"
        android:paddingLeft="@dimen/common_margin"
        android:paddingTop="4dp"
        android:paddingRight="@dimen/common_margin"
        android:paddingBottom="4dp"
        android:text="鞋子"
        android:textColor="@color/common_text_22"
        android:textSize="@dimen/text_size_14" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="6dp"
        android:layout_marginTop="6dp"
        android:background="@drawable/shape_solid_ff"
        android:paddingLeft="@dimen/common_margin"
        android:paddingTop="4dp"
        android:paddingRight="@dimen/common_margin"
        android:paddingBottom="4dp"
        android:text="吹風機"
        android:textColor="@color/common_text_22"
        android:textSize="@dimen/text_size_14" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="6dp"
        android:layout_marginTop="6dp"
        android:background="@drawable/shape_solid_ff"
        android:paddingLeft="@dimen/common_margin"
        android:paddingTop="4dp"
        android:paddingRight="@dimen/common_margin"
        android:paddingBottom="4dp"
        android:text="襪子 男 純棉"
        android:textColor="@color/common_text_22"
        android:textSize="@dimen/text_size_14" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="6dp"
        android:layout_marginTop="6dp"
        android:background="@drawable/shape_solid_ff"
        android:paddingLeft="@dimen/common_margin"
        android:paddingTop="4dp"
        android:paddingRight="@dimen/common_margin"
        android:paddingBottom="4dp"
        android:text="豆漿機 九陽"
        android:textColor="@color/common_text_22"
        android:textSize="@dimen/text_size_14" />


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="6dp"
        android:layout_marginTop="6dp"
        android:background="@drawable/shape_solid_ff"
        android:paddingLeft="@dimen/common_margin"
        android:paddingTop="4dp"
        android:paddingRight="@dimen/common_margin"
        android:paddingBottom="4dp"
        android:text="三隻松鼠"
        android:textColor="@color/common_text_22"
        android:textSize="@dimen/text_size_14" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="6dp"
        android:layout_marginTop="6dp"
        android:background="@drawable/shape_solid_ff"
        android:paddingLeft="@dimen/common_margin"
        android:paddingTop="4dp"
        android:paddingRight="@dimen/common_margin"
        android:paddingBottom="4dp"
        android:text="三隻松鼠 三隻松鼠  三隻松鼠  三隻松鼠  三隻松鼠  三隻松鼠  三隻松鼠"
        android:textColor="@color/common_text_22"
        android:textSize="@dimen/text_size_14" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="6dp"
        android:layout_marginTop="6dp"
        android:background="@drawable/shape_solid_ff"
        android:paddingLeft="@dimen/common_margin"
        android:paddingTop="4dp"
        android:paddingRight="@dimen/common_margin"
        android:paddingBottom="4dp"
        android:text="刮皮刀 水果刀"
        android:textColor="@color/common_text_22"
        android:textSize="@dimen/text_size_14" />


</com.istarshine.views.MFlowLayout>

MFlowLayout 代碼

onMeasure() 中測量子View,按流式佈局算出MFlowLayout自己(ViewGroup)的寬高;在 onLayout()中按流式佈局準確擺放子View。見如下代碼:

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

/**
 * 流式佈局
 */
public class MFlowLayout extends ViewGroup {

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

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //測量所有的子元素(調用子元素的measure()),
        // 只有測量過的元素調用child.getMeasuredHeight/Width()才能獲取到值,否則爲0
//        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int spaceWidth = widthSize - getPaddingLeft() - getPaddingRight();

        int resultWidth = 0;
        int resultHeight = 0;
        int lineWidth = 0;
        int lineHeight = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                continue;
            }
            //測量每個子元素的寬高
            int widthUsed = getPaddingLeft() + getPaddingRight();
            int heightUsed = getPaddingTop() + getPaddingBottom();
            measureChildWithMargins(child, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
//            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //測量後的寬高
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            //因爲子View可能設置margin,這裏要加上margin的距離
            MarginLayoutParams childMlp = (MarginLayoutParams) child.getLayoutParams();
            int childWidthWithMargin = childWidth + childMlp.leftMargin + childMlp.rightMargin;
            int childHeightWithMargin = childHeight + childMlp.topMargin + childMlp.bottomMargin;

            //一行放不下了,就換行
            if (lineWidth + childWidthWithMargin > spaceWidth) {
                //換行,計算寬高
                resultWidth = Math.max(resultWidth, lineWidth);
                resultHeight += lineHeight;
                //換行結束,重新給lineWidth和lineHeight賦值
                lineWidth = childWidthWithMargin;
                lineHeight = childHeightWithMargin;
            } else {
                //不換行,寬度直接相加
                lineWidth += childWidthWithMargin;
                //高度取二者最大值
                lineHeight = Math.max(lineHeight, childHeightWithMargin);

            }

            //最後一個肯定是最後一行
            if (i == getChildCount() - 1) {
                resultWidth = Math.max(resultWidth, lineWidth);
                resultHeight += lineHeight;
            }

        }

        //因爲上面resultWidth參與了寬度比較,所以計算padding必須放在這裏
        resultWidth += getPaddingLeft() + getPaddingRight();
        resultHeight += getPaddingTop() + getPaddingBottom();

        //設置FlowLayout的寬高
        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : resultWidth,
                heightMode == MeasureSpec.EXACTLY ? heightSize : resultHeight);


    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int spaceWidth = getWidth() - getPaddingLeft() - getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingLeft = getPaddingLeft();
        int childLeft = paddingLeft;
        int childTop = paddingTop;

        int lineHeight = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);

            if (child.getVisibility() == View.GONE) {
                continue;
            }

            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();
            MarginLayoutParams childMlp = (MarginLayoutParams) child.getLayoutParams();
            int childWidthWithMargin = childWidth + childMlp.leftMargin + childMlp.rightMargin;
            int childHeightWithMargin = childHeight + childMlp.topMargin + childMlp.bottomMargin;

            if (childLeft + childWidthWithMargin > spaceWidth) {
                childTop += lineHeight;

                //換行處理
                childLeft = paddingLeft;
                lineHeight = childHeightWithMargin;
            } else {
                lineHeight = Math.max(lineHeight, childHeightWithMargin);

            }
            int left = childLeft + childMlp.leftMargin;
            int top = childTop + childMlp.topMargin;
            int right = left + childWidth;
            int bottom = top + childHeight;
            child.layout(left, top, right, bottom);

            childLeft += childWidthWithMargin;

        }

    }


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

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

    @Override
    public void setLayoutParams(LayoutParams params) {
        super.setLayoutParams(params);
    }

    @Override
    protected boolean checkLayoutParams(LayoutParams p) {
        return p instanceof MarginLayoutParams;
    }
}

代碼並不難,但需要一些計算邏輯,和一些注意點。
解釋一下爲什麼這麼測量子View,而不是直接使用註釋部分(measureChild(child, widthMeasureSpec, heightMeasureSpec);):

            //測量每個子元素的寬高
            int widthUsed = getPaddingLeft() + getPaddingRight();
            int heightUsed = getPaddingTop() + getPaddingBottom();
            measureChildWithMargins(child, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
//            measureChild(child, widthMeasureSpec, heightMeasureSpec);

如果想要MFlowLayout可以設置padding,子View可以設置margin,就需要使用measureChildWithMargins(child, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed),這樣就會在測量的時候把MFlowLayout設置的padding(wideUsed,heightUsed)和子View設置的margin計算在內。而子View可以設置margin,則需要MarginLayoutParams,具體見上面代碼。

發佈了37 篇原創文章 · 獲贊 17 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章