ScFlowLayout一款A(yù)ndroid基于ViewGroup實(shí)現(xiàn)的流式布局
本文原創(chuàng)踩麦,轉(zhuǎn)載請(qǐng)注明出處背镇。歡迎關(guān)注我的 簡書玖媚。
安利一波我寫的開發(fā)框架:MyScFrame喜歡的話就給個(gè)Star
ScFlowLayout
已經(jīng)加入到該框架內(nèi)
場(chǎng)景
最近在做一個(gè)聊天功能,其中需要給對(duì)方打標(biāo)簽钝计,第一時(shí)間想到的就是流式布局茉帅,目前項(xiàng)目上用的是鴻洋大神的FlowLayout糙申,功能很強(qiáng)大宾添,不過我項(xiàng)目上只用到了展示效果,讀了大神的源碼柜裸,給了我一些靈感缕陕,這里我也寫一個(gè)FlowLayout,并且參考了一些Recycler.Adapter的做法疙挺。
參考資料
hongyangAndroid/FlowLayout
Android流式布局(FlowLayout)
自定義View扛邑、動(dòng)畫
實(shí)現(xiàn)功能
- 使用adapter的形式綁定并處理數(shù)據(jù)
- 支持多種布局一同展示
- 支持多行,單行,指定顯示行數(shù)
- 支持Item左對(duì)齊,居中對(duì)齊,右對(duì)齊
- 支持行布局頂部對(duì)齊,居中對(duì)齊,底部對(duì)齊
- 支持選中狀態(tài)
- 支持設(shè)置行間距
- 支持設(shè)置item間距
ScFlowLayout
1.思路
使用SparseArray<LineDes> mLineDesArray;
保存每行的數(shù)據(jù),其中包括行高,行寬以及該行中包含的View的集合铐然。
使用BaseTagFlowAdapter mAdapter;
來管理數(shù)據(jù)加載蔬崩,點(diǎn)擊事件,選中事件搀暑。
2.onMeasure()中的業(yè)務(wù)邏輯
在onMeasure()
中
首先遍歷測(cè)量子View沥阳,將子View的頂點(diǎn)坐標(biāo)通過view.setTag()
方法保存,同時(shí)把每行的數(shù)據(jù)保存在LineDes
中自点,這樣寫是為了后續(xù)在onLayout()
好處理桐罕,不用重復(fù)計(jì)算。
接下來我們?cè)谡{(diào)用setMeasuredDimension()
方法之前需要給出布局的寬跟高樟氢,我這邊是通過getLayoutParams().width
與getLayoutParams().height
來判斷布局的寬高冈绊,至于為什么要這樣寫大家可以參考這篇文章,如果想要實(shí)現(xiàn)指定行數(shù)的話需要遍歷每行高度埠啃,然后累加到mMeasuredHeight
中。
在計(jì)算高度的時(shí)候伟恶,由于我這里實(shí)現(xiàn)了自定義行間距碴开,因此實(shí)際計(jì)算高度的時(shí)候還需要加上行間距的高度。
//由于計(jì)算子view所占寬度
Map<String, Integer> compute = compute(widthSize, widthMeasureSpec, heightMeasureSpec);
mMeasuredWidth = widthSize;
mMeasuredHeight = heightSize;
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
mMeasuredWidth = compute.get(ALL_CHILD_WIDTH);
}
if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
mMeasuredHeight = compute.get(ALL_CHILD_HEIGHT);
if (mLineDesArray.size() > 1) {
//加上行間距
mMeasuredHeight += mLineSpace * (mLineDesArray.size() - 1);
}
}
if (mMaxShowRow != 0) {
mMeasuredHeight = 0;
int lineCount = Math.min(mLineDesArray.size(), mMaxShowRow);
for (int i = 0; i < lineCount; i++) {
mMeasuredHeight += mLineDesArray.get(i).rowsMaxHeight;
}
mMeasuredHeight += getPaddingBottom();
if (lineCount > 1) {
//加上行間距
mMeasuredHeight += mLineSpace * (lineCount - 1);
}
}
Map<String, Integer> compute(int flowWidth, int widthMeasureSpec, int heightMeasureSpec)
是遍歷子View的方法(整個(gè)控件都靠它了)
我們先要設(shè)置幾個(gè)參數(shù):
int lineIndex
行數(shù)
int rowsWidth
當(dāng)前行已占寬度
int columnHeight
當(dāng)前行頂部已占高度
int rowsMaxHeight
當(dāng)前行所有子元素的最大高度(用于換行累加高度)
LineDes lineDes
保存每行數(shù)據(jù)的bean類
思路是先遍歷所有子View博秫,然后計(jì)算出每個(gè)子View所占用的寬高潦牛,child.getMeasuredWidth()
計(jì)算出來的是包含子View中的Padding
參數(shù),但是不包含Margin
挡育,所以這里實(shí)際寬高還需要加上Margin
不然會(huì)導(dǎo)致實(shí)際大小與計(jì)算出來的不符
//遍歷去調(diào)用所有子元素的measure方法(child.getMeasuredHeight()才能獲取到值巴碗,否則為0)
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//獲取元素測(cè)量寬度和高度
int measuredWidth = child.getMeasuredWidth();
int measuredHeight = child.getMeasuredHeight();
//獲取元素的margin
marginParams = (MarginLayoutParams) child.getLayoutParams();
//子元素所占寬度 = MarginLeft+ child.getMeasuredWidth+MarginRight 注意此時(shí)不能child.getWidth,因?yàn)榻缑鏇]有繪制完成,此時(shí)wdith為0
int childWidth = marginParams.leftMargin + marginParams.rightMargin + measuredWidth;
int childHeight = marginParams.topMargin + marginParams.bottomMargin + measuredHeight;
得到每個(gè)子View的寬高后就要開始計(jì)算行數(shù)以及每行所存放的View的數(shù)量了
我們之前已經(jīng)有了一個(gè)rowsWidth
參數(shù)即寒,默認(rèn)值是getPaddingLeft()
橡淆,然后加上childWidth
看是否會(huì)超過父布局的寬度召噩,這邊還需要減去一個(gè)getPaddingRight()
切記切記!逸爵,如果超了具滴,表示這個(gè)View已經(jīng)無法存放在該行,需要換行师倔。最后使用Rect
把子View的寬高賦值進(jìn)去构韵,然后保存在tag中,方便后續(xù)使用趋艘。
//該布局添加進(jìn)去后會(huì)超過總寬度->換行
if (rowsWidth + childWidth > flowWidth - getPaddingRight()) {
getLineDesArray().put(lineIndex, lineDes);
lineDes = new LineDes();
lineIndex++;
//重置行寬度
rowsWidth = getPaddingLeft();
//累加上該行子元素最大高度
columnHeight += rowsMaxHeight;
//重置該行最大高度
rowsMaxHeight = childHeight;
} else {
rowsMaxHeight = Math.max(rowsMaxHeight, childHeight);
}
//累加上該行子元素寬度
rowsWidth += childWidth;
// 判斷時(shí)占的寬段時(shí)加上margin計(jì)算疲恢,設(shè)置頂點(diǎn)位置時(shí)不包括margin位置,
// 不然margin會(huì)不起作用瓷胧,這是給View設(shè)置tag,在onlayout給子元素設(shè)置位置再遍歷取出
Rect rect = new Rect(
rowsWidth - childWidth + marginParams.leftMargin,
columnHeight + marginParams.topMargin,
rowsWidth - marginParams.rightMargin,
columnHeight + childHeight - marginParams.bottomMargin);
child.setTag(rect);
lineDes.rowsMaxHeight = rowsMaxHeight;
lineDes.rowsMaxWidth = rowsWidth;
lineDes.views.add(child);
//累加上item間距
rowsWidth += mItemSpace;
3.onLayout()中的業(yè)務(wù)邏輯
在onLayout()
中冈闭,通過所需要實(shí)現(xiàn)的類型去做不同的排版
因?yàn)檫@里我們實(shí)現(xiàn)了行間上,中抖单,下與Item間的左萎攒,中,右對(duì)齊因此矛绘,這里需要有針對(duì)行與Item做兩次處理
我們先設(shè)置一個(gè)diffvalue
用于存放位移參數(shù)耍休。
為了讓行內(nèi)所有布局都居中對(duì)齊或下對(duì)齊,那么我們要先知道每行有多少個(gè)元素货矮,以及行高與元素高度羊精,這個(gè)時(shí)候LineDes
就派上用場(chǎng)了,之前在onMeasure()
中我們已經(jīng)計(jì)算并保存了LineDes
囚玫,現(xiàn)在只需要遍歷LineDes
即可喧锦,由于系統(tǒng)在繪制的時(shí)候就是使用頂部對(duì)齊,因此LINE_GRAVITY_TOP
不需要做處理抓督,我們只需要處理LINE_GRAVITY_CENTER
和LINE_GRAVITY_BOTTOM
即可
LINE_GRAVITY_CENTER
:diffvalue = (lineDes.rowsMaxHeight - childWidth) / 2;
LINE_GRAVITY_BOTTOM
:diffvalue = lineDes.rowsMaxHeight - childWidth;
再來說一下Item間的排版燃少,同樣的TAG_GRAVITY_LEFT
可以不做處理
LINE_GRAVITY_CENTER
:diffvalue = (mMeasuredWidth - getPaddingRight() - lineDes.rowsMaxWidth) / 2;
LINE_GRAVITY_BOTTOM
:diffvalue = mMeasuredWidth - lineDes.rowsMaxWidth - getPaddingRight();
改完重新寫入Rect
中并傳入子View的layout()
中即可。
private synchronized void formatAboveLine(int lineGravity) {
int lineIndex = getLineDesArray().size();
for (int i = 0; i < lineIndex; i++) {
LineDes lineDes = getLineDesArray().get(i);
List<View> views = lineDes.views;
int viewIndex = views.size();
for (int j = 0; j < viewIndex; j++) {
View child = views.get(j);
Rect rect = (Rect) child.getTag();
int childWidth = (rect.bottom - rect.top);
//如果是當(dāng)前行的高度大于了該view的高度話铃在,此時(shí)需要重新放該view了
int diffvalue = 0;
if (childWidth < lineDes.rowsMaxHeight) {
switch (lineGravity) {
case LINE_GRAVITY_TOP:
break;
case LINE_GRAVITY_CENTER:
diffvalue = (lineDes.rowsMaxHeight - childWidth) / 2;
rect.top += diffvalue;
rect.bottom += diffvalue;
break;
case LINE_GRAVITY_BOTTOM:
diffvalue = lineDes.rowsMaxHeight - childWidth;
rect.top += diffvalue;
rect.bottom += diffvalue;
break;
default:
break;
}
}
switch (mTagGravity) {
case TAG_GRAVITY_LEFT:
break;
case TAG_GRAVITY_CENTER:
diffvalue = (mMeasuredWidth - getPaddingRight() - lineDes.rowsMaxWidth) / 2;
if (diffvalue > 0) {
rect.left += diffvalue;
rect.right += diffvalue;
}
break;
case TAG_GRAVITY_RIGHT:
diffvalue = mMeasuredWidth - lineDes.rowsMaxWidth - getPaddingRight();
rect.left += diffvalue;
rect.right += diffvalue;
break;
default:
break;
}
//加上行間距
rect.top += mLineSpace * i;
rect.bottom += mLineSpace * i;
child.layout(rect.left, rect.top, rect.right, rect.bottom);
}
}
getLineDesArray().clear();
}
適配器
參考BaseRecyclerViewAdapterHelper
實(shí)現(xiàn)的一個(gè)Adapter
與ViewHolder
用于綁定相關(guān)數(shù)據(jù)阵具,并處理點(diǎn)擊,選中等事件定铜。
1.思路
使用SparseIntArray mLayoutResIds
保存layoutId阳液,實(shí)現(xiàn)多布局樣式。
使用SparseArray<ArrayList<Integer>> mCheckedStateViewResIds
保存需要實(shí)現(xiàn)選中狀態(tài)的子ViewId
使用HashMap<Integer, TagView> mCheckedPosList
保存選中的View揣炕,實(shí)現(xiàn)單選帘皿,多選等功能
2.加載布局
像RecyclerView.Adapter
一樣,我們把data
傳進(jìn)來畸陡,然后遍歷數(shù)據(jù)鹰溜,通過ViewType
來判斷到底使用mLayoutResIds
中的哪個(gè)布局虽填,并且遍歷 mCheckedStateViewResIds
對(duì)需要做選中狀態(tài)變更的view設(shè)置setDuplicateParentStateEnabled(true)
,然后把實(shí)例出來的View傳入ViewHolder
最后加載出來奉狈。
private void addNewView() {
mFlowLayout.removeAllViews();
mCheckedPosList.clear();
TagView tagViewContainer = null;
K baseViewHolder = null;
T data = null;
int viewType = DEFAULT_VIEW_TYPE;
for (int i = 0; i < getCount(); i++) {
data = getItem(i);
viewType = getDefItemViewType(data);
baseViewHolder = onCreateViewHolder(mFlowLayout, viewType, i);
tagViewContainer = new TagView(mContext);
//關(guān)鍵代碼,使得內(nèi)部View可以使用TagView的狀態(tài)
if (mCheckedStateViewResIds != null) {
ArrayList<Integer> viewResId = mCheckedStateViewResIds.get(viewType, new ArrayList<Integer>());
for (Integer stateViewId : viewResId) {
View stateView = baseViewHolder.getView(stateViewId.intValue());
if (stateView != null) {
stateView.setDuplicateParentStateEnabled(true);
}
}
}
baseViewHolder.itemView.setDuplicateParentStateEnabled(true);
if (baseViewHolder.itemView.getLayoutParams() != null) {
tagViewContainer.setLayoutParams(baseViewHolder.itemView.getLayoutParams());
} else {
ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
lp.setMargins(leftMargin, topMargin, rightMargin, bottomMargin);
tagViewContainer.setLayoutParams(lp);
}
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
baseViewHolder.itemView.setLayoutParams(lp);
tagViewContainer.addView(baseViewHolder.itemView);
//處理選中與非選中邏輯
if (setDefSelected(data, i)) {
if (mSelectedMax == 1 && mCheckedPosList.size() > 0) {
int oldSelected = 0;
TagView oldTagView;
for (Map.Entry<Integer, TagView> entry : mCheckedPosList.entrySet()) {
oldSelected = entry.getKey();
oldTagView = entry.getValue();
setChildUnChecked(oldSelected, oldTagView);
}
mCheckedPosList.clear();
}
mCheckedPosList.put(i, tagViewContainer);
setChildChecked(i, tagViewContainer);
}
mFlowLayout.addView(tagViewContainer);
convert(baseViewHolder, data);
bindViewClickListener(tagViewContainer, baseViewHolder);
}
}
3.ViewHolder
ViewHolder
里面只是保存一些常用數(shù)據(jù),方便在使用的時(shí)候調(diào)用
private final SparseArray<View> views;
private final LinkedHashSet<Integer> childClickViewIds;//需要添加點(diǎn)擊事件的子View
private final LinkedHashSet<Integer> itemChildLongClickViewIds;//需要添加點(diǎn)擊事件的子View
private final HashSet<Integer> nestViews;//需要添加兩種點(diǎn)擊事件的子View
public final View itemView;
private BaseTagFlowAdapter adapter;
private int position = -1;
private int viewType = BaseTagFlowAdapter.DEFAULT_VIEW_TYPE;
public BaseTagFlowViewHolder(final View view) {
this.itemView = view;
this.views = new SparseArray<>();
this.childClickViewIds = new LinkedHashSet<>();
this.itemChildLongClickViewIds = new LinkedHashSet<>();
this.nestViews = new HashSet<>();
}
歡迎大家留言指出我的不足卤唉。