2021-05-12 14:32:11
Android FlowLayout流式佈局實現詳解
本文範例為大家分享了Android FlowLayout流式佈局的具體程式碼,供大家參考,具體內容如下
最近使用APP的時候經常看到有
這種流式佈局 ,今天我就跟大家一起來動手擼一個這種自定義控制元件.
首先說一下自定義控制元件的流程:
自定義控制元件一般要麼繼承View要麼繼承ViewGroup
View的自定義流程:
繼承一個View-->重寫onMeasure方法-->重寫onDraw方法-->定義自定義屬性-->處理手勢操作
ViewGroup的自定義流程:
繼承一個ViewGroup-->重寫onMeasure方法-->重寫onLayout-->重寫onDraw方法->定義自定義屬性-->處理手勢操作
我們可以看到自定義View和自定義ViewGroup略微有些不同,自定義ViewGroup多了個onlayout方法,那麼這些方法都有什麼作用呢?這裡由於篇幅的問題不做過多的描述,簡單的說
onMeasure:用來計算,計算自身顯示在頁面上的大小
onLayout:用來計運算元View擺放的位置,因為View已經是最小單元了,所以沒有字View,所以沒有onLayout方法
onDraw:用來繪製你想展示的東西
定義自定義屬性就是暴露一些屬性給外部呼叫
好了,瞭解了自定義View的基本自定義流程,我們可以知道我們應該需要自定義一個ViewGroup就可以滿足該需求.
首先自定義一個View命名為FlowLayout繼承ViewGroup
public class FlowLayout extends ViewGroup { public FlowLayout(Context context) { this(context,null); } public FlowLayout(Context context, AttributeSet attrs) { this(context, attrs,0); } public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); for (int i = 0; i < count; i++) { getChildAt(i).layout(l,t,r,b); } } }
可以看到onLayout是必須重寫的,不然系統不知道你這個ViewGroup的子View擺放的位置.
然後XML中參照
<test.hxy.com.testflowlayout.FlowLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/mFlowLayout" android:layout_margin="10dp" android:layout_width="match_parent" android:layout_height="match_parent" />
Activity中設定資料
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FlowLayout mFlowLayout = (FlowLayout) findViewById(R.id.mFlowLayout); List<String> list = new ArrayList<>(); list.add("java"); list.add("javaEE"); list.add("javaME"); list.add("c"); list.add("php"); list.add("ios"); list.add("c++"); list.add("c#"); list.add("Android"); for (int i = 0; i < list.size(); i++) { View inflate = LayoutInflater.from(this).inflate(R.layout.item_personal_flow_labels, null); TextView label = (TextView) inflate.findViewById(R.id.tv_label_name); label.setText(list.get(i)); mFlowLayout.addView(inflate); } }
執行一下:
咦!!!這時候發現我們新增的子View竟然沒新增進去?這是為什麼呢?
這時候就不得不說一下onMeasure方法了,我們重寫一下onMeasure然後在看一下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int totalWidth = MeasureSpec.getSize(widthMeasureSpec); int totalHeight = MeasureSpec.getSize(heightMeasureSpec); int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight() - getPaddingLeft(); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); int modeWidth = MeasureSpec.getMode(widthMeasureSpec); int modeHeight = MeasureSpec.getMode(heightMeasureSpec); final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeWidth); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight); // 測量child child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } setMeasuredDimension(totalWidth, resolveSize(totalHeight, heightMeasureSpec)); }
在執行一下:
我們可以看到確實是有View顯示出來了,可是為什麼只有一個呢?
其實這裡顯示的不是隻有一個,而是所有的子View都蓋在一起了,所以看起來就像只有一個View,這是因為我們的onLayout裡面getChildAt(i).layout(l,t,r,b);所有的子View擺放的位置都是一樣的,所以這邊要注意一下,自定義ViewGroup的時候一般onLayout和onMeasure都必須重寫,因為這兩個方法一個是計運算元View的大小,一個是計運算元View擺放的位置,缺少一個子View都會顯示不出來.
接下來我們在改寫一下onLayout方法讓子View都顯示出來
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); for (int i = 0; i < count; i++) { getChildAt(i).layout(l,t,r,b); l+=getChildAt(i).getMeasuredWidth(); } }
這樣子View就不會重疊在一起了,可是又發現一個問題,就是子View都在排在同一行了,我們怎麼才能讓子View計算排滿一行就自動換行呢?
接下來我們定義一個行的類Line來儲存一行的子View:
class Line{ int mWidth = 0;// 該行中所有的子View累加的寬度 int mHeight = 0;// 該行中所有的子View中高度的那個子View的高度 List<View> views = new ArrayList<View>(); public void addView(View view) {// 往該行中新增一個 views.add(view); mWidth += view.getMeasuredWidth(); int childHeight = view.getMeasuredHeight(); mHeight = mHeight < childHeight ? childHeight : mHeight;//高度等於一行中最高的View } //擺放行中子View的位置 public void Layout(int l, int t){ } }
這樣我們就可以讓FlowLayout專門對Line進行擺放,然後Line專門對本行的View進行擺放
接下來針對Line我們重新寫一下onMeasure和onLayout方法
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int totalWidth = MeasureSpec.getSize(widthMeasureSpec); int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight() - getPaddingLeft(); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); restoreLine();// 還原資料,以便重新記錄 int modeWidth = MeasureSpec.getMode(widthMeasureSpec); int modeHeight = MeasureSpec.getMode(heightMeasureSpec); final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == View.GONE) { break; } int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeWidth); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight); // 測量child child.measure(childWidthMeasureSpec, childHeightMeasureSpec); if (mLine == null) { mLine = new Line(); } int measuredWidth = child.getMeasuredWidth(); mUsedWidth += measuredWidth;// 增加使用的寬度 if (mUsedWidth < sizeWidth) { //當本行的使用寬度小於行總寬度的時候直接加進line裡面 mLine.addView(child); mUsedWidth += mHorizontalSpacing;// 加上間隔 if (mUsedWidth >= sizeWidth){ if (!newLine()){ break; } } }else {// 使用寬度大於總寬度。需要換行 if (mLine.getViewCount() == 0){//如果這行一個View也沒有超過也得加進去,保證一行最少有一個View mLine.addView(child); if (!newLine()) {// 換行 break; } }else { if (!newLine()) {// 換行 break; } mLine.addView(child); mUsedWidth += measuredWidth + mHorizontalSpacing; } } } if (mLine !=null && mLine.getViewCount() > 0 && !mLines.contains(mLine)){ mLines.add(mLine); } int totalHeight = 0; final int linesCount = mLines.size(); for (int i = 0; i < linesCount; i++) {// 加上所有行的高度 totalHeight += mLines.get(i).mHeight; } totalHeight += mVerticalSpacing * (linesCount - 1);// 加上所有間隔的高度 totalHeight += getPaddingTop() + getPaddingBottom();// 加上padding // 設定佈局的寬高,寬度直接採用父view傳遞過來的最大寬度,而不用考慮子view是否填滿寬度,因為該佈局的特性就是填滿一行後,再換行 // 高度根據設定的模式來決定採用所有子View的高度之和還是採用父view傳遞過來的高度 setMeasuredDimension(totalWidth, resolveSize(totalHeight, heightMeasureSpec)); }
可能有點長,不過註釋都寫得比較清楚了,簡單的說就是遍歷計運算元View的寬高,動態加入行中,如果View的寬大於剩餘的行寬就在取一行放下,接下來我們在重寫一些onLayout:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (!mNeedLayout && changed){ mNeedLayout = false; int left = getPaddingLeft();//獲取最初的左上點 int top = getPaddingTop(); int count = mLines.size(); for (int i = 0; i < count; i++) { Line line = mLines.get(i); line.LayoutView(left,top);//擺放每一行中子View的位置 top +=line.mHeight+ mVerticalSpacing;//為下一行的top賦值 } } }
由於我們把子View的擺放都放在Line中了,所以onLayout比較簡單,接下來我們看一下Line的LayoutView方法:
public void LayoutView(int l, int t) { int left = l; int top = t; int count = getViewCount(); int layoutWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();//行的總寬度 //剩餘的寬度,是除了View和間隙的剩餘空間 int surplusWidth = layoutWidth - mWidth - mHorizontalSpacing * (count - 1); if (surplusWidth >= 0) { for (int i = 0; i < count; i++) { final View view = views.get(i); int childWidth = view.getMeasuredWidth(); int childHeight = view.getMeasuredHeight(); //計算出每個View的頂點,是由最高的View和該View高度的差值除以2 int topOffset = (int) ((mHeight - childHeight) / 2.0 + 0.5); if (topOffset < 0) { topOffset = 0; } view.layout(left,top+topOffset,left+childWidth,top + topOffset + childHeight); left += childWidth + mVerticalSpacing;//為下一個View的left賦值 } } }
也是比較簡單,其實就是根據寬度動態計算而已,我們看看效果吧
可以了吧,看起來是大功告成了,可是我們發現左邊和右邊的間距好像不相等,能不能讓子View居中顯示呢?答案當然是可以的,接下來我們提供個方法,讓外部可以設定裡面子View的對齊方式:
public interface AlienState { int RIGHT = 0; int LEFT = 1; int CENTER = 2; @IntDef(value = {RIGHT, LEFT, CENTER}) @interface Val {} } public void setAlignByCenter(@AlienState.Val int isAlignByCenter) { this.isAlignByCenter = isAlignByCenter; requestLayoutInner(); } private void requestLayoutInner() { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { requestLayout(); } }); }
提供一個setAlignByCenter的方法,分別有左對齊右對齊和居中對齊,然後我們在Line的layoutView中改寫一下:
//佈局View if (i == 0) { switch (isAlignByCenter) { case AlienState.CENTER: left += surplusWidth / 2; break; case AlienState.RIGHT: left += surplusWidth; break; default: left = 0; break; } }
在layoutView中把剩餘的寬度按照對齊的型別平分一下就得到我們要的效果了
好了,這樣就達到我們要的效果了.可是在回頭來看一下我們MainActivity裡面的寫法會不會感覺很撮呢?對於習慣了ListView,RecyclerView的Adapter寫法的我們有沒有辦法改一下,像寫adapter一樣來寫佈局呢?聰明的程式猿是沒有什麼辦不到的,接下來我們就來改寫一下:
public void setAdapter(List<?> list, int res, ItemView mItemView) { if (list == null) { return; } removeAllViews(); int layoutPadding = dipToPx(getContext(), 8); setHorizontalSpacing(layoutPadding); setVerticalSpacing(layoutPadding); int size = list.size(); for (int i = 0; i < size; i++) { Object item = list.get(i); View inflate = LayoutInflater.from(getContext()).inflate(res, null); mItemView.getCover(item, new ViewHolder(inflate), inflate, i); addView(inflate); } } public abstract static class ItemView<T> { abstract void getCover(T item, ViewHolder holder, View inflate, int position); } class ViewHolder { View mConvertView; public ViewHolder(View mConvertView) { this.mConvertView = mConvertView; mViews = new SparseArray<>(); } public <T extends View> T getView(int viewId) { View view = mViews.get(viewId); if (view == null) { view = mConvertView.findViewById(viewId); mViews.put(viewId, view); } try { return (T) view; } catch (ClassCastException e) { e.printStackTrace(); } return null; } public void setText(int viewId, String text) { TextView view = getView(viewId); view.setText(text); } }
然後我們在MainActivity中在使用一下:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FlowLayout mFlowLayout = (FlowLayout) findViewById(R.id.mFlowLayout); List<String> list = new ArrayList<>(); list.add("java"); list.add("javaEE"); list.add("javaME"); list.add("c"); list.add("php"); list.add("ios"); list.add("c++"); list.add("c#"); list.add("Android"); mFlowLayout.setAlignByCenter(FlowLayout.AlienState.CENTER); mFlowLayout.setAdapter(list, R.layout.item, new FlowLayout.ItemView<String>() { @Override void getCover(String item, FlowLayout.ViewHolder holder, View inflate, int position) { holder.setText(R.id.tv_label_name,item); } }); }
怎麼樣,是不是就根絕在跟使用adapter一樣了呢.
Demo已放到github,歡迎大家指點
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援it145.com。
相關文章