从 Android 开发到读懂源码 第02期:NestScroll 机制源码解析

2022-04-25 08:45:22 浏览数 (2)

作者简介

罗铁锤,六年安卓踩坑经验,致力于底层平台、上层应用等多领域开发。文能静坐弹吉他,武能通宵写代码。

Android 提供了一个官方的嵌套滑动机制,这一节内容我们就一起来聊聊吧。

1 何为嵌套滑动

首先我们来看一看嵌套滑动的效果,上面是 TopView (一般是banner类),下面是 RecyclerView,当 recyclerView 向上滑动时,topView 跟随往上滑动至隐藏后吸顶固定,recyclerView 下拉到顶时继续下拉,则把 topView 拉回初始位置:

2 嵌套滑动关键的两个接口

NestedScrollingChild2 和 NestedScrollingParent2,继承于 NestedScrollingChild 和 NestedScrollingParent,在回调中增加了事件类型,便于处理 fling 惯性滑动状态管理。目前最新的是 NestedScrollingChild3 和 NestedScrollingParent3,在此仅分析 2 类。

首先看下两个接口定义:

代码语言:javascript复制
<NestedScrollingChild2.java>
嵌套滑动的内部 View 需要实现的接口
public interface NestedScrollingChild2 extends NestedScrollingChild {

    /**
     * 开始滑动时应该调用
     * @param axes 滑动方向 {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}
     *             and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}.
     * @param type 事件类型:ViewCompat.TYPE_TOUCH 和 TYPE_NON_TOUCH
     * @return 遍历 parent 并且查找是否有 NestedScrollingParent 的父 view,如果存在切父 view 需要处理
     *         嵌套滑动则返回 true
     */
    boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);

    /**
     * 滑动停止时应该调用
     * @param type 事件类型:ViewCompat.TYPE_TOUCH和TYPE_NON_TOUCH
     */
    void stopNestedScroll(@NestedScrollType int type);

    /**
     * 是否存在 NestedScrollingParent 的父 view
     * @param type 事件类型:ViewCompat.TYPE_TOUCH 和 TYPE_NON_TOUCH
     * @return 是否存在 NestedScrollingParent 的父 view
     */
    boolean hasNestedScrollingParent(@NestedScrollType int type);

    /**
     * 嵌套滑动后的事件分发
     *
     * <p>Implementations of views that support nested scrolling should call this to report
     * info about a scroll in progress to the current nested scrolling parent. If a nested scroll
     * is not currently in progress or nested scrolling is not
     * {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.</p>
     *
     * <p>Compatible View implementations should also call
     * {@link #dispatchNestedPreScroll(int, int, int[], int[], int) dispatchNestedPreScroll} before
     * consuming a component of the scroll event themselves.</p>
     *
     * @param dxConsumed 水平消费的距离
     * @param dyConsumed 垂直消费的距离
     * @param dxUnconsumed 水平消费后剩余的距离
     * @param dyUnconsumed 垂直消费后剩余的距离
     * @param offsetInWindow Optional. If not null, on return this will contain the offset
     *                       in local view coordinates of this view from before this operation
     *                       to after it completes. View implementations may use this to adjust
     *                       expected input coordinate tracking.
     * @param type 事件类型:ViewCompat.TYPE_TOUCH和TYPE_NON_TOUCH
     * @return true if the event was dispatched, false if it could not be dispatched.
     * @see #dispatchNestedPreScroll(int, int, int[], int[], int)
     */
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type);

    /**
     * 嵌套滑动前的事件分发
     *
     * <p>Nested pre-scroll events are to nested scroll events what touch intercept is to touch.
     * <code>dispatchNestedPreScroll</code> offers an opportunity for the parent view in a nested
     * scrolling operation to consume some or all of the scroll operation before the child view
     * consumes it.</p>
     *
     * @param dx 该 child 水平滑动距离
     * @param dy 该 child 垂直滑动距离
     * @param consumed NestScrollParent 消费的距离:consumed[0] 代表水平消费距离,consumed[1] 代表垂直消费距离
     * @param offsetInWindow Optional. If not null, on return this will contain the offset
     *                       in local view coordinates of this view from before this operation
     *                       to after it completes. View implementations may use this to adjust
     *                       expected input coordinate tracking.
     * @param type 事件类型:ViewCompat.TYPE_TOUCH和TYPE_NON_TOUCH
     * @return true 表示 parent 消费了滑动距离
     */
    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type);
}

<NestedScrollingParent2.java>
嵌套滑动的外部 ViewGroup 需要实现的接口
public interface NestedScrollingParent2 extends NestedScrollingParent {
    /**
     *
     * @param child Direct child of this ViewParent containing target
     * @param target View that initiated the nested scroll
     * @param axes 滑动方向: {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both
     * @param type 事件类型:ViewCompat.TYPE_TOUCH和TYPE_NON_TOUCH
     * @return true 表示该 parent 需要处理该次嵌套滑动,true 会立即触发下面的 onNestedScrollAccepted 方法
     */
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
            @NestedScrollType int type);

    /**
     *
     * @param child 产生滑动事件的 view 的 parent,向上递归查找到的实现了 NestScrollParent 的父 view
     * @param target 产生滑动事件的 view
     * @param axes 滑动方向: {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both
     * @param type 事件类型:ViewCompat.TYPE_TOUCH 和 TYPE_NON_TOUCH
     */
    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
            @NestedScrollType int type);

    /**
     *
     * @param target 产生滑动事件的 view
     * @param type 事件类型:ViewCompat.TYPE_TOUCH和TYPE_NON_TOUCH
     */
    void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);

    /**
     *
     * @param target 产生滑动事件的 view
     * @param dxConsumed target 已经消费的水平距离
     * @param dyConsumed target 已经消费的垂直距离
     * @param dxUnconsumed target 消费后剩余的水平距离
     * @param dyUnconsumed target 消费后剩余的水平距离
     * @param type 事件类型:ViewCompat.TYPE_TOUCH和TYPE_NON_TOUCH
     */
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);

    /**
     *
     * @param target 产生滑动事件的 view
     * @param dx target 水平滑动距离
     * @param dy target 垂直滑动距离
     * @param consumed 需要消费的距离,consumed[0] 代表水平消费距离,consumed[1] 代表垂直消费距离,默认 0
     * @param type 事件类型:ViewCompat.TYPE_TOUCH 和 TYPE_NON_TOUCH
     */
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
            @NestedScrollType int type);

}
2.1 NestedScrollingChild2 流程分析

下面以 RecyclerView 为例:

代码语言:javascript复制
<RecyclerView.java>
// RecyclerView 默认已经实现了 NestedScrollingChild2 接口
public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
        ...
}

要触发拖拽滑动,肯定是从 touch 事件开始,定位到 RecyclerView 的 onTouchEvent 方法:

代码语言:javascript复制
<RecyclerView.java>
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        ...
        // 是水平还是垂直滚动
        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        boolean eventAddedToVelocityTracker = false;

        final int action = e.getActionMasked();
        final int actionIndex = e.getActionIndex();
        // 重置
        if (action == MotionEvent.ACTION_DOWN) {
            mNestedOffsets[0] = mNestedOffsets[1] = 0;
        }
        final MotionEvent vtev = MotionEvent.obtain(e);
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

        switch (action) {
            // dwon
            case MotionEvent.ACTION_DOWN: {
                mScrollPointerId = e.getPointerId(0);
                // 初始 down 坐标
                mInitialTouchX = mLastTouchX = (int) (e.getX()   0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY()   0.5f);
                // nestedScrollAxis 滑动方向标记
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                // 1.开始嵌套滑动:touch 拖拽类型
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            } break;

            case MotionEvent.ACTION_POINTER_DOWN: {
                // 多指场景,多指按下后重置初始坐标和触发滑动手指
                mScrollPointerId = e.getPointerId(actionIndex);
                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex)   0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex)   0.5f);
            } break;
            // move
            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;
                }
                // move 坐标
                final int x = (int) (e.getX(index)   0.5f);
                final int y = (int) (e.getY(index)   0.5f);
                // 计算 move 距离
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
                // fling 状态,手指按下并移动距离大于临界值,会标记为拖拽状态
                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally) {
                        if (dx > 0) {
                            dx = Math.max(0, dx - mTouchSlop);
                        } else {
                            dx = Math.min(0, dx   mTouchSlop);
                        }
                        if (dx != 0) {
                            startScroll = true;
                        }
                    }
                    ...
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }
                // 拖拽状态
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    // mReusableIntPair 这个数组保存了 NestScrollParent 消费的距离
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
                    // 开始分发嵌套滑动的距离
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        // 计算 NestScrollParent 消费后剩余的距离,供自己消费
                        dx -= mReusableIntPair[0];
                        dy -= mReusableIntPair[1];
                        // Updated the nested offsets
                        mNestedOffsets[0]  = mScrollOffset[0];
                        mNestedOffsets[1]  = mScrollOffset[1];
                        // 要求 parent 不要拦截该 touch 事件,由自己处理
                        // Scroll has initiated, prevent parents from intercepting
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }

                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    // 自己处理剩余的滑动距离,并触发 NestScrollParent 的 onNestedPreScroll
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            e)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

            ...

            // up
            case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally
                        ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically
                        ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                // 重置状态,通知嵌套滑动结束
                resetScroll();
            } break;
        ...
        return true;
    }
    
    // 自己处理滑动
    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        ...
        // 回调 NestScrollParent 的 onNestedScroll 方法
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        ...
        return consumedNestedScroll || consumedX != 0 || consumedY != 0;
    }

    private void resetScroll() {
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
        }
        // 通知嵌套滑动结束
        stopNestedScroll(TYPE_TOUCH);
        releaseGlows();
    }

在 onTouchEvent 方法中处理嵌套滑动的流程很清晰:

  1. ACTION_DOWN 开始嵌套滑动,传入滑动方向和滑动类型: startNestedScroll(nestedScrollAxis, TYPE_TOUCH); ->NestScrollParent的onStartNestedScroll
  2. ACTION_MOVE 开始分发嵌套滑动距离: dispatchNestedPreScroll(canScrollHorizontally ? dx : 0,

canScrollVertically ? dy : 0, mReusableIntPair, mScrollOffset, TYPE_TOUCH ) -> NestScrollParent的onNestedPreScroll scrollByInternal() -> NestScrollParent的onNestedScroll

  1. ACTION_UP 开始通知嵌套滑动结束: resetScroll(); ->onStopNestedScroll
代码语言:javascript复制
<RecyclerView.java>
    // 其内部都是调用 NestedScrollingChildHelper 的具体实现
    @Override
    public boolean startNestedScroll(int axes, int type) {
        return getScrollingChildHelper().startNestedScroll(axes, type);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
            int type) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
                type);
    }

    ...

所以,直接看 NestedScrollingChildHelper 中这几个方法对应的实现就行了,首先在 down 事件中开始嵌套滑动的一些准备工作,里面会触发 NestScrollParent 的 onStartNestedScroll 和 onNestedScrollAccepted 方法:

代码语言:javascript复制
<NestedScrollingChildHelper.java>
    // 开始嵌套滑动,一些准备工作
    public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        // 之前已经确认过存在 NestScrollParent 需要消费事件,直接返回 true
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        // 支持嵌套滑动
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();// 父 ViewParent
            View child = mView;// 当前 recyclerView
            // 遍历查找父 ViewParent,看是否存在需要消费事件的 NestScrollParent
            while (p != null) {
                // 这里会调用 NestScrollParent 的 onStartNestedScroll 方法
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    // 缓存当前 NestScrollParent,下次进来就直接上面 return true 了,避免多余查找操作
                    setNestedScrollingParentForType(type, p);
                    // 如 onStartNestedScroll 返回 true,立即回调 NestScrollParent 的 onNestedScrollAccepted方法
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                // 递归查找
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        // 不存在说明 parent 不需要处理嵌套滑动,返回 false
        return false;
    }

接着 move 事件中会计算出滑动距离,并调用 dispatchNestedPreScroll

代码语言:javascript复制
<NestedScrollingChildHelper.java>
    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;
                // view 在 window 的坐标
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }
                // 如果为 null,构造存储消费滑动距离数据的数组
                if (consumed == null) {
                    consumed = getTempNestedScrollConsumed();
                }
                consumed[0] = 0;
                consumed[1] = 0;
                // 回调 NestScrollParent的onNestedPreScroll 方法
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                // 返回值是 NestScrollParent 是否消费了距离
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

<RecyclerView.java>
    // 自己处理滑动
    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0;
        int unconsumedY = 0;
        int consumedX = 0;
        int consumedY = 0;

        consumePendingUpdateOperations();
        if (mAdapter != null) {
            // mReusableIntPair 这里会重置并且储存自身滑动的距离值
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            // 内部计算出自己需要滑动的距离
            scrollStep(x, y, mReusableIntPair);
            // 自身滑动,消费的距离
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            // 自己滑动后剩余的未消费的距离,这里留个问题:为什么自己不全部消费掉?
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }
        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }
        // 重置数组,这个数组只是用来存储数据而已,多处重置复用
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        // 跟进去最终调用 dispatchNestedScrollInternal,回调 NestScrollParent 的 onNestedScroll 方法
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];// 最终剩余的距离
        unconsumedY -= mReusableIntPair[1];// 最终剩余的距离
        // 看下 NestScrollParent 是否有消费剩余的滑动距离
        boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;

        // Update the last touch co-ords, taking any scroll offset into account
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        mNestedOffsets[0]  = mScrollOffset[0];
        mNestedOffsets[1]  = mScrollOffset[1];
        // 处理 OVER_SCROLL 场景
        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 consumedNestedScroll || consumedX != 0 || consumedY != 0;
    }

<NestedScrollingChildHelper.java>
    private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type, @Nullable int[] consumed) {
        // 可嵌套滑动
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }
            // 存在 child 需要消费或者还剩余未消费距离
            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];
                }

                if (consumed == null) {
                    consumed = getTempNestedScrollConsumed();
                    consumed[0] = 0;
                    consumed[1] = 0;
                }
                // 回调 NestScrollParent 的 onNestedScroll 方法
                ViewParentCompat.onNestedScroll(parent, mView,
                        dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);

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

最后 up 事件中会重置嵌套滑动状态,并调用 stopNestedScroll ,至此,NestScrollChild 的整个流程结束。

代码语言:javascript复制
   public void stopNestedScroll(@NestedScrollType int type) {
        ViewParent parent = getNestedScrollingParentForType(type);
        if (parent != null) {
            // 回调 NestScrollParent 的 onStopNestedScroll,本次嵌套滑动结束
            ViewParentCompat.onStopNestedScroll(parent, mView, type);
            setNestedScrollingParentForType(type, null);
        }
    }
2.2 NestScrollingParent2 接口实现

接下去,以实现上面示例图常见的 TopView RecyclerView 的界面为例,分析下 NestScrollParent 的流程。首先自定义一个 LinearLayout 布局去实现 NestScrollingParent2 接口:

代码语言:javascript复制
<NestScrollLinearLayout.java>
public class NestScrollLinearLayout extends LinearLayout implements NestedScrollingParent2 {
    private View mTopView;// 顶部布局
    private View mRecyclerView;// 下面的 RecyclerView
    private int mTopViewHeight;// 顶部布局高度

    public NestScrollLinearLayout(Context context) {
        this(context, null);
    }

    public NestScrollLinearLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestScrollLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOrientation(VERTICAL);// 设置为垂直方向
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ViewGroup.LayoutParams layoutParams = mRecyclerView.getLayoutParams();
        // 将 recyclerView 的高度设置为当前 parent 高度
        layoutParams.height = getMeasuredHeight();
        mRecyclerView.setLayoutParams(layoutParams);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // xml 解析完成
        if (getChildCount() > 0) {
            mTopView = getChildAt(0);
        }
        if (getChildCount() > 1) {
            mRecyclerView = getChildAt(1);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mTopView != null) {
            // 获取 topView 的高度
            mTopViewHeight = mTopView.getMeasuredHeight();
        }
    }

    /**
     * 即将开始嵌套滑动,由子 view 的 startNestedScroll 方法调用
     *
     * @param child  实现 NestScrollChild 的 View,不一定 =target
     * @param target 产生滑动事件具体的view
     * @param axes   滑动方向
     * @param type   上面分析所说的类型
     * @return true 表示自身需要处理嵌套滑动
     */
    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        // 垂直方向需要处理嵌套滑动
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    /**
     * 当 onStartNestedScroll 返回为 true 时调用
     *
     * @param child  实现 NestScrollChild 的 View,不一定 =target
     * @param target 产生滑动事件具体的 view
     * @param axes   滑动方向
     * @param type   上面分析所说的滑动类型
     */
    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
    }

    /**
     * 在子 view 滑动之前,会先回调此方法,由父 View 决定消耗滑动距离并将消耗的距离赋值给 consumed
     *
     * @param target   产生滑动事件具体的 view
     * @param dx       target 水平方向滑动距离
     * @param dy       target 垂直方向滑动距离
     * @param consumed 返回给 target,parent 消耗的距离
     * @param type     滑动类型
     */
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        // 需要隐藏 topView,topView 优先向上滑动
        boolean hideTop = dy > 0 && getScrollY() < mTopViewHeight;
        // 需要显示 topView,topView 优先向下滑动(一般是 recycelerView 滑动到顶部时继续下拉,展示 topView)
        boolean showTop = dy < 0 && getScrollY() >= 0 && !target.canScrollVertically(-1);
        if (hideTop || showTop) {
            // topView 需要优先处理滑动
            scrollBy(0, dy);
            consumed[1] = dy;
        }
    }

    /**
     * 子 view 消耗剩余距离后,如果还有剩余,则把剩余的距离再次回调给 parent
     *
     * @param target       产生滑动事件具体的 view
     * @param dxConsumed   target 消耗的水平滑动距离
     * @param dyConsumed   target 消耗的垂直滑动距离
     * @param dxUnconsumed target 消耗后剩余的水平滑动距离
     * @param dyUnconsumed target 消耗后剩余的垂直滑动距离
     */
    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
    }

    /**
     * 停止滑动
     *
     * @param target 产生滑动事件具体的 view
     * @param type   滑动类型
     */
    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
    }

    @Override
    public void scrollTo(int x, int y) {
        // 内容滑动不能为负,为负的话 topView 则继续往下滑
        if (y < 0) {
            y = 0;// topView 初始状态
        }
        // y 大于 topView 的高度了,则 topView 不应该继续往上滑动
        if (y > mTopViewHeight) {
             // 内容滑动最大为 topView 的高度
            y = mTopViewHeight;
        }
        super.scrollTo(x, y);
    }
}

0 人点赞