RecyclerView的NestedScroll实现

RecyclerView中的NestedScroll流程

  1. 手指触碰之后,onInterceptTouchEvent触发ACTION_DOWN
    • 调用startNestedScroll,沿着View树往上寻找可以接受嵌套滑动的父View,如果找到了,则会回调父View的onStartNestedScroll以及onNestedScrollAccepted
  2. 当手指滑动的时候,触发onTouchEvent中的ACTION_MOVE
    • 调用dispatchNestedPreScroll将嵌套滑动事件给父View,询问父View需要消费多少距离,其中就会回调父View的onNestedPreScroll
    • 接着调用dispatchNestedScroll将已经消费的距离与未消费的距离回调给父View,是否父View要对当前的View进行移动
  3. 当手指离开屏幕时,触发onInterceptTouchEvent触发ACTION_UP
    • 调用stopNestedScroll将停止事件告诉父View

NestedScroll的实现

  1. 当手指触摸到RecyclerView时,根据Touch事件的传递,会触发onInterceptTouchEvent判断是否要中断事件传递。
    • ACTION_DOWN分支中,会初始化Touch的X,Y位置,并且判断当前RecyclerView是允许横向或者纵向滑动,最后将滑动标志位以及滑动类型交给startNestedScroll
    • ACTION_UP分支中,会调用stopNestedScroll停止嵌套滑动
@Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        ...
        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();
        ...
        final int action = e.getActionMasked();
        final int actionIndex = e.getActionIndex();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                ...
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
                // Clear the nested offsets
                mNestedOffsets[0] = mNestedOffsets[1] = 0;
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
                break;
            case MotionEvent.ACTION_UP: {
                ...
                stopNestedScroll(TYPE_TOUCH);
            } break;
            ...
        }
        return mScrollState == SCROLL_STATE_DRAGGING;
    }

PS:其中省略部分与嵌套滑动无关的代码

  1. startNestedScroll中,调用NestedScrollingChildHelperstartNestedScroll函数
    • hasNestedScrollingParent:判断当前是否有正在进行嵌套滑动的父View,如果有的话说明当前正处于滑动状态,直接返回不用处理
    • 判断isNestedScrollingEnabled嵌套滑动是否可用,如果不可用则直接返回false
    • 递归向父View调用onStartNestedScroll询问,是否可以开始嵌套滑动,如果允许的话,则调用setNestedScrollingParentForType设置当前嵌套滑动的父View,并且调用onNestedScrollAccepted,这个函数主要用来设置嵌套滑动的方向(横向/纵向)
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    setNestedScrollingParentForType(type, p);
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }
  1. 当开始滑动时,ACTION_MOVE的分支代码中:
    • 计算当前与ACTION_DOWN之间移动的距离,也就是dx,dy
    • 调用dispatchNestedPreScroll将嵌套滑动的事件Pre-Scrolling分发给父View,并且判断父View需要消费(consume)多少,返回值代表父View是否有消费距离。如果有消费的话,则将dx,dy减去消费的距离
    • 如果当前的状态处于SCROLL_STATE_DRAGGING即正在拖动的话,则会将mLastTouchX,mLastTouchY更新,如果不更新的话,那么下一次计算的数据会出错。
    • 更新完后,会调用scrollByInternal开始滑动``dx,dy```的距离
@Override
    public boolean onTouchEvent(MotionEvent e) {
        ...
        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();
        ...
        final MotionEvent vtev = MotionEvent.obtain(e);
        final int action = e.getActionMasked();
        final int actionIndex = e.getActionIndex();

        if (action == MotionEvent.ACTION_DOWN) {
            mNestedOffsets[0] = mNestedOffsets[1] = 0;
        }
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mScrollPointerId = e.getPointerId(0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            } break;
            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id "
                            + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;

                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }

                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        if (dx > 0) {
                            dx -= mTouchSlop;
                        } else {
                            dx += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        if (dy > 0) {
                            dy -= mTouchSlop;
                        } else {
                            dy += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
            } break;
            case MotionEvent.ACTION_CANCEL: {
                cancelTouch();
            } break;
            ...
        }
        vtev.recycle();
        return true;
    }
  1. dispatchNestedPreScroll调用的就是NestedScrollChildHelper.dispatchNestedPreScroll
    • 首先判断嵌套滑动是否可用
    • 判断dx,dy是否均为0,如果均为0的话,则代表没有偏移
    • 如果有偏移的话,则从当前的View获取在Window中的偏移量
    • 调用ViewParent. onNestedPreScroll函数传入当前偏移的距离dx,dy让父View判断需要消费多少距离,通过consumed数据传回
    • 最后计算完offsetInWindow偏移量后返回是否父View有消费
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            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(parent, mView, dx, dy, consumed, type);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }
  1. scrollByInternal函数中,开始进行RecyclerView的滑动
    • 通过mLayout.scrollHorizontallyBy获取LayoutManager的对于scroll的消费距离
    • 调用dispatchNestedScroll告知父View已经消费的距离consumedX,consumedY以及还剩余的距离unconsumedX,unconsumedY
    • 当父View处理完消费的X以及Y之后,更新mLastTouchX,mLastTouchY
boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0, unconsumedY = 0;
        int consumedX = 0, consumedY = 0;

        consumePendingUpdateOperations();
        if (mAdapter != null) {
            startInterceptRequestLayout();
            onEnterLayoutOrScroll();
            TraceCompat.beginSection(TRACE_SCROLL_TAG);
            fillRemainingScrollValues(mState);
            if (x != 0) {
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                unconsumedX = x - consumedX;
            }
            if (y != 0) {
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }
            TraceCompat.endSection();
            repositionShadowingViews();
            onExitLayoutOrScroll();
            stopInterceptRequestLayout(false);
        }
        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }

        if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH)) {
            // Update the last touch co-ords, taking any scroll offset into account
            mLastTouchX -= mScrollOffset[0];
            mLastTouchY -= mScrollOffset[1];
            if (ev != null) {
                ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
            }
            mNestedOffsets[0] += mScrollOffset[0];
            mNestedOffsets[1] += mScrollOffset[1];
        } else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
            if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
                pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
            }
            considerReleasingGlowsOnScroll(x, y);
        }
        if (consumedX != 0 || consumedY != 0) {
            dispatchOnScrolled(consumedX, consumedY);
        }
        if (!awakenScrollBars()) {
            invalidate();
        }
        return consumedX != 0 || consumedY != 0;
    }
  1. dispatchNestedScroll调用的NestedScrollChildHelper.dispatchNestedScroll
    • 判断是否支持嵌套滑动
    • 回调父View的onNestedScroll将已经消费的距离与未消费的距离传入
    • 在父View中的回调函数中可以操作RecyclerView进行移动
    • 移动完后得到与移动前的偏移差,返回给scrollByInternal进行滑动距离的计算
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed, type);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return true;
            } else if (offsetInWindow != null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }
  1. 最后在onInterceptTouchEvent中的ACTION_UP中调用stopNestedScroll结束整个嵌套滑动的过程

Fling的流程与Touch的流程接近,也是先询问耗费多少再在内部进行处理

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Sorrower的专栏

Android弹窗二则: PopupWindow和AlertDialog

android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app....

1995
来自专栏Android学习之路

ViewDragHelper使用笔记及侧滑菜单实践

3376
来自专栏james大数据架构

Android网格视图(GridView)

GridView的一些属性: 1.android:numColumns=”auto_fit”   //GridView的列数设置为自动,也可以设置成2、3、4…...

2468
来自专栏Android开发指南

7.侧滑、ViewDragHelper、属性动画

3245
来自专栏jianhuicode

学问Chat UI(1)

前言 由于项目需要,最近开始借鉴学习下开源的Android即时通信聊天UI框架,为此结合市面上加上本项目需求列了ChatUI要实现的基本功能与扩展功能。 ? 融...

2499
来自专栏李蔚蓬的专栏

Material Design 实战 之第四弹 —— 卡片布局

首先这里准备用CardView来填充主题内容, CardView是用于实现卡片式布局效果的重要控件,由appcompat-v7库提供。 实际上,CardVi...

1271
来自专栏移动开发

FruitLoadView 一个自定义view可用来做加载view

Github地址:https://github.com/X-FAN/FruitLoadView 欢迎star

892
来自专栏androidBlog

你真的了解View的坐标吗?

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gdutxiaoxu/article/details/...

822
来自专栏计算机编程

点击显示更多文本自定义控件

写在前面的话: 在正常项目流程中,我们很多情况下会碰到点击显示更多文本,这样可以利于页面变化加载,点击显示更多可能会非常常用,现在博主利用自己的闲暇时间来一点一...

1503
来自专栏xingoo, 一个梦想做发明家的程序员

【插件开发】—— 7 SWT布局详解,不能再详细了!

前文回顾: 1 插件学习篇 2 简单的建立插件工程以及模型文件分析 3 利用扩展点,开发透视图 4 SWT编程须知 5 SWT简单控件的使用与布局搭...

23110

扫码关注云+社区

领取腾讯云代金券