自定義 FlowLayout
在這里什么是一個 FlowLayout 盖奈,就是我們在很多軟件的搜索頁面看到的一些搜索標(biāo)簽氧敢,先上一張圖吧笋庄。
就是根據(jù)子 View 的長度自動換行,當(dāng)然 Android 9.0 也出了一個類似的控件看幼,但是考慮到之前的版本,我這里自己擼一個幌陕,也順便熟悉熟悉整個流程诵姜。
這里想寫一個 FlowLayout 也就是重寫一個 ViewGroup,我自己覺得寫一個
ViewGroup 要比寫一個 View 要麻煩搏熄,不僅要考慮適配 WRAP_CONTENT棚唆,同時還要處理自身的長度和子 View 的擺放暇赤,但是我們可以對 Android 的一些 ViewGroup 的實(shí)現(xiàn)有所了解。當(dāng)然有時候可以曲線救國宵凌,有時候我覺得有些太底層的沒必要去自己實(shí)現(xiàn)鞋囊,而且自己寫的肯定沒有 Android 原生的考慮的周全,我就可以直接繼承已有的 ViewGroup 比如 LinearLayout瞎惫、RelativieLayout 之類的溜腐,曲線救國~~
自定義 FlowLayout 的基本步驟
- 重寫OnMeasure
- 重寫OnLayout
這里分別是 父ViewGroup 對我們 measure 和 layout 的方法的回調(diào)方法,我們在這里獲取到我們測量的大小和位置微饥,接著可以繼續(xù)去測量我們的子 Item 的大小和位置逗扒。
重寫OnMeasure
空的onMeasure如下所示
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
}
首先第一步要先獲取測量模式和大小
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
這個上面的 widthMeasureSpec 和 heightMeasureSpec 變量是OnMeasure的回調(diào)參數(shù),在這里我們獲取到了寬度和高度的大小和分別的測量模式欠橘,測量模式分成三種矩肩,但是我們在這里只討論常見的兩種 EXACTLY 和 AT_MOST, 前者對應(yīng)著指定寬度和 MATCH_PARENT,后者對應(yīng)著 WRAP_CONTENT肃续。我們在這個方法里最想確定自己在 MATCH_PARENT 和 WRAP_CONTENT 下長寬如何黍檩,這里還要注意一點(diǎn)就是如果你重寫一個 ViewGroup,如果沒有針對 WRAP_CONTENT 重寫 始锚,那么你的長寬和 MATCH_PARENT 模式下沒有區(qū)別刽酱。
我們在 onMeasure 主要是測量子 View 所有長度加起來的和,當(dāng)然不是盲目的加瞧捌,廢話不多說先上代碼棵里。
int wrapWidth = 0;
int wrapHeight = 0;
int lineWidth = 0;
int lineHeight = 0;
int cCount = getChildCount();
for (int i = 0; i < cCount; i++) {
// 得到第 i 個 View
View child = getChildAt(i);
// 這里要十分注意,如果這里不先 measureChild 是無法得到 child 的寬度和高度
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
// 獲得子 View 所占的寬度和高度的時候要加上他自身的 margin
int childWidth = child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin
+ lp.bottomMargin;
// 判斷是否需要換行 要減去左右的 padding
if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {
// 需要換行的時候
wrapWidth = Math.max(wrapWidth, lineWidth);
lineWidth = childWidth;
wrapHeight += lineHeight;
lineHeight = childHeight;
} else {
// 不需要換行的時候
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
// 特殊考慮姐呐,要考慮到獲得 wrapWidth 和 wrapHeight
if (i == cCount - 1) {
wrapWidth = Math.max(lineWidth, wrapWidth);
wrapHeight += lineHeight;
}
}
這上面有兩個變量要關(guān)注下殿怜,wrapWidth 和 wrapHeight 記錄著 WRAP_CONTENT 時的長和寬,原因也知道了曙砂,WRAP_CONTENT 的效果需要我們自己實(shí)現(xiàn)退个,要不然就和 MATCH_PARENT 一模一樣辐董。另一個需要注意的點(diǎn)就是上面調(diào)用了很重要的函數(shù)就是 measureChild 這是 ViewGroup 自帶的函數(shù)斗躏,作用就是測量子 View 的長和寬层释,這個很好理解,我作為一個父 ViewGroup笑陈,在WRAP_CONTENT 的模式下际度,連自己的子 View 的長寬都不清楚,我怎么確定自己的長寬大小新锈,最后還需要注意最后一個要另外記錄 wrapWidth 和 wrapHeight 甲脏,其它的大家自己可以通過閱讀代碼理解。
最后我們當(dāng)然要調(diào)用設(shè)置自己寬高的函數(shù) setMeasuredDimension ,代碼如下块请,包括不同的測量模式下娜氏,大小不一樣。
setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth : wrapWidth + getPaddingLeft() + getPaddingRight(), modeHeight == MeasureSpec.EXACTLY ? sizeHeight : wrapHeight + getPaddingTop() + getPaddingBottom());
重寫 OnLayout
我們在這個函數(shù)里面通過調(diào)用子 View 的layout來確定子 View 的位置墩新。這個方法的思路和上面的有相似的也有不同的贸弥,我們需要通過兩個變量來儲存每行有哪些 View 和每一行的高度,同時我們還得注意考慮一種情況海渊,如果這個子 View 的狀態(tài)是 GONE, 我們就需要跳過它绵疲,同時在計算間距的時候還別忘了加上 父 ViewGroup 的padding 和自身的 margin ,按照這個思路我們需要遍歷兩遍所有的子 View臣疑,其它的跟上面的也很類似盔憨,關(guān)鍵就是控制哪些 View 是一行的,以及什么時候換行讯沈,這都又自己想要達(dá)到的效果決定的郁岩。這里因?yàn)楦厦娴念愃疲疫@里就不一一講述了缺狠,下面直接給出所有的代碼问慎。
所有代碼
public class FlowLayout extends ViewGroup {
private static final String TAG = "FlowLayout";
public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowLayout(Context context) {
this(context, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
Log.d(TAG, "" + sizeWidth);
// 如果是warp_content情況下,記錄寬和高
int width = 0;
int height = 0;
// 記錄每一行的寬度與高度
int lineWidth = 0;
int lineHeight = 0;
// 得到內(nèi)部元素的個數(shù)
int count = getChildCount();
for (int i = 0; i < count; i++) {
// 通過索引拿到每一個子view
View child = getChildAt(i);
// 測量子View的寬和高,系統(tǒng)提供的measureChild
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 得到LayoutParams
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
// 子View占據(jù)的寬度
int childWidth = child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
// 子View占據(jù)的高度
int childHeight = child.getMeasuredHeight() + lp.topMargin
+ lp.bottomMargin;
// 換行 判斷 當(dāng)前的寬度大于 開辟新行
if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {
// 對比得到最大的寬度
width = Math.max(width, lineWidth);
// 重置lineWidth
lineWidth = childWidth;
// 記錄行高
height += lineHeight;
lineHeight = childHeight;
} else
// 未換行
{
// 疊加行寬
lineWidth += childWidth;
// 得到當(dāng)前行最大的高度
lineHeight = Math.max(lineHeight, childHeight);
}
// 特殊情況,最后一個控件
if (i == count - 1) {
width = Math.max(lineWidth, width);
height += lineHeight;
}
}
setMeasuredDimension(
modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()//
);
}
/**
* 存儲所有的View
*/
private List<List<View>> allViews = new ArrayList<List<View>>();
/**
* 每一行的高度
*/
private List<Integer> mLineHeight = new ArrayList<Integer>();
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
allViews.clear();
mLineHeight.clear();
// 當(dāng)前ViewGroup的寬度
int width = getWidth();
int lineWidth = 0;
int lineHeight = 0;
// 存放每一行的子view
List<View> lineViews = new ArrayList<>();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 如果需要換行
if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - getPaddingLeft() - getPaddingRight()) {
// 記錄LineHeight
mLineHeight.add(lineHeight);
// 記錄當(dāng)前行的Views
allViews.add(lineViews);
// 重置我們的行寬和行高
lineWidth = 0;
lineHeight = childHeight + lp.topMargin + lp.bottomMargin;
// 重置我們的View集合
lineViews = new ArrayList<View>();
}
lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
lineHeight = Math.max(lineHeight, childHeight + lp.topMargin
+ lp.bottomMargin);
lineViews.add(child);
}// for end
// 處理最后一行
mLineHeight.add(lineHeight);
allViews.add(lineViews);
// 設(shè)置子View的位置
int left = getPaddingLeft();
int top = getPaddingTop();
// 行數(shù)
int lineNum = allViews.size();
for (int i = 0; i < lineNum; i++) {
// 當(dāng)前行的所有的View
lineViews = allViews.get(i);
lineHeight = mLineHeight.get(i);
for (int j = 0; j < lineViews.size(); j++) {
View child = lineViews.get(j);
// 判斷child的狀態(tài)
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
// 為子View進(jìn)行布局
child.layout(lc, tc, rc, bc);
left += child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
}
left = getPaddingLeft();
top += lineHeight;
}
}
/**
* 與當(dāng)前ViewGroup對應(yīng)的LayoutParams
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
}
效果圖如下
最后的總結(jié)
自定義一個 ViewGroup 最少需要重寫這兩個方法挤茄,當(dāng)然也可以重寫他自己的 onDraw方法如叼,我們這里最需要注意的就是 onMeasure 里面的測量模式,而且要記住穷劈,要自己來控制 WRAP_CONTENT 下自身的大小笼恰。