近期工作需要用到流式布局碑定,網(wǎng)上也有很多關(guān)于這方面的資料慎皱。發(fā)現(xiàn)流式布局與網(wǎng)格布局的自定義很有意思裸扶,是學(xué)習(xí)自定義控件的一個(gè)很好的方式铡买,所以就擼了個(gè)幾百行代碼的控件更鲁,既實(shí)用又具有學(xué)習(xí)價(jià)值。
一奇钞、AutoFlowLayout應(yīng)用場景
流式布局澡为,在很多標(biāo)簽類的場景中可以用的;而網(wǎng)格布局在分類中以及自拍九宮格等場景很常見景埃。如下所示:
如此使用頻繁而又實(shí)現(xiàn)簡單的控件媒至,怎能不自己擼一個(gè)呢?控件谷徙,還是定制的好啊拒啰。
二、AutoFlowLayout實(shí)現(xiàn)效果
先介紹下自己擼的這個(gè)控件的功能及效果完慧。
1.功能
流式布局
- 自動(dòng)換行
- 行數(shù)自定:單行/多行
- 支持單選/多選
- 支持行居中/靠左顯示
- 支持添加/刪除子View
- 支持子View點(diǎn)擊/長按事件
網(wǎng)格布局
- 行數(shù)/列數(shù)自定
- 支持單選/多選
- 支持添加/刪除子View
- 支持子View點(diǎn)擊/長按事件
- 支持添加多樣式分割線及橫豎間隔
2.效果
下面以gif圖的形式展現(xiàn)下實(shí)現(xiàn)的效果谋旦,樣式簡單了些,不過依然能展示出這個(gè)簡單控件的多功能實(shí)用性屈尼。
流式布局
網(wǎng)格布局
最后一個(gè)是帶間隔以及分割線的册着,由于錄屏原因,只在跳過去的一瞬間顯示了粉紅色的一條線脾歧。真實(shí)如下圖所示甲捏,可以定義橫豎間距的大小,以及分割線的顏色涨椒,寬度摊鸡。
Github地址:AutoFlowLayout
三、AutoFlowLayout使用
1.添加依賴
①.在項(xiàng)目的 build.gradle 文件中添加
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
②.在 module 的 build.gradle 文件中添加依賴
dependencies {
compile 'com.github.LRH1993:AutoFlowLayout:1.0.5'
}
2.屬性說明
下表是自定義的屬性說明蚕冬,可在xml中聲明免猾,同時(shí)有對應(yīng)的get/set方法,可在代碼中動(dòng)態(tài)添加囤热。
3.使用示例
布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.library.AutoFlowLayout
android:id="@+id/afl_cotent"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>
代碼設(shè)置數(shù)據(jù)
mFlowLayout.setAdapter(new FlowAdapter(Arrays.asList(mData)) {
@Override
public View getView(int position) {
View item = mLayoutInflater.inflate(R.layout.special_item, null);
TextView tvAttrTag = (TextView) item.findViewById(R.id.tv_attr_tag);
tvAttrTag.setText(mData[position]);
return item;
}
});
與ListView,GridView使用方式一樣猎提,實(shí)現(xiàn)FlowAdapter即可。
四旁蔼、AutoFlowLayout原理
ViewGroup的測量锨苏、布局及繪制順序如下所示:
詳細(xì)的自定義View原理參考:圖解View測量、布局及繪制原理
下面具體介紹自定義實(shí)現(xiàn)網(wǎng)格布局的過程棺聊。
1.重寫generateLayoutParams()方法
因?yàn)槲覀円趏nMeasure以及onLayout的過程中伞租,測量子View的margin,所以要重寫該方法限佩,并返回MarginLayoutParams葵诈。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs)
{
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(super.generateDefaultLayoutParams());
}
2.onMeasure過程
主要針對wrap_content情況下裸弦,要逐行逐列的測量每個(gè)子View的寬高,padding作喘,margin以及橫豎間距理疙,來獲得最終ViewGroup的寬高。
private void setGridMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 獲得它的父容器為它設(shè)置的測量模式和大小
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
//獲取viewgroup的padding
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//最終的寬高值
int heightResult;
int widthResult;
//未設(shè)置行數(shù) 推測行數(shù)
if (mRowNumbers == 0) {
mRowNumbers = getChildCount()%mColumnNumbers == 0 ?
getChildCount()/mColumnNumbers : (getChildCount()/mColumnNumbers + 1);
}
int maxChildHeight = 0;
int maxWidth = 0;
int maxHeight = 0;
int maxLineWidth = 0;
//統(tǒng)計(jì)最大高度/最大寬度
for (int i = 0; i < mRowNumbers; i++) {
for (int j = 0; j < mColumnNumbers; j++) {
final View child = getChildAt(i * mColumnNumbers + j);
if (child != null) {
if (child.getVisibility() != GONE) {
measureChild(child,widthMeasureSpec,heightMeasureSpec);
// 得到child的lp
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
maxLineWidth +=child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin);
}
}
}
maxWidth = Math.max(maxLineWidth,maxWidth);
maxLineWidth = 0;
maxHeight += maxChildHeight;
maxChildHeight = 0;
}
int tempWidth = (int) (maxWidth+mHorizontalSpace*(mColumnNumbers-1)+paddingLeft+paddingRight);
int tempHeight = (int) (maxHeight+mVerticalSpace*(mRowNumbers-1)+paddingBottom+paddingTop);
if (tempWidth > sizeWidth) {
widthResult = sizeWidth;
} else {
widthResult = tempWidth;
}
//寬高超過屏幕大小泞坦,則進(jìn)行壓縮存放
if (tempHeight > sizeHeight) {
heightResult = sizeHeight;
} else {
heightResult = tempHeight;
}
setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth
: widthResult, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight
: heightResult);
}
3.onLayout過程
網(wǎng)格布局默認(rèn)所有子View的寬高一致窖贤,先推算出每個(gè)子View的平均寬高,然后逐個(gè)推算每個(gè)子View的left,top,right,bottom位置贰锁,調(diào)用child.layout()進(jìn)行子View布局赃梧。
private void setGridLayout() {
mCheckedViews.clear();
mCurrentItemIndex = -1;
int sizeWidth = getWidth();
int sizeHeight = getHeight();
//子View的平均寬高 默認(rèn)所有View寬高一致
View tempChild = getChildAt(0);
MarginLayoutParams lp = (MarginLayoutParams) tempChild
.getLayoutParams();
int childAvWidth = (int) ((sizeWidth - getPaddingLeft() - getPaddingRight() - mHorizontalSpace * (mColumnNumbers-1))/mColumnNumbers)-lp.leftMargin-lp.rightMargin;
int childAvHeight = (int) ((sizeHeight - getPaddingTop() - getPaddingBottom() - mVerticalSpace * (mRowNumbers-1))/mRowNumbers)-lp.topMargin-lp.bottomMargin;
for (int i = 0; i < mRowNumbers; i++) {
for (int j = 0; j < mColumnNumbers; j++) {
final View child = getChildAt(i * mColumnNumbers + j);
if (child != null) {
mCurrentItemIndex++;
if (child.getVisibility() != View.GONE) {
setChildClickOperation(child, -1);
int childLeft = (int) (getPaddingLeft() + j * (childAvWidth + mHorizontalSpace))+j * (lp.leftMargin + lp.rightMargin) + lp.leftMargin;
int childTop = (int) (getPaddingTop() + i * (childAvHeight + mVerticalSpace)) + i * (lp.topMargin + lp.bottomMargin) + lp.topMargin;
child.layout(childLeft, childTop, childLeft + childAvWidth, childAvHeight +childTop);
}
}
}
}
}
4.dispatchDraw過程
繪制分割線得問過程,需要逐個(gè)對子View進(jìn)行繪制分割線李根。所以重寫dispatchDraw()方法槽奕。因?yàn)椴恍枰獙ψ约哼M(jìn)行繪制,所以不需要重寫onDraw()方法房轿。
需要額外注意下粤攒,繪制過程中,考慮橫豎間距的大小囱持,這種情況下默認(rèn)不考慮margin夯接。
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mIsGridMode && mIsCutLine) {
Paint linePaint = new Paint();
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(mCutLineWidth);
linePaint.setColor(mCutLineColor);
for (int i = 0; i < mRowNumbers; i++) {
for (int j = 0; j < mColumnNumbers; j++) {
View child = getChildAt(i * mColumnNumbers + j);
//最后一列
if (j == mColumnNumbers-1) {
//不是最后一行 只畫底部
if (i != mRowNumbers-1){
canvas.drawLine(child.getLeft()-mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,
child.getRight(),child.getBottom()+mVerticalSpace/2,linePaint);
}
} else {
//最后一行 只畫右部
if (i == mRowNumbers -1) {
canvas.drawLine(child.getRight()+mHorizontalSpace/2, child.getTop()-mVerticalSpace/2,
child.getRight()+mHorizontalSpace/2,child.getBottom(),linePaint);
} else {
//底部 右部 都畫
if (j == 0) {
canvas.drawLine(child.getLeft(),child.getBottom()+mVerticalSpace/2,
child.getRight()+mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,linePaint);
} else {
canvas.drawLine(child.getLeft()-mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,
child.getRight()+mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,linePaint);
}
if (i == 0) {
canvas.drawLine(child.getRight()+mHorizontalSpace/2, child.getTop(),
child.getRight()+mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,linePaint);
} else {
canvas.drawLine(child.getRight()+mHorizontalSpace/2, child.getTop()-mVerticalSpace/2,
child.getRight()+mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,linePaint);
}
}
}
}
}
}
}
繪制流式標(biāo)簽的過程類似,一樣的簡單纷妆。不過通過實(shí)現(xiàn)的過程盔几,確實(shí)加深了對自定義ViewGroup的理解。
Github地址:https://github.com/LRH1993/AutoFlowLayout
點(diǎn)個(gè)star掩幢,一起來學(xué)習(xí)自定義ViewGroup吧逊拍!