从NestedScrollView解读NestedScroll

前言

CoordinatorLayout已出来好久了,该知识点是一个android工程师需必会的,其实研读源码还能够开拓思维,大家有时间都看看源码。

以最新v4包25.1.1版本忠的NestedScrollView为例说明。

NestedScroll包含两部分:NestedScrollingParent和NestedScrollingChild。

源码解读

习惯性的,我们从响应触摸事件的子视图说起。

对于子视图,实现的是NestedScrollingChild接口。触摸事件按下,调用子视图的onInterceptTouchEvent方法(先调用的是父视图的onInterceptTouchEvent方法,此处不关注)。

子视图的MotionEvent.ACTION_DOWN

在MotionEvent.ACTION_DOWN代码块下可看到这样一句:startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
该方法:

@Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }

mChildHelper是一个NestedScrollingChildHelper类,是辅助处理滚动事件的。相应的,NestedScrollingParent的辅助类为NestedScrollingParentHelper。具体的操作都在NestedScrollingChildHelper,让我们来揭开其神秘的面纱吧。

public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {//有嵌套滚动的父视图,说明已经在嵌套滚动中了,刚开始都是没有嵌套滚动的父视图的
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {//能嵌套滚动则进
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {//这里一层一层往上找父视图,看其是否需要嵌套滚动
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {//该视图需要嵌套滚动,则将该mNestedScrollingParent赋值,
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);//接受嵌套滚动的父视图的相应回调(onNestedScrollAccepted)做一些初始化操作
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

关键代码:ViewParentCompat.onStartNestedScroll(p, child, mView, axes),对于Lollipop以前的:

public boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes) {
            if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
            return false;
        }

会判断父视图是否实现了NestedScrollingParent。而对于Lollipop之后的:

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes) {
        try {
            return parent.onStartNestedScroll(child, target, nestedScrollAxes);
        } catch (AbstractMethodError e) {
            Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                    "method onStartNestedScroll", e);
            return false;
        }
    }

其调用的是父视图重写的onStartNestedScroll方法。
对于Lollipop之后的版本,ViewParent中定义了与NestedScrollingParent一样的方法,而NestedScrollingChild中的方法对应View中的方法。
从上面的过程来看:

结论一,子视图接收触摸事件,然后回调父视图的对应NestedScrollingParent中的方法。

如果父视图需要嵌套滚动,则父视图的onStartNestedScroll方法返回true。上面这一过程还回调了父视图的onNestedScrollAccepted方法,一般在该方法中做初始配置。

子视图的MotionEvent.ACTION_MOVE

在onTouchEvent的MotionEvent.ACTION_MOVE代码块下找到关键代码:
                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];
                }

具体操作都是在NestedScrollingChildHelper中,具体代码:

public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {//获取子视图的窗体位置,方便计算偏移量
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    if (mTempNestedScrollConsumed == null) {
                        mTempNestedScrollConsumed = new int[2];
                    }
                    consumed = mTempNestedScrollConsumed;
                }
                consumed[0] = 0;
                consumed[1] = 0;//供父视图使用的消费数组重置
                ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);//回调父视图的onNestedPreScroll方法,回调方法中对consumed数组做操作

                if (offsetInWindow != null) {//因为父视图的onNestedPreScroll方法中做了视图滚动操作(scrollTo或者其他使视图变动的操作),计算偏移量
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                //父视图消费了就返回true。
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

如果父视图消费了,再看下上上部分的关键代码:

                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];

这里会将滑动偏移量deltaY减去父视图消费的部分。mNestedYOffset 加上滚动的偏移量,而在onTouchEvent刚开始时:

        MotionEvent vtev = MotionEvent.obtain(ev);//复制一份触摸事件

        final int actionMasked = MotionEventCompat.getActionMasked(ev);

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }
        vtev.offsetLocation(0, mNestedYOffset);//设置嵌套滚动的y偏移量

往后看

            //overScrollByCompat方法中最终会调用scrollTo方法来滚动
                    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;//滚动之后的scrollY减去滚动之前的scrollY得到滚动偏移量
                    final int unconsumedY = deltaY - scrolledDeltaY;//得到没消耗的y值
                    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    }

关于该方法的最后一部分,在NestedScrollView嵌套NestedScrollView的情况下,如果子视图在子视图的滚动范围内可滚动,则unconsumedY 为0,当子视图滚动到子视图的边界继续滚动时,scrolledDeltaY为0,unconsumedY 为deltaY,则回调父视图的onNestedScroll方法时父视图做滚动,这样就实现父视图与子视图的无缝滚动。

对于fly方面就不做描述了,其在MotionEvent.ACTION_UP下的代码块里,相信有了上面的分析过程,很轻松就能看懂。

对于父视图而言,如果想继续向上提供触摸事件,可用NestedScrollingChild中的方法继续分发事件,如NestedScrollView中的

@Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        dispatchNestedPreScroll(dx, dy, consumed, null);
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值