天天看點

Android PullToRefresh 分析之三、手勢響應一、 問題思考二、源碼分析三、結語

前言:接着上一篇《Android PullToRefresh 分析之二、UI結構》,這一篇主要分析是如何響應手勢事件的,即我們手指滑動的時候促發的一系列響應,該篇将詳細講清楚。

一、 問題思考

我們首先來思考下如果讓我們做手勢響應要考慮哪些問題, 我們先提出幾個問題:

  1. 向下滑動時如何判斷滑動到了頭部?
  2. 滑動到頭部之後是馬上就促發重新整理操作嗎?

OK,來分析下這兩個問題:       (1)判斷滑動到了頭部,如下兩圖所示:向下滑動但是第一個條目還沒有完全顯示,這個時候是不能促發刷  新操作的;

Android PullToRefresh 分析之三、手勢響應一、 問題思考二、源碼分析三、結語

    (2)隻有第一個條目完全可見才能促發重新整理動作的開始,如下圖所示,第一個條目完全可見;

Android PullToRefresh 分析之三、手勢響應一、 問題思考二、源碼分析三、結語

    (3)隻有第一個條目完全顯示後再往下滑動,開始促發重新整理動作的開始,這裡為什麼說重新整理動作的開始呢?因為重新整理動作又可以分解為三個階段四種狀态,①、"重新整理頭部"開始顯示 ②、"重新整理頭部"完全顯示 ③、"重新整理頭部"完全顯示後釋放 ④、"重新整理頭部"未完全顯示就釋放

Android PullToRefresh 分析之三、手勢響應一、 問題思考二、源碼分析三、結語

    (4)當"重新整理頭部"完全顯示的時候,放開就會調用回調方法onPullDownToRefresh(),讓我們去更新資料

Android PullToRefresh 分析之三、手勢響應一、 問題思考二、源碼分析三、結語

二、源碼分析

    通過以上問題的了解,我們再分析源碼就好了解了,下面來對源碼進行分析。     在PullToRefreshBase中關于手勢識别的代碼就在兩個方法的回調中,onInterceptTouchEvent()和 onTouchEvent(),如果這兩個方法的作用就不再詳細說明,onInterceptTouchEvent()作用就是判斷是否把該事件分發給子View,具體的說明請參考《Android-onInterceptTouchEvent()和onTouchEvent()總結》,這篇文章講述的比較清楚。

    首先來看下onInterceptTouchEvent()方法:

@Override
public final boolean onInterceptTouchEvent(MotionEvent event) {

    // 1、如果不是重新整理加載模式,讓子View接收觸摸事件
    if (!isPullToRefreshEnabled()) {
        return false;
    }

    final int action = event.getAction();

    // 2、如果目前手勢被釋放或手指擡起,讓子View接收觸摸事件
    if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
        mIsBeingDragged = false;
        return false;
    }

    // 3、如果手指按下,并且已經處在滑動重新整理加載狀态,不讓子View接收觸摸事件
    if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
        return true;
    }

    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            // If we're refreshing, and the flag is set. Eat all MOVE events
            // 4、如果在重新整理加載時不允許再次滑動而且正在重新整理狀态,不讓子View接收觸摸事件
            // 即正在加載資料時不允許使用者再次滑動重新整理,隻能等待該次資料加載完成
            if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
                return true;
            }

            if (isReadyForPull()) {
                final float y = event.getY(), x = event.getX();
                final float diff, oppositeDiff, absDiff;

                // We need to use the correct values, based on scroll
                // direction
                switch (getPullToRefreshScrollDirection()) {
                    case HORIZONTAL:
                        diff = x - mLastMotionX;
                        oppositeDiff = y - mLastMotionY;
                        break;
                    case VERTICAL:
                    default:
                        diff = y - mLastMotionY;
                        oppositeDiff = x - mLastMotionX;
                        break;
                }
                absDiff = Math.abs(diff);

                // 滑動距離大于滑動事件觸發的最小距離,重新整理方向上滑動的距離大于非重新整理方向的距離
                if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff))) {
                    // 第一個條目完全可見且為向下滑動,設定目前為 頭部重新整理
                    if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart()) {
                        mLastMotionY = y;
                        mLastMotionX = x;
                        // 5、觸發頭部重新整理,不讓子View接收觸摸事件
                        mIsBeingDragged = true;
                        if (mMode == Mode.BOTH) {
                            mCurrentMode = Mode.PULL_FROM_START;
                        }
                    // 最後一個條目完全可見且為向上滑動,設定目前為 尾部加載
                    } else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) {
                        mLastMotionY = y;
                        mLastMotionX = x;
                        // 6、觸發尾部加載,不讓子View接收觸摸事件
                        mIsBeingDragged = true;
                        if (mMode == Mode.BOTH) {
                            mCurrentMode = Mode.PULL_FROM_END;
                        }
                    }
                }
            }
            break;
        }
        case MotionEvent.ACTION_DOWN: {
            if (isReadyForPull()) {
                mLastMotionY = mInitialMotionY = event.getY();
                mLastMotionX = mInitialMotionX = event.getX();
                mIsBeingDragged = false;
            }
            break;
        }
    }

    return mIsBeingDragged;
}
           

通過以上代碼的注釋也可以看出,onInterceptTouchEvent()主要處理的就是在重新整理加載的時候不允許子View擷取該觸摸事件,其實我們最關心的是是否開始重新整理加載mIsBeingDragged的狀态切換!!!     值得一提的是判斷第一個條目是否完全顯示、最後一個條目是否完全顯示是通過抽象的方法isReadyForPullStart()、isReadyForPullEnd()由子類實作的。

    然後分析onTouchEvent()方法:

@Override
public final boolean onTouchEvent(MotionEvent event) {

    ......

    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE: {
            if (mIsBeingDragged) {
                mLastMotionY = event.getY();
                mLastMotionX = event.getX();
                pullEvent();
                return true;
            }
            break;
        }

        ......

        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP: {
            if (mIsBeingDragged) {
                mIsBeingDragged = false;

                if (mState == State.RELEASE_TO_REFRESH
                        && (null != mOnRefreshListener || null != mOnRefreshListener2)) {
                    setState(State.REFRESHING, true);
                    return true;
                }

                // If we're already refreshing, just scroll back to the top
                if (isRefreshing()) {
                    smoothScrollTo(0);
                    return true;
                }

                // If we haven't returned by here, then we're not in a state
                // to pull, so just reset
                setState(State.RESET);

                return true;
            }
            break;
        }
    }

    return false;
}
           

其實該方法中主要的就是 ACTION_MOVE中調用的 pullEvent()(注意的是隻有在mIsBeingDragged == ture狀态才會執行pullEvent(),就是為什麼我在onInterceptTouchEvent()分析時說"我們最關心的是是否開始重新整理加載mIsBeingDragged的狀态切換!!!"), 及MotionEvent.ACTION_UP中調用的setState(),接着看pullEvent()方法:

private void pullEvent() {
    final int newScrollValue;
    final int itemDimension;
    final float initialMotionValue, lastMotionValue;

    switch (getPullToRefreshScrollDirection()) {
        case HORIZONTAL:
            initialMotionValue = mInitialMotionX;
            lastMotionValue = mLastMotionX;
            break;
        case VERTICAL:
        default:
            initialMotionValue = mInitialMotionY;
            lastMotionValue = mLastMotionY;
            break;
    }

    switch (mCurrentMode) {
        case PULL_FROM_END:
            newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION);
            itemDimension = getFooterSize();
            break;
        case PULL_FROM_START:
        default:
            newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION);
            itemDimension = getHeaderSize();
            break;
    }

    setHeaderScroll(newScrollValue);
    if (newScrollValue != 0 && !isRefreshing()) {
            float scale = Math.abs(newScrollValue) / (float) itemDimension;
            
            ......

            if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue)) {
                setState(State.PULL_TO_REFRESH);
            } else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue)) {
                setState(State.RELEASE_TO_REFRESH);
            }
        }
}
           

可以看到主要是根據重新整理狀态擷取頭部或者尾部的大小,然後調用了 setHeaderScroll()方法,進入該方法:

protected final void setHeaderScroll(int value) {

    ......

    if (mLayoutVisibilityChangesEnabled) {
        if (value < 0) {
            mHeaderLayout.setVisibility(View.VISIBLE);
        } else if (value > 0) {
            mFooterLayout.setVisibility(View.VISIBLE);
        } else {
            mHeaderLayout.setVisibility(View.INVISIBLE);
            mFooterLayout.setVisibility(View.INVISIBLE);
        }
    }

    ......

    switch (getPullToRefreshScrollDirection()) {
        case VERTICAL:
            scrollTo(0, value);
            break;
        case HORIZONTAL:
            scrollTo(value, 0);
            break;
    }
}
           

可以看到也比較簡單,就是根據向下還是向上滑動來設定頭部或尾部的顯示,然後控件随手指滾動,這樣頭部或者尾部就顯示出來了。然後回到 pullEvent()方法,有如下代碼比較關鍵:

if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue)) {
    setState(State.PULL_TO_REFRESH);
} else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue)) {
    setState(State.RELEASE_TO_REFRESH);
}
           

當滑動距離大于頭部或尾部大小的時候,設定狀态為釋放以重新整理,這就是我們上面所說的重新整理頭部或尾部完全顯示,這時候放開手指就可以觸發重新整理加載的回調。接着回到 onTouchEvent()方法的手指擡起,有如下關鍵代碼:

if (mState == State.RELEASE_TO_REFRESH
        && (null != mOnRefreshListener || null != mOnRefreshListener2)) {
    setState(State.REFRESHING, true);
    return true;
}
           

就是釋放的時候回調重新整理加載的方法,我們就可以通過設定監聽的方式在回調方法中來處理重新整理加載的事件啦~

三、結語

    在該篇中,我們也是搞清楚了一個問題,就是如何響應手指滑動事件的。在下篇中《Android PullToRefresh 分析之四、擴充RecyclerView》我們主要講解如何判斷重新整理加載View是如何判斷是否第一個條目完全顯示,最後一個條目完全顯示的,然後編寫一個自己的擴充 PullToRefreshRecyclerView。