流式佈局FlowLayout

     

                     圖1                                                                       圖2

流式佈局的應用在很多的app上都可以看到,尤其是在一些購物類的app上,流式佈局大致的佈局原理就是先在一行上顯示,一行顯示不下了,就換行到下一行繼續顯示。它類似於LinearLayout的horizontal和vertical的結合體。

        原理分析圖

從上圖我們可以看出流式佈局在一行佈局完成後換行的幾種情況,就是在不斷計算一行寬度的時候有沒有超過父容器寬度,大致可以分爲兩種情況判斷它有沒有超過父容器,然後開始換行,第一種情況就是如圖(原理分析圖)第一行的情況:加完了view寬度和space之後再加view時判斷時候超過父容器。第二種情況就是第二三行的情況:加完view寬度之後,在加space寬度就超出了父容器,在加space的時候判斷時候超出父容器寬度。總結:就是在加view寬度和space寬度的時候都要判斷時候超出父容器,超出就換行。核心代碼如下

//獲取子view的寬度
	int childWidth = childView.getMeasuredWidth();
	    //將子view的寬度加到一行的寬度中
            usedWidth += childWidth;
	    //加完子view的時候,就判斷時候超出了父容器
            if (usedWidth <= widthSize) {
		//如果沒有超出父容器,就將子view添加到一行的集合中
                mLine.addView(childView);
		//再加上space,
                usedWidth += mHorizontalSpacing;
                //判斷時候超出
                if (usedWidth >= widthSize) {
		    //如果超出就換行
                    if (!newLine()) {
                        break;
                    }
                }
            } else {
                //如果添加子view寬度的時候超出父容器,就換行
                if (!newLine()) {
                    break;
                }
		//將子view添加到下一行的集合中
                mLine.addView(childView);
		//下一行的寬度重新計算,就算加mHorizontalSpacing超出父容器,再加下一個子view的寬度的時候,也還是換行
                usedWidth += childWidth + mHorizontalSpacing;
            }
在onMeasure的中獲取每個子view,測量子view,約束它不能超過父容器的大小

//獲取當前的子view
            View childView = getChildAt(i);
            if (childView.getVisibility() == GONE) {
                 //隱藏的就不予處理
                continue;
            }
            //測量子view 規範子view的大小,不讓他超過父view的大小
            int childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode);
            int childHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode);
            childView.measure(childWidthSpec, childHeightSpec);
在添加到最後一行的時候,循環就結束了,那麼在循環結束的時候要將最後一行也要添加到集合中去

//將最後一行添加到行集合中
        if (mLine != null && mLine.getViewCount() > 0 && !lineList.contains(mLine)) {
            lineList.add(mLine);
        }
在onMeasure中就可以計算出每行的寬和高,這樣就可以計算出父容器的寬和高

//flowLayout的寬
        int flowLayoutWidth = MeasureSpec.getSize(widthMeasureSpec);
        //當前控件行高的總和
        int totalLineHeight = 0;
        for (int i = 0; i < lineList.size(); i++) {
            //行高總和
            totalLineHeight += lineList.get(i).lineHeight;
        }
        //flowLayout的高
        int flowLayoutHeight = totalLineHeight + (lineList.size() - 1) * mVerticalSpacing + getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(flowLayoutWidth, flowLayoutHeight);
如何來保存每一行的所有view和它的行高和行寬(當前所佔的寬度),如果行寬沒有佔滿父容器,可以將剩餘的寬度平均分配給每個view,如果分配,效果圖如圖1;如果不分配,效果圖如圖2。在其內部定義一個內部類Line,用來保存行的一些信息,和將一行的佈局layout交給它來負責。

內部屬性定義如下

/**
         * 記錄每一行view的集合
         */
        private List<View> viewList = new ArrayList<View>();
        /**
         * 行高
         */
        private int lineHeight;
        /**
         * 當前行控件寬度的和
         */
        private int totalLineWidth;
我們要將一行中的所有view都添加到Line對象中進行管理,view添加到Line集合中的方法如下

/**
         * 往當前行添加子view的方法
         *
         * @param view
         */
        private void addView(View view) {
	    //將view添加到集合中
            viewList.add(view);
            //獲取當前行的行高
            int viewHeight = view.getMeasuredHeight();
             //保存一行中最大view的高度作爲本行的高度
            lineHeight = Math.max(viewHeight, lineHeight);
            //獲取當前行每一個控件的寬度
            int viewWidth = view.getMeasuredWidth();
	    //計算行寬,並保存
            totalLineWidth += viewWidth;
        }
將所有的view都保存在相應的Line中,每個Line也都保存在集合中去管理,下面的工作就是如何來佈局每行的view,和每個Line的佈局。下面我們就把Line的佈局和行中的所有view佈局分開處理。我們將Line的佈局放到onLayout()中去,而每行的佈局放到Line中去,讓Line去佈局所在行的所有view,只需給它該行所在的left和top的位置即可。

Line的佈局如下

/**
     * 佈局每一行的位置
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = getPaddingLeft();
        int top = getPaddingTop();
        for (int i = 0; i < lineList.size(); i++) {
            Line line = lineList.get(i);
            line.layout(left, top);
            top += line.lineHeight + mVerticalSpacing;
        }
    }
每行view的佈局如下

/**
         * 確定當前行中所有子view的位置
         *
         * @param left
         * @param top
         */
        public void layout(int left, int top) {
            //1.處理水平留白區域
            int layoutWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
            //水平留白區域
            int surplusWidth = layoutWidth - totalLineWidth - (getViewCount() - 1) * mHorizontalSpacing;
            //將水平留白區域平均分配跟當前行的每一個控件
            int oneSurplusWidth =surplusWidth/getViewCount();
            if(oneSurplusWidth>=0){
                for (int i=0;i<viewList.size();i++){
                    View view = viewList.get(i);
                    int viewWidth = view.getMeasuredWidth() + oneSurplusWidth;
                    int viewheight = view.getMeasuredHeight();

                    int viewWidthSec =MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY);
                    int viewHeightSec =MeasureSpec.makeMeasureSpec(viewheight,MeasureSpec.EXACTLY);
		    //重新測量子view的寬高
                    view.measure(viewWidthSec,viewHeightSec);
                    //解決細節2,獲取讓當前控件垂直居中的top
                    int childTop = (lineHeight -viewheight)/2;
                    //佈局每一個子view的位置
                    view.layout(left,top+childTop,left+view.getMeasuredWidth(),top+childTop+viewheight);
		    //重新計算left
                    left+=view.getMeasuredWidth()+mHorizontalSpacing;

                }
            }
        }
流式佈局的原理實現和代碼分析道這裏就分析完了,如發現問題歡迎留言


全部源碼如下:

package com.cj.chenj.expandtextview;

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

import java.util.ArrayList;
import java.util.List;

public class FlowLayout extends ViewGroup {
	
	public static final int MAX_LINES_COUNT = 100; 
	
    /**
     * 行對象
     */
    private Line mLine;
    /**
     * 已使用的寬度
     */
    private int usedWidth;
    /**
     * 水平間距
     */
    private int mHorizontalSpacing = 6;
    /**
     * 垂直間隙
     */
    private int mVerticalSpacing = 6;
    /**
     * 保存行的集合
     */
    private List<Line> lineList = new ArrayList<Line>();

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

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

        //獲取當前控件的測量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //獲取當前控件的測量尺寸
        int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
        int heightSize = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();

        //清空數據
        restore();
        //獲取當前控件所有字view的個數
        int childCount = getChildCount();
        //遍歷獲取所有子view
        for (int i = 0; i < childCount; i++) {
            //獲取當前的子view
            View childView = getChildAt(i);
            if (childView.getVisibility() == GONE) {
                continue;
            }
            //測量子view 規範子view的大小,不讓他超過父view的大小
            int childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode);
            int childHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode);
            childView.measure(childWidthSpec, childHeightSpec);
            //創建行對象
            if (mLine == null) {
                mLine = new Line();
            }

            //獲取子view的寬度
            int childWidth = childView.getMeasuredWidth();
            //將子view的寬度加到一行的寬度中
            usedWidth += childWidth;
            //加完子view的時候,就判斷時候超出了父容器
            if (usedWidth <= widthSize) {
                //如果沒有超出父容器,就將子view添加到一行的集合中
                mLine.addView(childView);
                //再加上space,
                usedWidth += mHorizontalSpacing;
                //判斷時候超出
                if (usedWidth >= widthSize) {
                    //如果超出就換行
                    if (!newLine()) {
                        break;
                    }
                }
            } else {
                //如果添加子view寬度的時候超出父容器,就換行
                if (!newLine()) {
                    break;
                }
                //將子view添加到下一行的集合中
                mLine.addView(childView);
                //下一行的寬度重新計算,就算加mHorizontalSpacing超出父容器,再加下一個子view的寬度的時候,也還是換行
                usedWidth += childWidth + mHorizontalSpacing;
            }
        }
        //將最後一行添加到行集合中
        if (mLine != null && mLine.getViewCount() > 0 && !lineList.contains(mLine)) {
            lineList.add(mLine);
        }
        //flowLayout的寬
        int flowLayoutWidth = MeasureSpec.getSize(widthMeasureSpec);
        //當前控件行高的總和
        int totalLineHeight = 0;
        for (int i = 0; i < lineList.size(); i++) {
            //行高總和
            totalLineHeight += lineList.get(i).lineHeight;
        }
        //flowLayout的高
        int flowLayoutHeight = totalLineHeight + (lineList.size() - 1) * mVerticalSpacing + getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(flowLayoutWidth, flowLayoutHeight);
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * 清空數據的方法
     */
    private void restore() {
        lineList.clear();
        mLine = new Line();
        usedWidth=0;
    }

    /**
     * 創建一個新行
     *
     * @return
     */
    private boolean newLine() {
        lineList.add(mLine);
        if (lineList.size() < MAX_LINES_COUNT) {
            mLine = new Line();
            usedWidth = 0;
            return true;
        }

        return false;
    }

    /**
     * 佈局每一行的位置
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = getPaddingLeft();
        int top = getPaddingTop();
        for (int i = 0; i < lineList.size(); i++) {
            Line line = lineList.get(i);
            line.layout(left, top);
            top += line.lineHeight + mVerticalSpacing;
        }
    }

    /**
     * 行對象
     */
    class Line {
        /**
         * 記錄每一行view的集合
         */
        private List<View> viewList = new ArrayList<View>();
        /**
         * 行高
         */
        private int lineHeight;
        /**
         * 當前行控件寬度的和
         */
        private int totalLineWidth;

        /**
         * 往當前行添加子view的方法
         *
         * @param view
         */
        private void addView(View view) {
            viewList.add(view);
            //獲取當前行的行高
            int viewHeight = view.getMeasuredHeight();
            lineHeight = Math.max(viewHeight, lineHeight);
            //獲取當前行每一個控件的寬度
            int viewWidth = view.getMeasuredWidth();
            totalLineWidth += viewWidth;
        }

        /**
         * 獲取當前行中有多少個子view
         *
         * @return
         */
        private int getViewCount() {
            return viewList.size();
        }

        /**
         * 確定當前行中所有子view的位置
         *
         * @param left
         * @param top
         */
        public void layout(int left, int top) {
            //1.處理水平留白區域
            int layoutWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
            //水平留白區域
            int surplusWidth = layoutWidth - totalLineWidth - (getViewCount() - 1) * mHorizontalSpacing;
            //將水平留白區域平均分配跟當前行的每一個控件
            int oneSurplusWidth =surplusWidth/getViewCount();
            if(oneSurplusWidth>=0){
                for (int i=0;i<viewList.size();i++){
                    View view = viewList.get(i);
                    int viewWidth = view.getMeasuredWidth() + oneSurplusWidth;
                    int viewheight = view.getMeasuredHeight();

                    int viewWidthSec =MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY);
                    int viewHeightSec =MeasureSpec.makeMeasureSpec(viewheight,MeasureSpec.EXACTLY);
                    view.measure(viewWidthSec,viewHeightSec);
                    //解決細節2,獲取讓當前控件垂直居中的top
                    int childTop = (lineHeight -viewheight)/2;
                    //佈局每一個子view的位置
                    view.layout(left,top+childTop,left+view.getMeasuredWidth(),top+childTop+viewheight);
                    left+=view.getMeasuredWidth()+mHorizontalSpacing;

                }
            }
        }
    }
}



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