NestedScrollView分析

前言

    级联滑动在现在的app设计中越来越常见,在android的老版本中,并没有添加对级联滑动的支持,但是如今,几乎所有view都会默认实现了级联滑动的功能。

    除了在源码中实现了级联滑动,为了兼容,android还在support中添加了级联滑动的接口,实际上实现方案和源码相同。为了方便分析,我们就从support入手,来看一下级联滑动的实现。

NestedScrollingChild

public interface NestedScrollingChild {
    // 参数enabled:true表示view使用嵌套滚动,false表示禁用.
    public void setNestedScrollingEnabled(boolean enabled);

    public boolean isNestedScrollingEnabled();

    // 参数axes:表示滚动的方向如:ViewCompat.SCROLL_AXIS_VERTICAL(垂直方向滚动)和
    // ViewCompat.SCROLL_AXIS_HORIZONTAL(水平方向滚动)
    // 返回值:true表示本次滚动支持嵌套滚动,false不支持
    public boolean startNestedScroll(int axes);

    public void stopNestedScroll();

    public boolean hasNestedScrollingParent();

    // 参数dxConsumed: 表示view消费了x方向的距离长度
    // 参数dyConsumed: 表示view消费了y方向的距离长度
    // 参数dxUnconsumed: 表示滚动产生的x滚动距离还剩下多少没有消费
    // 参数dyUnconsumed: 表示滚动产生的y滚动距离还剩下多少没有消费
    // 参数offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    // 参数dx: 表示view本次x方向的滚动的总距离长度
    // 参数dy: 表示view本次y方向的滚动的总距离长度
    // 参数consumed: 表示父布局消费的距离,consumed[0]表示x方向,consumed[1]表示y方向
    // 参数offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    // 这个是滑动的就不详细分析了
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

    来说 NestedScrollingChild。如果你有一个可以滑动的 View,需要被用来作为嵌入滑动的子 View,就必须实现本接口。在此 View 中,包含一个 NestedScrollingChildHelper 辅助类。NestedScrollingChild 接口的实现,基本上就是调用本 Helper 类的对应的函数即可,因为 Helper 类中已经实现好了 Child 和 Parent 交互的逻辑。原来的 View 的处理 Touch 事件,并实现滑动的逻辑大体上不需要改变。

需要做的就是,如果要准备开始滑动了,需要告诉 Parent,你要准备进入滑动状态了,调用 startNestedScroll()。你在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用 dispatchNestedPreScroll()。如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。然后,你自己进行余下的滑动。最后,如果滑动距离还有剩余,你就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用 dispatchNestedScroll()

NestedScrollingParent

public interface NestedScrollingParent {

      // 参数child:ViewParent包含触发嵌套滚动的view的对象
      // 参数target:触发嵌套滚动的view  (在这里如果不涉及多层嵌套的话,child和target)是相同的
      // 参数nestedScrollAxes:就是嵌套滚动的滚动方向了.
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    public void onStopNestedScroll(View target);

    // 参数target:同上
    // 参数dxConsumed:表示target已经消费的x方向的距离
    // 参数dyConsumed:表示target已经消费的x方向的距离
    // 参数dxUnconsumed:表示x方向剩下的滑动距离
    // 参数dyUnconsumed:表示y方向剩下的滑动距离
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    // 参数dx:表示target本次滚动产生的x方向的滚动总距离
    // 参数dy:表示target本次滚动产生的y方向的滚动总距离
    // 参数consumed:表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离.
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    public int getNestedScrollAxes();
}

    作为一个可以嵌入 NestedScrollingChild 的父 View,需要实现 NestedScrollingParent,这个接口方法和 NestedScrollingChild 大致有一一对应的关系。同样,也有一个 NestedScrollingParentHelper 辅助类来默默的帮助你实现和 Child 交互的逻辑。滑动动作是 Child 主动发起,Parent 就收滑动回调并作出响应。

从上面的 Child 分析可知,滑动开始的调用 startNestedScroll(),Parent 收到 onStartNestedScroll() 回调,决定是否需要配合 Child 一起进行处理滑动,如果需要配合,还会回调 onNestedScrollAccepted()

每次滑动前,Child 先询问 Parent 是否需要滑动,即 dispatchNestedPreScroll(),这就回调到 Parent 的 onNestedPreScroll(),Parent 可以在这个回调中“劫持”掉 Child 的滑动,也就是先于 Child 滑动。

Child 滑动以后,会调用 onNestedScroll(),回调到 Parent 的 onNestedScroll(),这里就是 Child 滑动后,剩下的给 Parent 处理,也就是 后于 Child 滑动。

最后,滑动结束,调用 onStopNestedScroll() 表示本次处理结束。

其实,除了上面的 Scroll 相关的调用和回调,还有 Fling 相关的调用和回调,处理逻辑基本一致。

NestedScrollView

    NestedScrollView 作为级联滑动最典型的实现,我们可以通过它来详细了解这中间的流程。

public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
        NestedScrollingChild, ScrollingView {
..........
}

    首先该类的定义,它同时继承了Parent和Child,原因在于NestedScrollView中嵌套NestedScrollView是非常多见的,这个时候它既是child也是parent。这会对我们代码分析造成一定障碍,但是影响并不会很大。

    忽略构造,初始化等过程,我们此文的目标是滚动,所以直接来看第一个和滚动相关的方法

    第一段代码实际上并不是最开始起作用的,因为我们第一个事件一定是DOWN,所以需要先看一下对于DOWN的处理

case MotionEvent.ACTION_DOWN: {
                final int y = (int) ev.getY();
                if (!inChild((int) ev.getX(), (int) y)) {
                    mIsBeingDragged = false;
                    recycleVelocityTracker();
                    break;
                }

                /*
                 * Remember location of down touch.
                 * ACTION_DOWN always refers to pointer index 0.
                 */
                mLastMotionY = y;
                mActivePointerId = ev.getPointerId(0);

                initOrResetVelocityTracker();
                mVelocityTracker.addMovement(ev);
                /*
                 * If being flinged and user touches the screen, initiate drag;
                 * otherwise don't. mScroller.isFinished should be false when
                 * being flinged. We need to call computeScrollOffset() first so that
                 * isFinished() is correct.
                */
                mScroller.computeScrollOffset();
                mIsBeingDragged = !mScroller.isFinished();
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
            }

    如果点击的位置不在child中,我们intercept方法直接返回false,表示不拦截事件(这个时候就算我们不拦截,其实也会传入onTouch的吧)。

    如果当前正在滚动(正在滚动,又遇到手指按下……可能的情况就是在flinged的时候按下)那么isBeingDragged为true表示会拦截后面的事件,否则为false不拦截。然后调用startNestedScroll。

    疑问,如果该scrollview是parent的,那么它调用这个方法会不会出问题?来看下代码:

public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            //递归查找parent,如果parent的onStartNestedScroll返回true,表示找到了父NestedScrolling
            // 并且调用它的onNestedScrollAccepted方法
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

    这是childHelper的最终调用。如果已经有NestedScrollingParent了,那么直接返回true。如果该view允许级联滑动(NestedScrollView在初始化时就设置为允许级联了。)版本寻找它的父NestedScrolling。方法就是递归调用父控件的onStartNestedScroll(与当前版本相关,所以用到了ViewParentCompat.)

    在NestedScrollView嵌套的环境下,实际上就是调用父NestedScrollView的onStartNestedScroll方法

 @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    }

    支持y轴,返回true。然后再调用onNestedScrollAccepted方法。一般情况下,我们只会调用parentHelper的对应方法(实际上就是记录一下滚动轴,没有其他操作),但是由于NestedScrollView同事继承了parent和child,所以会继续调用startNestedScroll用于多层(大于两层)scrollView的级联。

    假设假设子view不会拦截onTouch事件,所以还是会回传到child的onTouch方法中。onTouch方法中关于DOWN事件的操作我们本篇不关心,无非是停止滚动动画之类的,我们关心的是,onTouch方法会返回true!也就是child的onIntercept方法将不再调用。

    接下去就是Move事件了

 public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * This method JUST determines whether we want to intercept the motion.
         * If we return true, onMotionEvent will be called and we do the actual
         * scrolling there.
         */

        /*
        * Shortcut the most recurring case: the user is in the dragging
        * state and he is moving his finger.  We want to intercept this
        * motion.
        */
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }
..........
}

   第一关,不是isBeingDragged,所以忽略,继续往下走。

final int y = (int) ev.getY(pointerIndex);
                final int yDiff = Math.abs(y - mLastMotionY);
                if (yDiff > mTouchSlop
                        && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
                    ...
                }

    至少在NestedScrollView嵌套的情况下

    (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0

    永远不成立,所以这段代码且略过。

    所以接下去还是child的onTouch方法。

case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                    break;
                }

                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }
                ....
                break;

    首先会调用dispatchNestedPreScroll方法,该方法意义在开篇有说。在NestedScrollView中,该方法仅仅只是级联调用,在本例情况下(双NestedScrollView嵌套)实际上最终并没有什么效果,返回false。

if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    mLastMotionY = y - mScrollOffset[1];

                    final int oldY = getScrollY();
                    final int range = getScrollRange();
                    final int overscrollMode = getOverScrollMode();
                    boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
                            || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

                    // Calling overScrollByCompat will call onOverScrolled, which
                    // calls onScrollChanged if applicable.
                    if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
                            0, true) && !hasNestedScrollingParent()) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }

                    final int scrolledDeltaY = getScrollY() - oldY;
                    final int unconsumedY = deltaY - scrolledDeltaY;
                    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    } else if (canOverscroll) {
                        ensureGlows();
                        final int pulledToY = oldY + deltaY;
                        if (pulledToY < 0) {
                            mEdgeGlowTop.onPull((float) deltaY / getHeight(),
                                    ev.getX(activePointerIndex) / getWidth());
                            if (!mEdgeGlowBottom.isFinished()) {
                                mEdgeGlowBottom.onRelease();
                            }
                        } else if (pulledToY > range) {
                            mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
                                    1.f - ev.getX(activePointerIndex)
                                            / getWidth());
                            if (!mEdgeGlowTop.isFinished()) {
                                mEdgeGlowTop.onRelease();
                            }
                        }
                        if (mEdgeGlowTop != null
                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                            ViewCompat.postInvalidateOnAnimation(this);
                        }
                    }
                }

    如果大于一个滚动阈值,那么就会进入下面操作了。

    requestDisallowInterceptTouchEvent,表示禁用父控件的onIntercept方法。所以最后的事件实际上只有最底层的NestedScrollView能够去处理,parent连onIntercept都不会调用(需要主要,如果当前的点击不是发生在子NestedScrollView的控件上的话实际上就不会涉及级联滚动的,相当于单层scrollview而已)。

    并且把isBegingDragged设置为true,表示需要开始滚动了。

    主要回去调用dispatchNestedScroll方法,通知父控件消费滚动距离。

 

转载于:https://my.oschina.net/zzxzzg/blog/891397

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值