1. 程式人生 > >Android-下拉重新整理(三)-RefreshLayout

Android-下拉重新整理(三)-RefreshLayout

時間推移,記得以前寫過關於下拉重新整理的部落格文章

現在回看程式碼,發現這種構建方式存在很大問題,當我專案中期突然需要一個ListView的帶有下拉重新整理功能,這時候,如果用以前的方式,就是用自定義的的RefreshListView去代替原生的ListView,這種體驗非常一般,因為需要修改很多程式碼,而且,針對每一個型別的View,都要去實現一個自定義View。

Google官方對於下拉重新整理自然早就有方案,在support library中加入了SwipeRefreshLayout佈局,當開發者需要下拉重新整理功能的時候,可以讓SwipeRefreshLayout包裹任意目標檢視,實現下拉效果。這時候我們真的體驗到SwipeRefreshLayout的好處,不需要我們去修改自己的業務邏輯程式碼,就能輕鬆為應用加上重新整理效果。於是,改進原來的體驗較差的重新整理庫的想法湧上心頭。

這篇部落格主要是分析SwipeRefreshLayout原理,以及根據原理依樣畫葫蘆,實現類似效果的下拉重新整理佈局。

SwipeRefreshLayout 原理

首先我們看到SwipeRefreshLayout的簡單實用:

<android.support.v4.widget.SwipeRefreshLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:layout_width="match_parent"
android:layout_height="match_parent"/> </android.support.v4.widget.SwipeRefreshLayout>

由於之前部落格的經驗,我們可以簡單總結下拉重新整理的關鍵步驟:

1 對於觸控事件的攔截並處理
2 對於能否繼續上滑或者下滑的判斷

我們看到SwipeRefreshLayout#onInterceptTouchEvent()方法

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ...
switch (action) { case MotionEvent.ACTION_DOWN: ... mIsBeingDragged = false; pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { return false; } /* 記錄手指按下的座標Y值 */ mInitialDownY = ev.getY(pointerIndex); break; case MotionEvent.ACTION_MOVE: ... /* 記錄手指滑動的座標Y值 */ final float y = ev.getY(pointerIndex); /* 通過這裡做判斷是否處於下拉狀態 */ startDragging(y); break; ... case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; break; } return mIsBeingDragged; }

我們看到關鍵變數mIsBeingDragged,這個變數用來標記當前是否正處於下拉操作,如果當前正在下拉,則攔截觸控事件。這裡主要是根據mIsBeingDragged的值,攔截觸控事件不讓事件傳遞到子檢視,直接交接觸控處理到onTouchEvent()方法裡處理。

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        ...
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mActivePointerId = ev.getPointerId(0);
                mIsBeingDragged = false;
                break;

            case MotionEvent.ACTION_MOVE: {
                ...
                final float y = ev.getY(pointerIndex);
                /* 通過這裡做判斷是否處於下拉狀態 */
                startDragging(y);

                if (mIsBeingDragged) {
                    /* 計算手指滑動距離 */
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                    if (overscrollTop > 0) {
                        moveSpinner(overscrollTop);
                    } else {
                        return false;
                    }
                }
                break;
            }
            ...
            case MotionEvent.ACTION_UP: {
                ...
                if (mIsBeingDragged) {
                    final float y = ev.getY(pointerIndex);
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                    mIsBeingDragged = false;
                    finishSpinner(overscrollTop);
                }
                mActivePointerId = INVALID_POINTER;
                return false;
            }
            case MotionEvent.ACTION_CANCEL:
                return false;
        }

        return true;
    }

當觸控事件為MotionEvent.ACTION_MOVE的時候,此時開始滑動的時候,startDragging()方法被持續呼叫,當手指的滑動超過系統規定的滑動距離mTouchSlop,設定當前狀態為下拉拖拽。

    @SuppressLint("NewApi")
    private void startDragging(float y) {
        final float yDiff = y - mInitialDownY;
        if (yDiff > mTouchSlop && !mIsBeingDragged) {
            mInitialMotionY = mInitialDownY + mTouchSlop;
            mIsBeingDragged = true;
            mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
        }
    }

繼續往下走,判斷mIsBeingDragged,此時為滑動狀態,計算當前滑動距離overscrollTop,如果滑動距離大於0,呼叫moveSpinner(overscrollTop)使得Spinner下拉並且旋轉。

if (mIsBeingDragged) {
        final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
         if (overscrollTop > 0) {
            moveSpinner(overscrollTop);
         } else {
            return false;
         }
}

觸控事件的處理很簡單,流程主要是先通過onInterceptTouchEvent攔截,再onTouchEvent做下拉處理。

接下來是對於能否繼續上滑或者下滑的判斷。
我們看到SwipeRefreshLayout#canChildScrollUp()程式碼

/**
     * @return Whether it is possible for the child view of this layout to
     *         scroll up. Override this if the child view is a custom view.
     */
    public boolean canChildScrollUp() {
        if (mChildScrollUpCallback != null) {
            return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
        }
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (mTarget instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) mTarget;
                return absListView.getChildCount() > 0
                        && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
                                .getTop() < absListView.getPaddingTop());
            } else {
                return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
            }
        } else {
            return ViewCompat.canScrollVertically(mTarget, -1);
        }
    }

繼續跟蹤ViewCompat.canScrollVertically()

public static boolean canScrollVertically(View v, int direction) {
   return IMPL.canScrollVertically(v, direction);
}

繼續跟蹤,我們發現API小於14的時候呼叫的是BaseViewCompatImpl#canScrollVertically()(低版本這裡就不討論了),API大於等於14的時候,呼叫的是ICSViewCompatImpl#canScrollVertically()

看到ICSViewCompatImpl#canScrollVertically()

@Override
public boolean canScrollVertically(View v, int direction) {
    return ViewCompatICS.canScrollVertically(v, direction);
}

繼續看ViewCompatICS#canScrollVertically

public static boolean canScrollVertically(View v, int direction) {
     return v.canScrollVertically(direction);
}

最後我們發現呼叫的是View#canScrollVertically()

    /**
     * Check if this view can be scrolled vertically in a certain direction.
     *
     * @param direction Negative to check scrolling up, positive to check scrolling down.
     * @return true if this view can be scrolled in the specified direction, false otherwise.
     */
    public boolean canScrollVertically(int direction) {
        final int offset = computeVerticalScrollOffset();
        final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
        if (range == 0) return false;
        if (direction < 0) {
            return offset > 0;
        } else {
            return offset < range - 1;
        }
    }

其實當API到大於等於14的時候,如果判斷目標View是否可以繼續下拉,都是通過來View#canScrollVertically()判斷的,那麼為什麼通過這個方法就能實現對ListView, TextView, RecyclerView等View的適配呢?

我們發現computeVerticalScrollOffset(),computeVerticalScrollRange()這兩個方法,基本上都被重寫,通過重寫這兩個方法,實現了對檢視是否可下拉的判斷,詳細的可以檢視ListView或者RecyclerView等類的程式碼。

借鑑SwipeRefreshLayout實現下拉重新整理庫

基本瞭解了SwipeRefreshLayout的原理,我們可以模仿它,來實現一個帶有下拉重新整理以及上拉載入更多功能的佈局。

例如如何實現是否可下拉或者是否可上拉的判斷,這裡我們可以直接借鑑SwipeRefreshLayout的程式碼

    /**
     * 是否可以繼續下拉
     * @param targetView
     * @return
     */
    public static boolean canChildPullDown(View targetView) {
        /* 模仿SwipeRefreshLayout */
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (targetView instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) targetView;
                return absListView.getChildCount() > 0
                        && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
                        .getTop() < absListView.getPaddingTop());
            } else {
                return ViewCompat.canScrollVertically(targetView, -1) || targetView.getScrollY() > 0;
            }
        } else {
            return ViewCompat.canScrollVertically(targetView, -1);
        }
    }

同時上拉也一樣,只需要把一些條件置反就行

    /**
     * 是否可以繼續上拉
     * @param targetView
     * @return
     */
    public static boolean canChildPullUp(View targetView) {
        /* 模仿SwipeRefreshLayout */
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (targetView instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) targetView;
                return absListView.getChildCount() > 0
                        && (absListView.getLastVisiblePosition() < absListView.getAdapter().getCount() - 1
                        || absListView.getChildAt(absListView.getChildCount() - 1).getBottom() > absListView.getPaddingBottom());
            } else {
                return ViewCompat.canScrollVertically(targetView, 1);
            }
        } else {
            return ViewCompat.canScrollVertically(targetView, 1);
        }
    }

其他具體的操作可以看專案原始碼。

下拉重新整理庫RefreshLayout介紹

如何引入

在build.gradle檔案中加入

compile 'com.zero.refreshlayout.library:RefreshLayout:1.0.0'

簡單使用

首先是xml程式碼:

    <com.zero.refreshlayout.library.RefreshLayout
        android:id="@+id/refresh_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/text_view"
            android:text="text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

    </com.zero.refreshlayout.library.RefreshLayout>

在java程式碼設定相關的屬性,並且設定監聽器

mRefreshLayout = (RefreshLayout) findViewById(R.id.refresh_layout);
mRefreshLayout.setHeaderView(new TextHeaderView(this));
mRefreshLayout.setFooterView(new TextFooterView(this));
mRefreshLayout.setRefreshListener(new RefreshListener() {
            @Override
            public void onRefresh() {
                MainThreadHandler.execute(new Runnable() {
                    @Override
                    public void run() {
                        mRefreshLayout.finishRefresh();
                    }
                }, 2000);
            }

            @Override
            public void onLoadMore() {
                MainThreadHandler.execute(new Runnable() {
                    @Override
                    public void run() {
                        mRefreshLayout.finishLoadMore();
                    }
                }, 2000);
            }
        });

更多的開放API:

  • void finishRefresh()
    結束下拉重新整理狀態
  • void finishLoadMore()
    結束上拉載入狀態
  • void setHeaderViewHeight(int headerViewHeight)
    設定HeaderView的高度,如果不設定的話,HeaderView預設為wrap_content
  • void setFooterViewHeight(int footerViewHeight)
    設定FooterView的高度,如果不設定的話,FooterView預設為wrap_content
  • setHeaderViewMaxPullDistance(int headerViewMaxPullDistance)
    設定下拉最大拉伸長度,如果不設定的話,預設為HeaderView高度的2.5倍
  • setFooterViewMaxPullDistance(int footerViewMaxPullDistance)
    設定上拉最大拉伸長度,如果不設定的話,預設為FooterView高度的2.5倍
  • void setHeaderViewEnable(boolean headerViewEnable)
    設定HeaderView是否可用
  • void setFooterViewEnable(boolean footerViewEnable)
    設定FooterView是否可用

個性化使用

1)佈局樣式多樣化

如果你對檢視的佈局樣式不喜歡,這裡暫時提供了另外兩種佈局樣式。

如何切換佈局樣式,只需要呼叫

void setRefreshMode(RefreshMode refreshMode)

預設樣式 RefreshMode.LINEAR

mRefreshLayout.setRefreshMode(RefreshMode.LINEAR);

這裡寫圖片描述

覆蓋樣式 RefreshMode.COVER_LAYOUT_FRONT (佈局覆蓋在下拉控制元件之前)

mRefreshLayout.setRefreshMode(RefreshMode.COVER_LAYOUT_FRONT);

這裡寫圖片描述

覆蓋樣式 RefreshMode.COVER_LAYOUT_BEHIND (佈局被下拉控制元件覆蓋)

mRefreshLayout.setRefreshMode(RefreshMode.COVER_LAYOUT_BEHIND);

這裡寫圖片描述

2) 定製HeaderView以及FooterView

最重要當然是自己定製HeaderView以及FooterView,通過下面簡單的例子我們就能輕鬆看懂

所有的HeaderView都必須繼承AbsHeaderView, 所有的FooterView都必須繼承AbsFooterView,我們看到,這裡實現了一個TextHeaderView類

public class TextHeaderView extends AbsHeaderView {

    private TextView mTextView;

    public TextHeaderView(Context context) {
        super(context);
        mTextView = new TextView(context);
        mTextView.setBackgroundColor(0xFFFFFFFF);
        mTextView.setTextColor(0xFF000000);
        mTextView.setTextSize(20);
        mTextView.setPadding(50, 50, 50, 50);
        mTextView.setGravity(Gravity.CENTER);
        mTextView.setText("下拉重新整理");
    }

    @Override
    public View getContentView() {
        return mTextView;
    }

    @Override
    public void onPullDown(float fraction) {
        mTextView.setText("下拉重新整理");
    }

    @Override
    public void onFinishPullDown(float fraction) {

    }

    @Override
    public void onRefreshing() {
        mTextView.setText("重新整理...");
    }
}
  • AbsHeaderView#onPullDown
    HeaderView正在被下拉時呼叫,fraction引數為HeaderView完全顯示的比例
  • AbsHeaderView#onFinishPullDown
    HeaderView被鬆手彈回,fraction引數為HeaderView完全顯示的比例
  • AbsHeaderView#onRefreshing
    HeaderView在處於重新整理狀態

3)預設提供的HeaderView,FooterView主題

如果我們只是想快速使用,可以使用現成的HeaderView以及FooterView, RefreshLayout暫時提供了幾個樣式

mRefreshLayout.setHeaderView(new BezierWaveHeader(this));
mRefreshLayout.setFooterView(new BezierWaveFooter(this));

這裡寫圖片描述

mRefreshLayout.setHeaderView(new SquareSpreadHeader(this));
mRefreshLayout.setFooterView(new SquareSpreadFooter(this));

這裡寫圖片描述

更新歷史

  • v1.0.1
    修復bug

學習不易,可能會有很多問題,歡迎拍磚