Android 深入探究自定義view之流式布局FlowLayout的使用
引子
文章開始前思考個問題,view到底是如何擺放到屏幕上的?在xml布局中,我們可能用到match_parent、wrap_content或是具體的值,那我們如何轉為具體的dp?對于層層嵌套的布局,他們用的都不是具體的dp,我們又該如何確定它們的尺寸?
下圖是實現效果

自定義View的流程
想想自定義view我們都要做哪些事情
- 布局,我們要確定view的尺寸以及要擺放的位置,也就是 onMeasure() 、onLayout() 兩方法
- 顯示,布局之后是怎么把它顯示出來,主要用的是onDraw,可能用到 :canvas paint matrix clip rect animation path(貝塞爾) line
- 交互,onTouchEvent
本文要做的是流式布局,繼承自ViewGroup,主要實現函數是onMeasure() 、onLayout() 。下圖是流程圖

onMeasure
onMeasure是測量方法,那測量的是什么?我們不是在xml已經寫好view的尺寸了嗎,為什么還要測量?
有這么幾個問題,我們在xml寫view的時候確實寫了view的width與height,但那玩意是具體的dp嗎?我們可能寫的是match_parent,可能是wrap_content,可能是權重,可能是具體的長度。對于不是確定值的我們要給它什么尺寸?哪怕寫的是確定值就一定能給它嗎,如果父布局最大寬度是100dp,子布局寫的是200dp咋辦?對于多層級的view,我們只是調用本個view的onMeasure方法嗎?
以下面的圖為栗子

如果上圖圈紅的View就是我們要自定義的view,我們該怎么測量它?
- 首先我們要知道它的父布局能給它多大的空間
- 對于容器類型的view,根據其所有子view需要的空間計算出view所需的尺寸
首先要明確一點:測量是自上而下遞歸的過程!以FlowLayout的高度舉例,它的height要多少合適?根據布局的擺放逐個測量每行的高度得出其所需的height,這個測出的高度再根據父布局給出的參考做計算,最后得到真正的高度。在測量子view的時候,子view可能也是容器,其內部也有很多view,其本身的不確定性需要遍歷其子布局,這是一個遞歸的過程!
下面開始我們的測量過程,假設FlowLayout的父布局是LinearLayout,整體UI布局如下

LinearLayout給它的空間有多大,還記得onMeasure的兩個參數嘛,這倆是父布局給的參考值,也是父布局對其約束限制
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
MeasureSpec由兩部分構成,高2位表示mode,低30位表示size;父布局給的參考寬高
int selfWidth = MeasureSpec.getSize(widthMeasureSpec); int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
由此我們可以得到父布局給的空間大小,也就是FlowLayout的最大空間。那我們實際需要多大空間呢,我們需要測量所以的子view。
子view的擺放邏輯:
- 本行能放下則放到本行,即滿足條件 lineUsed + childWidthMeasured + mHorizontalSpacing < selfWidth
- 本行放不下則另起一行
擺放邏輯有了,怎么測量子view
- 獲得子view的LayoutParams從而獲得xml里設置的layout_width與layout_height
- 調用getChildMeasureSpec方法算出MeasureSpec
- 子view調用measure方法測量
// 獲得LayoutParams LayoutParams childParams = childView.getLayoutParams(); // 計算measureSpec int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, parentLeft + parentRight, childParams.width); int childHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, parentTop + parentBottom, childParams.height); // 測量 childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
下面是 getChildMeasureSpec 內部實現,以橫向尺寸為例
// 以橫向尺寸為例,第一個參數是父布局給的spec,第二個參數是扣除自己使用的尺寸,第三個是layoutParams
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
// 老王的錢是確定的,小王有三種可能
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
// 老王的錢最多有多少,小王有三種可能
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
// 老王的錢不確定,小王有三種可能
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面的算法其實很簡單,根據父布局給的mode和size結合自身的尺寸算出自己的mode和size,具體規(guī)則如下

算法的實現是父布局有三種可能,子布局三種可能,總共9種可能。就像下面的小王想跟老王借錢買房,有幾種可能?

測量完子view后怎么確定布局的大小?
- 測量所有行,得到最大的值作為布局的width
- 測量所有行的高度,高度的總和是布局的height
- 調用 setMeasuredDimension 函數設置最終的尺寸
onLayout
基于測量工作我們基本確定了所有子view的擺放位置,這階段要做的就是把所有的view擺放上去,調用子view的layout函數即可
具體代碼實現
public class FlowLayout extends ViewGroup {
private int mHorizontalSpacing = dp2px(16); //每個item橫向間距
private int mVerticalSpacing = dp2px(8); //每個item橫向間距
// 記錄所有的行
private List<List<View>> allLines = new ArrayList<>();
// 記錄所有的行高
private List<Integer> lineHeights = new ArrayList<>();
/**
* new FlowLayout(context) 的時候用
* @param context
*/
public FlowLayout(Context context) {
super(context);
}
/**
* xml是序列化格式,里面都是鍵值對;所有的都在LayoutInflater解析
*反射
*
* @param context
* @param attrs
*/
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 主題style
* @param context
* @param attrs
* @param defStyleAttr
*/
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 自定義屬性
* @param context
* @param attrs
* @param defStyleAttr
* @param defStyleRes
*/
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* onMeasure 可能會被調用多次
*/
private void clearMeasureParams() {
// 不斷創(chuàng)建回收會造成內存抖動,clear即可
allLines.clear();
lineHeights.clear();
}
/**
* 度量---大部分是先測量孩子再測量自己。孩子的大小可能是一直在變的,父布局隨之改變
* 只有ViewPager是先測量自己再測量孩子
* spec 是一個參考值,不是一個具體的值
* @param widthMeasureSpec 父布局給的。這是個遞歸的過程
* @param heightMeasureSpec 父布局給的
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
clearMeasureParams();
// 先測量孩子
int childCount = getChildCount();
int parentTop = getPaddingTop();
int parentLeft = getPaddingLeft();
int parentRight = getPaddingRight();
int parentBottom = getPaddingBottom();
// 爺爺給的參考值
int selfWidth = MeasureSpec.getSize(widthMeasureSpec);
int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
// 保存一行所有的 view
List<View> lineViews = new ArrayList<>();
// 記錄這行已使用多寬 size
int lineWidthUsed = 0;
// 一行的高
int lineHeight = 0;
// measure過程中,子view要求的父布局寬高
int parentNeedWidth = 0;
int parentNeedHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
LayoutParams childParams = childView.getLayoutParams();
// 將LayoutParams轉為measureSpec
/**
* 測量是個遞歸的過程,測量子View確定自身大小
* getChildMeasureSpec的三個參數,第一個是父布局傳過來的MeasureSpec,第二個參數是去除自身用掉的padding,第三個是子布局需要的寬度或高度
*/
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, parentLeft + parentRight, childParams.width);
int childHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, parentTop + parentBottom, childParams.height);
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// 獲取子View測量的寬高
int childMeasuredWidth = childView.getMeasuredWidth();
int childMeasuredHeight = childView.getMeasuredHeight();
// 需要換行
if (childMeasuredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
// 換行時確定當前需要的寬高
parentNeedHeight = parentNeedHeight + lineHeight + mVerticalSpacing;
parentNeedWidth = Math.max(parentNeedWidth, lineWidthUsed + mHorizontalSpacing);
// 存儲每一行的數據 ?。?! 最后一行會被漏掉
allLines.add(lineViews);
lineHeights.add(lineHeight);
// 數據清空
lineViews = new ArrayList<>();
lineWidthUsed = 0;
lineHeight = 0;
}
lineViews.add(childView);
lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childMeasuredHeight);
//處理最后一行數據
if (i == childCount - 1) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeedHeight = parentNeedHeight + lineHeight + mVerticalSpacing;
parentNeedWidth = Math.max(parentNeedWidth, lineWidthUsed + mHorizontalSpacing);
}
}
// 測量完孩子后再測量自己
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 如果父布局給的是確切的值,測量子view則變得毫無意義
int realWidth = (widthMode == MeasureSpec.EXACTLY) ? selfWidth : parentNeedWidth;
int realHeight = (heightMode == MeasureSpec.EXACTLY) ? selfHeight : parentNeedHeight;
setMeasuredDimension(realWidth, realHeight);
}
/**
* 布局
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int currentL = getPaddingLeft();
int currentT = getPaddingTop();
for (int i = 0; i < allLines.size(); i++) {
List<View> lineViews = allLines.get(i);
int lineHeight = lineHeights.get(i);
for (int j = 0; j < lineViews.size(); j++) {
View view = lineViews.get(j);
int left = currentL;
int top = currentT;
// 此處為什么不用 int right = view.getWidth(); getWidth是調用完onLayout才有的
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
// 子view位置擺放
view.layout(left, top, right, bottom);
currentL = right + mHorizontalSpacing;
}
currentT = currentT + lineHeight + mVerticalSpacing;
currentL = getPaddingLeft();
}
}
public static int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}
}
實現效果如文章開頭
FlowLayout 的 onMeasure方法是什么時候被調用的
FlowLayout的onMeasure是在上面什么調用的,肯定是在其父布局做測量遞歸的時候調用的。比如FlowLayout的父布局是LinearLayout,咱們去LinearLayout中找實現
LinearLayout.onMeasure()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
void measureVertical(widthMeasureSpec, heightMeasureSpec){
// 獲取子view 的 LayoutParams
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
...
...
// 開始測量
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,heightMeasureSpec, usedHeight);
}
void measureChildBeforeLayout(View child, int childIndex,int widthMeasureSpec, int totalWidth, int heightMeasureSpec,int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 去除自己的使用,padding、margin剩下的再給子view
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
// 此處子view調用其測量函數,也就是FlowLayout的測量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
一些其他概念
MeasureSpec 是什么
自定義view常用的一個屬性MeasureSpec,是View的內部類,封裝了對子View的布局要求,由尺寸和模式組成。由于int類型由32位構成,所以他用高2位表示 Mode,低30位表示Size。
MeasureMode有三種 00 01 11
- UNSPECIFIED:不對View大小做限制,系統(tǒng)使用
- EXACTLY:確切的大小,如100dp
- AT_MOST:大小不可超過某值,如:matchParent,最大不能超過父布局
LayoutParams 與 MeasureSpec 的關系
我們在xml寫的鍵值對是不能直接轉化為具體的dp的,根據父布局給的尺寸與模式計算出自己的MeasureSpec,通過不斷的遞歸測量,得到最后的測量值。LayoutParams.width獲取的就是xml里寫的或許是match_parent,或許是wrap_content,這些是不能直接用的,根據父布局給出的參考值再通過測量子布局的尺寸最后才能得到一個具體的測量值
onLayout為什么不用 int right = view.getWidth() 而用 getMeasuredWidth
這要對整個流程有完整的理解才能回答,getWidth 是在 onLayout 調用后才有的值,getMeasuredWidth在測量后有值
到此這篇關于Android 深入探究自定義view之流式布局FlowLayout的使用的文章就介紹到這了,更多相關Android 流式布局FlowLayout內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

