Android PullToRefresh 分析之三、手势响应

前言:接着上一篇《Android PullToRefresh 分析之二、UI结构》,这一篇主要分析是如何响应手势事件的,即我们手指滑动的时候促发的一系列响应,该篇将详细讲清楚。

一、 问题思考

我们首先来思考下如果让我们做手势响应要考虑哪些问题, 我们先提出几个问题:
  1. 向下滑动时如何判断滑动到了头部?
  2. 滑动到头部之后是马上就促发刷新操作吗?

OK,来分析下这两个问题:
      (1)判断滑动到了头部,如下两图所示:向下滑动但是第一个条目还没有完全显示,这个时候是不能促发刷  新操作的;
Android PullToRefresh 分析之三、手势响应_第1张图片
    (2)只有第一个条目完全可见才能促发刷新动作的开始,如下图所示,第一个条目完全可见;
Android PullToRefresh 分析之三、手势响应_第2张图片
    (3)只有第一个条目完全显示后再往下滑动,开始促发刷新动作的开始,这里为什么说刷新动作的开始呢?因为刷新动作又可以分解为三个阶段四种状态,①、"刷新头部"开始显示 ②、"刷新头部"完全显示 ③、"刷新头部"完全显示后释放 ④、"刷新头部"未完全显示就释放
Android PullToRefresh 分析之三、手势响应_第3张图片
    (4)当"刷新头部"完全显示的时候,放开就会调用回调方法onPullDownToRefresh(),让我们去更新数据
Android PullToRefresh 分析之三、手势响应_第4张图片

二、源码分析


    通过以上问题的了解,我们再分析源码就好理解了,下面来对源码进行分析。
    在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。


你可能感兴趣的:(Android,刷新框架,PullToRefresh)