前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >RecyclerView的NestedScroll实现

RecyclerView的NestedScroll实现

作者头像
None_Ling
发布2018-10-24 14:40:40
1K0
发布2018-10-24 14:40:40
举报
文章被收录于专栏:Android相关Android相关Android相关

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的流程接近,也是先询问耗费多少再在内部进行处理

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.07.03 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • RecyclerView中的NestedScroll流程
  • NestedScroll的实现
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档