背景
最近項目中用到了流式佈局,最初就決定自己寫一個,發現一時竟然沒有思路。雖然自定義控件的博客看了不少,也寫過簡單的自定義控件,但是真正自己獨立寫出一個流式佈局,還是有些考驗的。查找了幾篇博客,思路大同小異,理清思路,自己開幹寫了一下。中間改了幾個問題,覺得可以正常使用後,這纔有了這篇博客。
我想說,會寫流式佈局了,表示你對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,具體見上面代碼。