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

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

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

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

LinearLayout給它的空間有多大,還記得onMeasure的兩個(gè)參數(shù)嘛,這倆是父布局給的參考值,也是父布局對(duì)其約束限制
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
MeasureSpec由兩部分構(gòu)成,高2位表示mode,低30位表示size;父布局給的參考寬高
int selfWidth = MeasureSpec.getSize(widthMeasureSpec); int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
由此我們可以得到父布局給的空間大小,也就是FlowLayout的最大空間。那我們實(shí)際需要多大空間呢,我們需要測(cè)量所以的子view。
子view的擺放邏輯:
- 本行能放下則放到本行,即滿足條件 lineUsed + childWidthMeasured + mHorizontalSpacing < selfWidth
- 本行放不下則另起一行
擺放邏輯有了,怎么測(cè)量子view
- 獲得子view的LayoutParams從而獲得xml里設(shè)置的layout_width與layout_height
- 調(diào)用getChildMeasureSpec方法算出MeasureSpec
- 子view調(diào)用measure方法測(cè)量
// 獲得LayoutParams LayoutParams childParams = childView.getLayoutParams(); // 計(jì)算measureSpec int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, parentLeft + parentRight, childParams.width); int childHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, parentTop + parentBottom, childParams.height); // 測(cè)量 childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
下面是 getChildMeasureSpec 內(nèi)部實(shí)現(xiàn),以橫向尺寸為例
// 以橫向尺寸為例,第一個(gè)參數(shù)是父布局給的spec,第二個(gè)參數(shù)是扣除自己使用的尺寸,第三個(gè)是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);
}
上面的算法其實(shí)很簡(jiǎn)單,根據(jù)父布局給的mode和size結(jié)合自身的尺寸算出自己的mode和size,具體規(guī)則如下

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

測(cè)量完子view后怎么確定布局的大???
- 測(cè)量所有行,得到最大的值作為布局的width
- 測(cè)量所有行的高度,高度的總和是布局的height
- 調(diào)用 setMeasuredDimension 函數(shù)設(shè)置最終的尺寸
onLayout
基于測(cè)量工作我們基本確定了所有子view的擺放位置,這階段要做的就是把所有的view擺放上去,調(diào)用子view的layout函數(shù)即可
具體代碼實(shí)現(xiàn)
public class FlowLayout extends ViewGroup {
private int mHorizontalSpacing = dp2px(16); //每個(gè)item橫向間距
private int mVerticalSpacing = dp2px(8); //每個(gè)item橫向間距
// 記錄所有的行
private List<List<View>> allLines = new ArrayList<>();
// 記錄所有的行高
private List<Integer> lineHeights = new ArrayList<>();
/**
* new FlowLayout(context) 的時(shí)候用
* @param context
*/
public FlowLayout(Context context) {
super(context);
}
/**
* xml是序列化格式,里面都是鍵值對(duì);所有的都在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 可能會(huì)被調(diào)用多次
*/
private void clearMeasureParams() {
// 不斷創(chuàng)建回收會(huì)造成內(nèi)存抖動(dòng),clear即可
allLines.clear();
lineHeights.clear();
}
/**
* 度量---大部分是先測(cè)量孩子再測(cè)量自己。孩子的大小可能是一直在變的,父布局隨之改變
* 只有ViewPager是先測(cè)量自己再測(cè)量孩子
* spec 是一個(gè)參考值,不是一個(gè)具體的值
* @param widthMeasureSpec 父布局給的。這是個(gè)遞歸的過程
* @param heightMeasureSpec 父布局給的
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
clearMeasureParams();
// 先測(cè)量孩子
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轉(zhuǎn)為measureSpec
/**
* 測(cè)量是個(gè)遞歸的過程,測(cè)量子View確定自身大小
* getChildMeasureSpec的三個(gè)參數(shù),第一個(gè)是父布局傳過來的MeasureSpec,第二個(gè)參數(shù)是去除自身用掉的padding,第三個(gè)是子布局需要的寬度或高度
*/
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, parentLeft + parentRight, childParams.width);
int childHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, parentTop + parentBottom, childParams.height);
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// 獲取子View測(cè)量的寬高
int childMeasuredWidth = childView.getMeasuredWidth();
int childMeasuredHeight = childView.getMeasuredHeight();
// 需要換行
if (childMeasuredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
// 換行時(shí)確定當(dāng)前需要的寬高
parentNeedHeight = parentNeedHeight + lineHeight + mVerticalSpacing;
parentNeedWidth = Math.max(parentNeedWidth, lineWidthUsed + mHorizontalSpacing);
// 存儲(chǔ)每一行的數(shù)據(jù) ?。?! 最后一行會(huì)被漏掉
allLines.add(lineViews);
lineHeights.add(lineHeight);
// 數(shù)據(jù)清空
lineViews = new ArrayList<>();
lineWidthUsed = 0;
lineHeight = 0;
}
lineViews.add(childView);
lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childMeasuredHeight);
//處理最后一行數(shù)據(jù)
if (i == childCount - 1) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeedHeight = parentNeedHeight + lineHeight + mVerticalSpacing;
parentNeedWidth = Math.max(parentNeedWidth, lineWidthUsed + mHorizontalSpacing);
}
}
// 測(cè)量完孩子后再測(cè)量自己
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 如果父布局給的是確切的值,測(cè)量子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是調(diào)用完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());
}
}
實(shí)現(xiàn)效果如文章開頭
FlowLayout 的 onMeasure方法是什么時(shí)候被調(diào)用的
FlowLayout的onMeasure是在上面什么調(diào)用的,肯定是在其父布局做測(cè)量遞歸的時(shí)候調(diào)用的。比如FlowLayout的父布局是LinearLayout,咱們?nèi)inearLayout中找實(shí)現(xiàn)
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();
...
...
// 開始測(cè)量
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調(diào)用其測(cè)量函數(shù),也就是FlowLayout的測(cè)量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
一些其他概念
MeasureSpec 是什么
自定義view常用的一個(gè)屬性MeasureSpec,是View的內(nèi)部類,封裝了對(duì)子View的布局要求,由尺寸和模式組成。由于int類型由32位構(gòu)成,所以他用高2位表示 Mode,低30位表示Size。
MeasureMode有三種 00 01 11
- UNSPECIFIED:不對(duì)View大小做限制,系統(tǒng)使用
- EXACTLY:確切的大小,如100dp
- AT_MOST:大小不可超過某值,如:matchParent,最大不能超過父布局
LayoutParams 與 MeasureSpec 的關(guān)系
我們?cè)趚ml寫的鍵值對(duì)是不能直接轉(zhuǎn)化為具體的dp的,根據(jù)父布局給的尺寸與模式計(jì)算出自己的MeasureSpec,通過不斷的遞歸測(cè)量,得到最后的測(cè)量值。LayoutParams.width獲取的就是xml里寫的或許是match_parent,或許是wrap_content,這些是不能直接用的,根據(jù)父布局給出的參考值再通過測(cè)量子布局的尺寸最后才能得到一個(gè)具體的測(cè)量值
onLayout為什么不用 int right = view.getWidth() 而用 getMeasuredWidth
這要對(duì)整個(gè)流程有完整的理解才能回答,getWidth 是在 onLayout 調(diào)用后才有的值,getMeasuredWidth在測(cè)量后有值
到此這篇關(guān)于Android 深入探究自定義view之流式布局FlowLayout的使用的文章就介紹到這了,更多相關(guān)Android 流式布局FlowLayout內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android中Volley框架進(jìn)行請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)的使用
這篇文章主要介紹了Android中Volley框架進(jìn)行請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)的使用,本文給大家介紹的非常詳細(xì)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-10-10
Android實(shí)現(xiàn)原生鎖屏頁面音樂控制
這篇文章主要介紹了Android實(shí)現(xiàn)原生鎖屏頁面音樂控制,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12
Android廣播實(shí)現(xiàn)App開機(jī)自啟動(dòng)
這篇文章主要為大家詳細(xì)介紹了Android廣播實(shí)現(xiàn)App開機(jī)自啟動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
Android使用viewpager實(shí)現(xiàn)畫廊式效果
這篇文章主要為大家詳細(xì)介紹了Android使用viewpager實(shí)現(xiàn)畫廊式效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-08-08

