圖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;
}
}
}
}
}