Android NestScroll機制源碼解析

  • 何為嵌套滑動,這里不多說,列張效果圖,上面是TopView(一般是banner類),下面是RecyclerView,當recyclerView向上滑動時,topView跟隨往上滑動至隱藏后吸頂固定,recyclerView下拉到頂時繼續(xù)下拉,則把topView拉回初始位置:


    nestscroll.gif
  • 嵌套滑動關鍵的兩個接口:NestedScrollingChild2NestedScrollingParent2,繼承于NestedScrollingChildNestedScrollingParent,在回調中增加了事件類型,便于處理fling慣性滑動狀態(tài)管理。目前最新的是NestedScrollingChild3NestedScrollingParent3,在此僅分析2類。
  • 首先看下兩個接口定義:
<NestedScrollingChild2.java>
嵌套滑動的內部View需要實現(xiàn)的接口
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);

    /**
     * 嵌套滑動后的事件分發(fā)
     *
     * <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);

    /**
     * 嵌套滑動前的事件分發(fā)
     *
     * <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需要實現(xiàn)的接口
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會立即觸發(fā)下面的onNestedScrollAccepted方法
     */
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
            @NestedScrollType int type);

    /**
     *
     * @param child 產生滑動事件的view的parent,向上遞歸查找到的實現(xiàn)了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);

}
  • 下面以RecyclerView為例,分析下NestedScrollingChild2的流程。
<RecyclerView.java>
// RecyclerView默認已經實現(xiàn)了NestedScrollingChild2接口
public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
        ...
}
  • 要觸發(fā)拖拽滑動,肯定是從touch事件開始,定位到RecyclerViewonTouchEvent方法:
<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: {
                // 多指場景,多指按下后重置初始坐標和觸發(fā)滑動手指
                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: {
                // 獲取觸發(fā)滑動的那個手指
                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狀態(tài),手指按下并移動距離大于臨界值,會標記為拖拽狀態(tài)
                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);
                    }
                }
                // 拖拽狀態(tài)
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    // mReusableIntPair這個數(shù)組保存了NestScrollParent消費的距離
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
                    // 開始分發(fā)嵌套滑動的距離
                    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];
                    // 自己處理剩余的滑動距離,并觸發(fā)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);
                }
                // 重置狀態(tài),通知嵌套滑動結束
                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方法中處理嵌套滑動的流程很清晰:
<RecyclerView.java>
1.ACTION_DOWN 開始嵌套滑動,傳入滑動方向和滑動類型:
        startNestedScroll(nestedScrollAxis, TYPE_TOUCH); ->NestScrollParent的onStartNestedScroll
2.ACTION_MOVE 開始分發(fā)嵌套滑動距離:
        dispatchNestedPreScroll(canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    ) -> NestScrollParent的onNestedPreScroll
        scrollByInternal() -> NestScrollParent的onNestedScroll
3.ACTION_UP 開始通知嵌套滑動結束:
        resetScroll(); ->onStopNestedScroll

    // 其內部都是調用NestedScrollingChildHelper的具體實現(xiàn)
    @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中這幾個方法對應的實現(xiàn)就行了,首先在down事件中開始嵌套滑動的一些準備工作,里面會觸發(fā)NestScrollParentonStartNestedScrollonNestedScrollAccepted方法:
<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
<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,構造存儲消費滑動距離數(shù)據(jù)的數(shù)組
                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();
        }
        // 重置數(shù)組,這個數(shù)組只是用來存儲數(shù)據(jù)而已,多處重置復用
        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事件中會重置嵌套滑動狀態(tài),并調用stopNestedScroll,至此,NestScrollChild的整個流程結束。
    public void stopNestedScroll(@NestedScrollType int type) {
        ViewParent parent = getNestedScrollingParentForType(type);
        if (parent != null) {
            // 回調NestScrollParent的onStopNestedScroll,本次嵌套滑動結束
            ViewParentCompat.onStopNestedScroll(parent, mView, type);
            setNestedScrollingParentForType(type, null);
        }
    }
  • 接下去,以實現(xiàn)上面示例圖常見的TopView+RecyclerView的界面為例,分析下NestScrollParent的流程。首先自定義一個LinearLayout布局去實現(xiàn)NestScrollingParent2接口:
<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  實現(xiàn)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  實現(xiàn)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優(yōu)先向上滑動
        boolean hideTop = dy > 0 && getScrollY() < mTopViewHeight;
        // 需要顯示topView,topView優(yōu)先向下滑動(一般是recyclerView滑動到頂部時繼續(xù)下拉,展示topView)
        boolean showTop = dy < 0 && getScrollY() >= 0 && !target.canScrollVertically(-1);
        if (hideTop || showTop) {
            // topView需要優(yōu)先處理滑動
            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則繼續(xù)往下滑
        if (y < 0) {
            y = 0;// topView初始狀態(tài)
        }
        // y大于topView的高度了,則topView不應該繼續(xù)往上滑動
        if (y > mTopViewHeight) {
             // 內容滑動最大為topView的高度
            y = mTopViewHeight;
        }
        super.scrollTo(x, y);
    }
}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容