【Android - 自定義View】之自定義可滾動的流式佈局

  首先來介紹一下這個自定義View:java

  • (1)這個自定義View的名稱叫作 FlowLayout ,繼承自ViewGroup類;
  • (2)在這個自定義View中,用戶能夠放入全部繼承自View類的視圖,這個佈局會自動獲取其寬高並排列在佈局中,保證每個視圖都完整的顯示在界面上;
  • (3)若是用戶放入佈局的視圖的總高度大於設置給這個視圖的高度,那麼視圖就能夠支持上下滾動;
  • (4)能夠在XML佈局文件或JAVA文件中設置佈局的padding屬性和子元素的margin屬性。

  接下來簡單介紹一下在這個自定義View中用到的技術點:android

  • (1)用到了自定義View三大流程中的測量和佈局流程,分別體如今 onMeasure() 和 onLayout() 兩個方法中;
  • (2)在onMeasure()方法中,測量全部子元素的寬高,最後經過累加判斷獲得自身要顯示的寬高;
  • (3)在onMeasure()方法中還爲全部子元素進行了分行,保證每一個子元素都能完整的顯示在佈局中,達到「流式佈局」的功能需求;
  • (4)在onLayout()方法中,一次取出全部子元素,獲取onMeasure()方法中測量的寬高,開始佈局;
  • (5)在上面的測量和佈局過程當中,都有將佈局的 padding 屬性和元素的 margin 屬性考慮在內;
  • (6)設置了這個佈局中的子元素具備的LayoutParams:經過 generateLayoutParams() 方法設置;
  • (7)在 onInterceptTouchEvent() 方法中對事件進行攔截,保證佈局滾動和元素點擊不會產生衝突;
  • (8)在 onTouchEvent() 方法中處理了觸摸事件,實現佈局的滾動功能。

  下面是這個自定義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

相關文章
相關標籤/搜索