AutoFlowLayout多功能流式佈局與網格佈局控制元件詳解
近期工作需要用到流式佈局,網上也有很多關於這方面的資料。發現流式佈局與網格佈局的自定義很有意思,是學習自定義控制元件的一個很好的方式,所以就擼了個幾百行程式碼的控制元件,既實用又具有學習價值。
一、AutoFlowLayout應用場景
流式佈局,在很多標籤類的場景中可以用的;而網格佈局在分類中以及自拍九宮格等場景很常見。如下所示:

如此使用頻繁而又實現簡單的控制元件,怎能不自己擼一個呢?控制元件,還是定製的好啊。
二、AutoFlowLayout實現效果
先介紹下自己擼的這個控制元件的功能及效果。
1.功能
流式佈局
- 自動換行
- 行數自定:單行/多行
- 支援單選/多選
- 支援行居中/靠左顯示
- 支援新增/刪除子View
- 支援子View點選/長按事件
網格佈局
- 行數/列數自定
- 支援單選/多選
- 支援新增/刪除子View
- 支援子View點選/長按事件
- 支援新增多樣式分割線及橫豎間隔
2.效果
下面以gif圖的形式展現下實現的效果,樣式簡單了些,不過依然能展示出這個簡單控制元件的多功能實用性。
流式佈局


網格佈局

最後一個是帶間隔以及分割線的,由於錄屏原因,只在跳過去的一瞬間顯示了粉紅色的一條線。真實如下圖所示,可以定義橫豎間距的大小,以及分割線的顏色,寬度。

iage
Github地址:AutoFlowLayout
三、AutoFlowLayout使用
1.新增依賴
①.在專案的 build.gradle 檔案中新增
allprojects { repositories { ... maven { url 'https://jitpack.io' } } }
②.在 module 的 build.gradle 檔案中新增依賴
dependencies { compile 'com.github.LRH1993:AutoFlowLayout:1.0.5' }
2.屬性說明
下表是自定義的屬性說明,可在xml中宣告,同時有對應的get/set方法,可在程式碼中動態新增。

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>
程式碼設定資料
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使用方式一樣,實現FlowAdapter即可。
四、AutoFlowLayout原理
ViewGroup的測量、佈局及繪製順序如下所示:

詳細的自定義View原理參考:圖解View測量、佈局及繪製原理
下面具體介紹自定義實現網格佈局的過程。
1.重寫generateLayoutParams()方法
因為我們要在onMeasure以及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情況下,要逐行逐列的測量每個子View的寬高,padding,margin以及橫豎間距,來獲得最終ViewGroup的寬高。
private void setGridMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 獲得它的父容器為它設定的測量模式和大小 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; //未設定行數 推測行數 if (mRowNumbers == 0) { mRowNumbers = getChildCount()%mColumnNumbers == 0 ? getChildCount()/mColumnNumbers : (getChildCount()/mColumnNumbers + 1); } int maxChildHeight = 0; int maxWidth = 0; int maxHeight = 0; int maxLineWidth = 0; //統計最大高度/最大寬度 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; } //寬高超過螢幕大小,則進行壓縮存放 if (tempHeight > sizeHeight) { heightResult = sizeHeight; } else { heightResult = tempHeight; } setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth : widthResult, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight : heightResult); }
3.onLayout過程
網格佈局預設所有子View的寬高一致,先推算出每個子View的平均寬高,然後逐個推算每個子View的left,top,right,bottom位置,呼叫child.layout()進行子View佈局。
private void setGridLayout() { mCheckedViews.clear(); mCurrentItemIndex = -1; int sizeWidth = getWidth(); int sizeHeight = getHeight(); //子View的平均寬高 預設所有View寬高一致 ViewtempChild = getChildAt(0); MarginLayoutParamslp = (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過程
繪製分割線得問過程,需要逐個對子View進行繪製分割線。所以重寫dispatchDraw()方法。因為不需要對自己進行繪製,所以不需要重寫onDraw()方法。
需要額外注意下,繪製過程中,考慮橫豎間距的大小,這種情況下預設不考慮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); } } } } } } }
繪製流式標籤的過程類似,一樣的簡單。不過通過實現的過程,確實加深了對自定義ViewGroup的理解。
Github地址:https://github.com/LRH1993/AutoFlowLayout
點個star,一起來學習自定義ViewGroup吧!
自己是從事了七年開發的Android工程師,不少人私下問我,2019年Android進階該怎麼學,方法有沒有?
沒錯,年初我花了一個多月的時間整理出來的學習資料,希望能幫助那些想進階提升Android開發,卻又不知道怎麼進階學習的朋友。【包括高階UI、效能優化、架構師課程、NDK、Kotlin、混合式開發(ReactNative+Weex)、Flutter等架構技術資料 】,希望能幫助到您面試前的複習且找到一個好的工作,也節省大家在網上搜索資料的時間來學習。
資料獲取方式:加入Android架構交流QQ群聊:513088520 ,進群即領取資料!!!
點選連結加入群聊【Android移動架構總群】:加入群聊

資料大全