本篇主要內(nèi)容:從0到1寫一個流式布局TagFlowLayout
1 通過本篇可以了解什么
- 繼承至
ViewGroup
的組件如何編寫onMeasure
和onLayout
方法闪湾; - 子
View
的margin
值是如何在onMeasure
和onLayout
中使用的妻导; - 流式布局的基本原理。
2 繼承ViewGroup的組件到底意味著什么
首先,ViewGroup
是一個組件容器纳猫,它自身沒有進行任何測量和布局恕出,但是它提供了一系列測量子View
的方法蔼卡,方便我們調(diào)用屯伞。
再者腿箩,我們需要在繼承ViewGroup
組件中的測量方法中進行子View
控件的測量。
onMeasure
中確定各個View的大小以及onLayout
中需要的擺放參數(shù)劣摇,onLayout
中進行擺放珠移。
padding
和margin
值需要在測量和擺放時加入計算中,onMeasure
中大部分考慮的是margin末融,onLayout
中考慮padding
和margin
值钧惧。
3 實現(xiàn)過程
廢話不多說,先看效果圖:
3.1 創(chuàng)建控件
這一步相對簡單勾习,就不做過多說明浓瞪,代碼如下:
public class TagFlowLayout extends ViewGroup {
public TagFlowLayout(Context context) {
this(context, null);
}
public TagFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TagFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
}
}
3.2 onMeasure方法實現(xiàn)過程
先用圖說明一下measure邏輯流程,其實也很簡單巧婶。
由上圖可知乾颁,我們有兩個目標:
- 其一,遍歷并計算每個子
View
艺栈; - 其二英岭,找出超出屏幕位置的
View
,并進行換行湿右。
3.2.1 遍歷并計算每個View
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
LogUtils.d("onMeasure: " + onMeasureCount++);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
接下來開始對每個子View
進行測量诅妹,ViewGroup
給我們提供了測量子View
的方法measureChildWithMargins()
于是就有了如下代碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
LogUtils.d("onMeasure: " + onMeasureCount++);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
measureChildWithMargins();
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
但是發(fā)現(xiàn)measureChildWithMargins
有五個參數(shù),如下:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed)
- 第一個參數(shù)
child
诅需,就是我們需要測量的View
漾唉; - 第二個參數(shù)
parentWidthMeasureSpec
荧库,這是我們傳遞給子view
對于寬度的建議堰塌; - 第三個參數(shù)是指父控件在水平方向上已使用的寬度,有可能是其他子
view
使用的空間分衫; - 第四個和第五個參數(shù)與第二個场刑、三個參數(shù)雷同,只是是豎直方向蚪战。
知道這幾個參數(shù)的意義后牵现,于是我們就可以在計算子view
時傳遞相應的參數(shù)值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
LogUtils.d("onMeasure: " + onMeasureCount++);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
到這里我們應該會有個疑問:widthUsed和heightUsed這兩個參數(shù)為什么是0邀桑?
原因如下:
- 因為如果把已用的空間傳入這個參數(shù)瞎疼,那么有可能會導致影響子
view
的測量結(jié)果;
至于為什么會影響子view
的測量結(jié)果壁畸,我們看看源碼:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// widthUsed會作為getChildMeasureSpec方法中padding參數(shù)值的一部分進入到view的MeasureSpec參數(shù)的計算中去贼急,
// 所以父view的建議有可能會影響子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);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
- 而我們所想要的效果是子
View
按照自己的測量方式測量出自己大小茅茂,空間不足,另起一行太抓,不能約束子View
空闲。
好了,上面查看了部分源碼進行分析走敌,我們繼續(xù)回歸主題碴倾。
3.2.2 找出超出屏幕的View,并且進行換行掉丽。
基本思路如下:
- 定義相應數(shù)據(jù)結(jié)構(gòu)跌榔;
- 同一行寬度累加;
- 與控件
TagFlowLayout
本身寬度進行比較查看是否超出當前行捶障。
由圖2可知矫户,其實就是計算出每一行都有哪些View
,最容易想到的數(shù)據(jù)結(jié)構(gòu)就是:List<List<View>>
残邀。外層List
表示有多少行皆辽,里層List
表示每一行多少個子View
。
另外芥挣,需要一個臨時變量記錄住當前子View
已經(jīng)使用的空間驱闷,定義為currentLineTotalWidth
。
還需知道控件本身的寬度widthSize
空免,為了和當前所有View
已占用的空間進行寬度對比空另,代碼如下:
// 所有的view
private List<List<View>> mAllViews;
// 每一行的View
private List<View> mRowViewList;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
LogUtils.d("onMeasure: " + onMeasureCount++);
mAllViews.clear();
mRowViewList.clear();
// TagFlowLayout的寬度
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 當前行遍歷過程中子View的寬度累加
// 也可以當成當前行已使用的空間
int currentLineTotalWidth = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 獲取當前子View的寬度
int childWidth = childView.getMeasuredWidth();
// 一行已經(jīng)超出,另起一行
if (currentLineTotalWidth + childWidth > widthSize) {
// 重置當前currentLineTotalWidth
currentLineTotalWidth = 0;
// 添加當前行的所有子View
mAllViews.add(mRowViewList);
// 另起一行蹋砚,需要新開辟一個集合
mRowViewList = new ArrayList<>();
mRowViewList.add(childView);
// 最后一個控件單獨一行
if (i == (childCount - 1)) {
mAllViews.add(mRowViewList);
}
} else {
// 沒換行扼菠,繼續(xù)累加
currentLineTotalWidth += childView.getMeasuredWidth();
mRowViewList.add(childView);
// 最后一個view并且沒有超出寬度
if (i == (childCount - 1)) {
mAllViews.add(mRowViewList);
}
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
接下來,我們的目標是算出控件的寬高坝咐,并且設(shè)置進setMeasuredDimension
方法中循榆。
而最終的寬度就是所有行中最大的那個寬,所以每次新增加一個控件就可以比較兩個值中的最大值墨坚。而高度是累加秧饮,在每次換行時都加上上一行的高度。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 省略無關(guān)代碼 ...
// 測量最終的寬度
int selfMeasureWidth = 0;
// 測量最終的高度
int selfMeasureHeight = 0;
// 當前行的最大高度
int currentLineMaxHeight = 0;
if (currentLineTotalWidth + childWidth > widthSize) {
selfMeasureWidth = Math.max(selfMeasureWidth, currentLineTotalWidth);
selfMeasureHeight += currentLineMaxHeight;
currentLineMaxHeight = childHeight + marginLayout.topMargin + marginLayout.bottomMargin;
if (i == (childCount - 1)) {
selfMeasureHeight += currentLineMaxHeight;
}
} else {
currentLineMaxHeight = Math.max(currentLineMaxHeight, (childHeight + marginLayout.topMargin + marginLayout.bottomMargin));
currentLineTotalWidth += childView.getMeasuredWidth();
selfMeasureWidth = Math.max(selfMeasureWidth, currentLineTotalWidth);
if (i == (childCount - 1)) {
selfMeasureHeight += currentLineMaxHeight;
}
}
setMeasuredDimension(selfMeasureWidth, selfMeasureHeight);
}
到此為止泽篮,控件的onMeasure
方法就已基本完成盗尸。
3.3 onLayout方法實現(xiàn)過程
接下來就是擺放位置,分別從總的控件集合中取出相應的控件帽撑,然后進行擺放泼各,坐標位置就是控件的上下左右四個點。
在橫向上亏拉,如果一行有多個控件扣蜻,則進行寬度的累加來確定其他子View
的位置寸癌。
在縱向上,主要就是確定每一行的起始高度位置弱贼。
而針對這個起始高度位置蒸苇,我們在測量onMeasure
過程中,會保存一個高度集合mHeightList吮旅,記錄每一行最大高度溪烤,將在onLayout
中使用。
主要代碼如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
LogUtils.d("onLayout: " + onLayoutCount++);
int alreadyUsedWidth;
// 擺放的開始高度位置
int beginHeight = 0;
int left, top, right, bottom;
// mHeightList存放了每一行的最大高度
if (mHeightList.size() != mAllViews.size()) {
LogUtils.e("mHeightList's size is not equal to mAllViews's size");
return;
}
for (int i = 0; i < mAllViews.size(); i++) {
List<View> rowList = mAllViews.get(i);
if (rowList == null) continue;
// 每一行開始的擺放的位置alreadyUsedWidth庇勃,累加后檬嘀,成為每一行已經(jīng)使用的空間
alreadyUsedWidth = getPaddingLeft();
beginHeight += mHeightList.get(i);
if (i == 0) {
beginHeight += getPaddingTop();
}
for (int j = 0; j < rowList.size(); j++) {
View childView = rowList.get(j);
MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();
left = alreadyUsedWidth + params.leftMargin;
right = left + childView.getMeasuredWidth();
top = beginHeight + params.topMargin;
bottom = top + childView.getMeasuredHeight();
childView.layout(left, top, right, bottom);
alreadyUsedWidth = right;
}
}
}
3.4 關(guān)于實現(xiàn)子 View MarginLayout的布局
默認情況下,ViewGroup的LayoutParams為ViewGroup.LayoutParams责嚷,如需要使用margin_left這類屬性操作鸳兽,需要重寫generateLayoutParams方法。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
綜上罕拂,就是流式布局的基本原理揍异。如有錯誤,歡迎指出討論爆班。
4 寫在最后
在這個微涼的早餐終于完成了這篇文章的編寫衷掷,前幾天和一朋友喝茶聊天,聊到了文章創(chuàng)作其實也是一種服務柿菩,服務于其他有需求的人戚嗅。而服務意識又是打開另一扇大門的一種重要品質(zhì),未來希望能創(chuàng)作更多更好的服務枢舶。
如果喜歡懦胞,歡迎點贊與分享,創(chuàng)作不易凉泄,你的支持將是最好的回饋躏尉。