首先來介紹一下這個自定義View:java
接下來簡單介紹一下在這個自定義View中用到的技術點:android
下面是這個自定義View—— FlowLayout 的實現代碼:ide
自定義View類 FlowLayout.java 中的代碼:佈局
import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.List; /** * 自定義流式佈局 */ public class FlowLayout extends ViewGroup { private List<List<View>> views; // 存放全部子元素(一行一行存儲) private List<View> lineViews; // 存儲每一行中的子元素 private List<Integer> heights; // 存儲每一行的高度 private boolean scrollable; // 是否能夠滾動 private int measuredHeight; // 測量獲得的高度 private int realHeight; // 整個流式佈局控件的實際高度 private int scrolledHeight = 0; // 已經滾動過的高度 private int startY; // 本次滑動開始的Y座標位置 private int offsetY; // 本次滑動的偏移量 private boolean pointerDown; // 在ACTION_MOVE中,視第一次觸發爲手指按下,從第二次觸發開始計入正式滑動 public FlowLayout(Context context) { super(context); } public FlowLayout(Context context, AttributeSet attrs) { super(context, attrs); } public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 初始化 */ private void init() { views = new ArrayList<>(); lineViews = new ArrayList<>(); heights = new ArrayList<>(); } /** * 計算佈局中全部子元素的寬度和高度,累加獲得整個佈局最終顯示的寬度和高度 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); measuredHeight = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); // 當前行的寬度和高度(寬度是子元素寬度的和,高度是子元素高度的最大值) int lineWidth = 0; int lineHeight = 0; // 整個流式佈局最終顯示的寬度和高度 int flowLayoutWidth = 0; int flowLayoutHeight = 0; // 初始化各類參數(列表) init(); // 遍歷全部子元素,對子元素進行排列 int childCount = this.getChildCount(); for (int i = 0; i < childCount; i++) { View child = this.getChildAt(i); // 獲取到子元素的寬度和高度 measureChild(child, widthMeasureSpec, heightMeasureSpec); int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); // 若是當前行中剩餘的空間不足以容納下一個子元素,則換行 // 換行的同時,保存當前行中的全部元素,疊加行高,而後將行寬和行高重置爲0 if (lineWidth + childWidth + lp.leftMargin + lp.rightMargin > widthSize - getPaddingLeft() - getPaddingRight()) { views.add(lineViews); lineViews = new ArrayList<>(); flowLayoutWidth = Math.max(flowLayoutWidth, lineWidth); // 以最寬的行的寬度做爲最終佈局的寬度 flowLayoutHeight += lineHeight; heights.add(lineHeight); lineWidth = 0; lineHeight = 0; } // 不管換不換行,都須要將元素添加到列表中、處理寬度和高度的值 lineViews.add(child); lineWidth += childWidth + lp.leftMargin + lp.rightMargin; lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin); // 處理最後一行,不然最後一行不能顯示 if (i == childCount - 1) { flowLayoutHeight += lineHeight; flowLayoutWidth = Math.max(flowLayoutWidth, lineWidth); heights.add(lineHeight); views.add(lineViews); } } // 獲得最終的寬高 // 寬度:若是是EXACTLY模式,則遵循測量值,不然使用咱們計算獲得的寬度值 // 高度:只要佈局中內容的高度大於測量高度,就使用內容高度(無視測量模式);不然才使用測量高度 int width = widthMode == MeasureSpec.EXACTLY ? widthSize : flowLayoutWidth + getPaddingLeft() + getPaddingRight(); realHeight = flowLayoutHeight + getPaddingTop() + getPaddingBottom(); if (heightMode == MeasureSpec.EXACTLY) { realHeight = Math.max(measuredHeight, realHeight); } scrollable = realHeight > measuredHeight; // 設置最終的寬高 setMeasuredDimension(width, realHeight); } /** * 對全部子元素進行佈局 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 當前子元素應該佈局到的X、Y座標 int currentX = getPaddingLeft(); int currentY = getPaddingTop(); // 遍歷全部子元素,對每一個子元素進行佈局 // 遍歷每一行 for (int i = 0; i < views.size(); i++) { int lineHeight = heights.get(i); List<View> lineViews = views.get(i); // 遍歷當前行中的每個子元素 for (int j = 0; j < lineViews.size(); j++) { View child = lineViews.get(j); // 獲取到當前子元素的上、下、左、右的margin值 MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childL = currentX + lp.leftMargin; int childT = currentY + lp.topMargin; int childR = childL + child.getMeasuredWidth(); int childB = childT + child.getMeasuredHeight(); // 對當前子元素進行佈局 child.layout(childL, childT, childR, childB); // 更新下一個元素要佈局的X、Y座標 currentX += lp.leftMargin + child.getMeasuredWidth() + lp.rightMargin; } currentY += lineHeight; currentX = getPaddingLeft(); } } /** * 滾動事件的處理,當佈局能夠滾動(內容高度大於測量高度)時,對手勢操做進行處理 */ @Override public boolean onTouchEvent(MotionEvent event) { // 只有當佈局能夠滾動的時候(內容高度大於測量高度的時候),纔會對手勢操做進行處理 if (scrollable) { int currY = (int) event.getY(); switch (event.getAction()) { // 由於ACTION_DOWN手勢多是爲了點擊佈局中的某個子元素,所以在onInterceptTouchEvent()方法中沒有攔截這個手勢 // 所以,在這個事件中不能獲取到startY,也所以纔將startY的獲取移動到第一次滾動的時候進行 case MotionEvent.ACTION_DOWN: break; // 當第一次觸發ACTION_MOVE事件時,視爲手指按下;之後的ACTION_MOVE事件才視爲滾動事件 case MotionEvent.ACTION_MOVE: // 用pointerDown標誌位只是手指是否已經按下 if (!pointerDown) { startY = currY; pointerDown = true; } else { offsetY = startY - currY; // 下滑大於0 // 佈局中的內容跟隨手指的滾動而滾動 // 用scrolledHeight記錄之前的滾動事件中滾動過的高度(由於不必定每一次滾動都是從佈局的最頂端開始的) this.scrollTo(0, scrolledHeight + offsetY); } break; // 手指擡起時,更新scrolledHeight的值; // 若是滾動過界(滾動到高於佈局最頂端或低於佈局最低端的時候),設置滾動回到佈局的邊界處 case MotionEvent.ACTION_UP: scrolledHeight += offsetY; if (scrolledHeight + offsetY < 0) { this.scrollTo(0, 0); scrolledHeight = 0; } else if (scrolledHeight + offsetY + measuredHeight > realHeight) { this.scrollTo(0, realHeight - measuredHeight); scrolledHeight = realHeight - measuredHeight; } // 手指擡起後別忘了重置這個標誌位 pointerDown = false; break; } } return super.onTouchEvent(event); } /** * 調用在這個佈局中的子元素對象的getLayoutParams()方法,會獲得一個MarginLayoutParams對象 */ @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } /** * 事件攔截,當手指按下或擡起的時候不進行攔截(由於可能這個操做只是點擊了佈局中的某個子元素); * 當手指移動的時候,纔將事件攔截; * 所以,咱們在onTouchEvent()方法中,只能將ACTION_MOVE的第一次觸發做爲手指按下 */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: intercepted = true; break; case MotionEvent.ACTION_UP: intercepted = false; break; } return intercepted; } }
佈局文件 activity_main.xml 中的代碼以下:this
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <my.itgungnir.flowlayout.FlowLayout android:layout_width="match_parent" android:layout_height="match_parent" android:padding="20.0dip"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="My name is ITGungnir" /> ......(省略N個Button) <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="20.0dip" android:text="Hello" /> ......(省略N個Button) <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="搜嘎" /> </my.itgungnir.flowlayout.FlowLayout> </LinearLayout>
這裏注意:我給FlowLayout佈局設置了padding爲20.0dip;Hello那個按鈕的margin屬性也設置成了20.0dip,具體的效果見最後的運行效果圖。spa
主界面JAVA代碼中不須要書寫任何代碼。固然,咱們也能夠在Activity中經過JAVA代碼,動態生成View後添加到這個佈局中。這裏就不演示了。code
項目的運行效果圖以下圖所示:xml