从NestedScrollingChild、NestedScrollingParent源码分析嵌套滑动机制

前言

1、类概述:
NestedScrollingChild接口
定义了“与父view交互”的方法,需要嵌套滑动的子view就实现它;

NestedScrollingChildHelper帮助类
协助子view(如RecyclerView)实现“与父view交互”的方法。在实现这些方法时,发现有很多逻辑是可以共用的,所以抽了一个Helper类,专门来实现这些逻辑,让其它需要嵌套滑动的子view可以复用,也让RecyclerView的代码更简介、易懂;

NestedScrollingParent接口
定义了“与子view交互”的方法,需要嵌套滑动的父view就实现它;

NestedScrollingParentHelper帮助类
功能跟上面的xxxChildHelper一样,但是由于不同父view与子view交互差别很大,所以这个帮助类里并没有多少功能,具体实现还是在父view里自己实现;

2、嵌套滑动流程概述:
子view接收touch事件,开始滑动前,通过ViewParentCompat这个类通知父view,父view做相应处理;
子view滑动后,通知父view自己的滑动情况,父view做相应处理;


滑动机制源码分析

NestedScrollingChild和NestedScrollingParent中的方法都是一一对应的,子view通过某个方法通知父view,父view通过某个方法接收到通知,对应关系如下:

启动嵌套滑动:startNestedScroll() —— onStartNestedScroll();
滑动前:dispatchNestedPreScroll() —— onNestedPreScroll();
滑动:dispatchNestedScroll() —— onNestedScroll();
fling滑动前:dispatchNestedPreFling() —— onNestedPreFling();
fling滑动:dispatchNestedFling() —— onNestedFling();
停止滑动:stopNestedScroll() —— onStopNestedScroll();

下面就用RecyclerView、NestedScrollView的源码来分析(由于NestedScrollingParentHelper非常简单,也没做什么实际实现,下面的源码分析中就不贴出了)。

假设子view是RecyclerView,父view是NestedScrollView,父父view是也是NestedScrollView(后面简称爷view),总共三层。

启动嵌套滑动

RecyclerView源码:

// 在dispatchTouchEvent、onTouchEvent方法中,都会调用startNestedScroll()
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;
    }
    // 如果canScroll,就启动嵌套滑动,通知父view。至于如何通知,见下面源码
    startNestedScroll(nestedScrollAxis);
} break;


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) {
            // 通知父view,如果父view接受嵌套滑动,就调用其onNestedScrollAccepted()方法
            // 如果父view不接受嵌套滑动,就继续通知爷view......直到找到一个接受的
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                mNestedScrollingParent = p;
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                // 如果找到一个接受嵌套滑动的父view,就直接返回了,不再继续通知爷view了
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}


NestedScrollView源码:

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    // 如果是纵向滑动,就接受,返回true;否则就返回false
    return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

 @Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
    // 记录嵌套滑动的方向
    mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    // 父view接受了嵌套滑动之后,子view就直接返回,不再继续通知爷view了,所以通知爷view就由父view来完成;
    // 父view也继承了NestedScrollingChild接口,并和子view一样使用同样的helper,通知爷view的方式也是一样
}


滑动前

RecyclerView源码:

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;

    // 通知父view,把滑动情况通知给父view。如果父view处理了,就减去父view消费的距离,剩下的就是子view需要滑动的距离
    // 把实参mScrollConsumed传过去,父、爷view消费的距离会记录在上面
    if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
        dx -= mScrollConsumed[0];
        dy -= mScrollConsumed[1];
        vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        // Updated the nested offsets
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += 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;
            // 通知父view执行onNestedPreScroll()方法
            ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

            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;
}


NestedScrollView源码:

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        // 跟子view一样,把滑动情况通知给爷view
        dispatchNestedPreScroll(dx, dy, consumed, null);
    }


滑动后

RecyclerView源码:

// 该方法在dispatchTouchEvent、onTouchEvent中调用,调用时已完成滑动。参数x、y分别是手指滑动的距离
boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int unconsumedX = 0, unconsumedY = 0;
    int consumedX = 0, consumedY = 0;

    consumePendingUpdateOperations();
    if (mAdapter != null) {
        eatRequestLayout();
        onEnterLayoutOrScroll();
        TraceCompat.beginSection(TRACE_SCROLL_TAG);
        if (x != 0) {
            // 消费的距离就是子view真实滑动的距离
            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();
        resumeRequestLayout(false);
    }
    if (!mItemDecorations.isEmpty()) {
        invalidate();
    }

    // 调用dispatchNestedScroll()通知父view本次滑动情况
    if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
        // Update the last touch co-ords, taking any scroll offset into account
        // mScrollOffset记录的是,因为父view的滑动,导致子view的偏移。通过这个偏移量,重新计算当前手指的位置,保证下次滑动的准确性
        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) {
            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;
}


NestedScrollingChildHelper源码:

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
            int startX = 0;
            int startY = 0;
            if (offsetInWindow != null) {
                // 父view滑动前,子view的位置
                mView.getLocationInWindow(offsetInWindow);
                startX = offsetInWindow[0];
                startY = offsetInWindow[1];
            }

            // 通知父view调用onNestedScroll()方法
            ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed);

            if (offsetInWindow != null) {
                // 父view滑动后,子view的位置
                mView.getLocationInWindow(offsetInWindow);
                // 父view滑动后的位置减去之前的位置,得到偏移量
                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;
}


NestedScrollView源码:

@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed) {
    final int oldScrollY = getScrollY();
    // 滑动dyUnconsumed
    scrollBy(0, dyUnconsumed);
    final int myConsumed = getScrollY() - oldScrollY;
    final int myUnconsumed = dyUnconsumed - myConsumed;
    // 通知爷view滑动情况。由于把子view未消费的dy全部消费掉了,所以爷view在dy上不会有滑动了,也就自然不需要传实参偏移量了
    dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
}


fling滑动前

RecyclerView源码:

// 在onTouchEvent方法的Action.UP中,会根据当前手指离开屏幕的方向、速度等,计算出最终可以滑动的距离,然后调用fling()方法,下面的代码是fling()方法中的一段

// fling滑动前,通知父view,如果父view接受fling滑动,子view将不再处理fling滑动;否则就自己fling滑动
if (!dispatchNestedPreFling(velocityX, velocityY)) {
    final boolean canScroll = canScrollHorizontal || canScrollVertical;
    dispatchNestedFling(velocityX, velocityY, canScroll);

    if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
        return true;
    }

    if (canScroll) {
        velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
        velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
        mViewFlinger.fling(velocityX, velocityY);
        return true;
    }
}


NestedScrollingChildHelper源码:

public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        // 通知父view调用onNestedPreFling()方法
        return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
                    velocityY);
    }
    return false;
}


NestedScrollView源码:

@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
    // 继续通知爷view
    return dispatchNestedPreFling(velocityX, velocityY);
}


fling滑动后

RecyclerView源码:

// 跟fling滑动前是同一段代码,就不做太多解释

// 如果父view、爷view都没有拦截,就通知父view,并执行fling滑动
if (!dispatchNestedPreFling(velocityX, velocityY)) {
    final boolean canScroll = canScrollHorizontal || canScrollVertical;
    // 如果自己能滑动,就自己fling,并通知父view
    dispatchNestedFling(velocityX, velocityY, canScroll);

    // 如果mOnFlingListener为null,或正在fling中,就返回
    if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
        return true;
    }

    if (canScroll) {
        velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
        velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
        // 开始fling滑动
        mViewFlinger.fling(velocityX, velocityY);
        return true;
    }
}


NestedScrollingChildHelper源码:

public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        // 通知父view调用onNestedFling()方法
        return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
                    velocityY, consumed);
    }
    return false;
}


NestedScrollView源码:

    @Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
    // 如果子view没有滑动,就自己滑动
    if (!consumed) {
        flingWithNestedDispatch((int) velocityY);
        return true;
    }
    return false;
}
// 这里有2个值得注意的点:
// 1、fling传递的参数不再是消费多少、未消费多少,而是只有一个速度,说明google其实并不推荐两个控件同时fling;
// 2、父view执行完fling后,也没有向爷view通知,可能也是不希望让爷view再对fling进行处理;


停止滑动

RecyclerView源码:

// dispatchTouchEvent中,在ACTION_UP和ACTION_CANCEL中调用了stopNestedScroll()方法
case MotionEvent.ACTION_UP: {
            mVelocityTracker.clear();
            stopNestedScroll();
        } break;

        case MotionEvent.ACTION_CANCEL: {
            cancelTouch();
        }


NestedScrollingChildHelper源码:

public void stopNestedScroll() {
    if (mNestedScrollingParent != null) {
        // 通知父view调用onStopNestedScroll()方法
        ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
        mNestedScrollingParent = null;
    }
}


NestedScrollView源码:

@Override
public void onStopNestedScroll(View target) {
    // 停止滑动,并通知爷view
    mParentHelper.onStopNestedScroll(target);
    stopNestedScroll();
}
发布了35 篇原创文章 · 获赞 52 · 访问量 8万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章