效果图

概况可见:github.com/stewForAni/…
顺便给另一个项目求个Star:github.com/stewForAni/…

NestedScrolling嵌套滑动基础版

布景

一直想抽时间稳固一下嵌套滑动的知识点,尤其是NestedScrolling系列的东西,也查阅了很多文章,多多少少都不尽如人意,要么只讲解了NestedScrollingParent的用法,没有讲到NestedScrollingChild,这样导致顶部的TopView不能滑动;要么TopView简单实现了NestedScrollingChild,TopView和RecyclerView联动性时,始终是没有RecyclerView丝滑,打不过就加入,所以就去扒了RecyclerView的源码,看看人家到底是怎么做到如此丝滑的。。。

不足

很多临界点的处理暂时没有做,某些速度会导致Fling传导到RecyclerView时,造成RecyclerView推迟滑动等等问题,本篇重点处理了NestedScrollingChild滑动不顺的问题,首要是参考了RecyclerView的做法,不足之处待后续优化。。。

查阅

RecyclerView要害代码如下:


@Override
public boolean onTouchEvent(MotionEvent e) {
    //获取速度跟踪器
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    //给事件设置嵌套偏移量,这个设置还挺要害的,决定了正常的滑动速度
    //还请有研究的大佬告知小弟其详细原理
    final MotionEvent vtev = MotionEvent.obtain(e);
    vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            //和NestedScrollingParent交互信息
           startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
        } break;
        case MotionEvent.ACTION_MOVE: {
            //滑动前,先让父布局滑动
            dispatchNestedPreScroll
            //如果有剩下未消费,则交给RecyclerView内部来消化,即RecyclerView自己滑动
            scrollByInternal
        } break;
        case MotionEvent.ACTION_UP: {
            //UP里首要处理fling事件,也是本篇的要害
            //将事件交给速度跟踪器,并核算当时的速度
            mVelocityTracker.addMovement(vtev);
            mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
            //执行fling,交给了ViewFlinger,是一个Runnable
            //内部run中的逻辑和MOVE中类似,先询问父布局,有剩下再自己处理
            //经过不断提交Runnable,使fling继续下去,当然速度必定越来越小
            fling(...)
        } break;
    }
    ...
}

详细实现

NSParentLayout2
public class NSParentLayout2 extends LinearLayout implements NestedScrollingParent2 {
    private static final String TAG = NSParentLayout2.class.getSimpleName();
    private View topView;
    private View bottomView;
    private int topViewHeight;
    private final NestedScrollingParentHelper mNestedScrollingParentHelper;
    private final LinearLayout.LayoutParams params;
    public NSParentLayout2(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
        setOrientation(VERTICAL);
        params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
    }
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        topView = findViewById(R.id.top_view);
        bottomView = findViewById(R.id.bottom_view);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        params.height = getMeasuredHeight();
        bottomView.setLayoutParams(params);
        topViewHeight = topView.getMeasuredHeight();
    }
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y >= topViewHeight) {
            y = topViewHeight;
        }
        super.scrollTo(x, y);
    }
    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        return true;
    }
    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type);
    }
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        //dy代表方向,getScrollY代表总的间隔原始方位的滑动间隔
        boolean TOPSHOW = dy > 0 && getScrollY() < topViewHeight;
        //canScrollVertically(-1)负值查看向上翻滚,正向查看向下翻滚
        boolean TOPHIDE = dy < 0 && getScrollY() >= 0 && !target.canScrollVertically(-1);
        if (TOPSHOW || TOPHIDE) {
            scrollBy(0, dy);
            consumed[1] = dy;
        }
    }
    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        if (target == topView) {
            bottomView.scrollBy(0, dyUnconsumed);
        }
    }
    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
        mNestedScrollingParentHelper.onStopNestedScroll(target, type);
    }
    @Override
    public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
        return false;
    }
    @Override
    public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }
}
NSChildLayout3
public class NSChildLayout3 extends LinearLayout implements NestedScrollingChild2, NestedScrollingChild3 {
    private int lastX = -1;
    private int lastY = -1;
    private final int[] consumed = new int[2];
    private final int[] offset = new int[2];
    private final int[] mNestedOffsets = new int[2];
    private int mScrollPointerId = INVALID_POINTER;
    private VelocityTracker vt;
    private int mMinFlingVelocity;
    private int mMaxFlingVelocity;
    private FlingTool flingTool;
    public NSChildLayout3(Context context) {
        super(context);
    }
    public NSChildLayout3(Context context, AttributeSet attrs) {
        super(context, attrs);
        //设置当时子View是否支撑嵌套滑动,如果是false,父View无法呼应嵌套滑动
        setNestedScrollingEnabled(true);
        setOrientation(VERTICAL);
        ViewConfiguration vc = ViewConfiguration.get(context);
        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();//默许50
        mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();//默许8000
        flingTool = new FlingTool(context);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //处理fling
        if (vt == null) {
            vt = VelocityTracker.obtain();
        }
        boolean eventAddedToVelocityTracker = false;
        final int action = event.getActionMasked();
        if (action == MotionEvent.ACTION_DOWN) {
            mNestedOffsets[0] = mNestedOffsets[1] = 0;
        }
        final MotionEvent vtev = MotionEvent.obtain(event);
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mScrollPointerId = event.getPointerId(0);
                lastX = (int) event.getRawX();
                lastY = (int) event.getRawY();
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, TYPE_TOUCH);
                break;
            case MotionEvent.ACTION_MOVE:
                int currentX = (int) event.getRawX();
                int currentY = (int) event.getRawY();
                int dx = lastX - currentX;
                int dy = lastY - currentY;
                if (dispatchNestedPreScroll(dx, dy, consumed, offset, TYPE_TOUCH)) {
                    dx -= consumed[0];
                    dy -= consumed[1];
                    // Updated the nested offsets
                    mNestedOffsets[0] += offset[0];
                    mNestedOffsets[1] += offset[1];
                }
                dispatchNestedScroll(0, 0, dx, dy, null, TYPE_TOUCH);
                lastX = currentX;
                lastY = currentY;
                break;
            case MotionEvent.ACTION_UP:
                vt.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                vt.computeCurrentVelocity(1000, mMaxFlingVelocity);
                //int xvt = (int) -vt.getXVelocity(mScrollPointerId);
                int yvt = (int) -vt.getYVelocity(mScrollPointerId);
                fling(0, yvt);
                reSetScroll();
                break;
            case MotionEvent.ACTION_CANCEL:
                reSetScroll();
                break;
        }
        if (!eventAddedToVelocityTracker) {
            vt.addMovement(vtev);
        }
        vtev.recycle();
        return true;
    }
    private void reSetScroll() {
        lastX = -1;
        lastY = -1;
        if (vt != null) {
            vt.clear();
        }
        stopNestedScroll(TYPE_TOUCH);
    }
    private void fling(int xvt, int yvt) {
        if (Math.abs(xvt) < mMinFlingVelocity) {
            xvt = 0;
        }
        if (Math.abs(yvt) < mMinFlingVelocity) {
            yvt = 0;
        }
        if (xvt == 0 && yvt == 0) {
            return;
        }
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
        xvt = Math.max(-mMaxFlingVelocity, Math.min(xvt, mMaxFlingVelocity));
        yvt = Math.max(-mMaxFlingVelocity, Math.min(yvt, mMaxFlingVelocity));
        flingTool.fling(xvt, yvt);
    }
    static final Interpolator sQuinticInterpolator = t -> {
        t -= 1.0f;
        return t * t * t * t * t + 1.0f;
    };
    private final int[] mReusableIntPair = new int[2];
    private class FlingTool implements Runnable {
        private int mLastFlingX;
        private int mLastFlingY;
        OverScroller mOverScroller;
        Interpolator mInterpolator = sQuinticInterpolator;
        // When set to true, postOnAnimation callbacks are delayed until the run method completes
        private boolean mEatRunOnAnimationRequest = false;
        // Tracks if postAnimationCallback should be re-attached when it is done
        private boolean mReSchedulePostAnimationCallback = false;
        FlingTool(Context context) {
            mOverScroller = new OverScroller(context, sQuinticInterpolator);
        }
        void fling(int xvt, int yvt) {
            mLastFlingX = mLastFlingY = 0;
            if (mInterpolator != sQuinticInterpolator) {
                mInterpolator = sQuinticInterpolator;
                mOverScroller = new OverScroller(getContext(), sQuinticInterpolator);
            }
            mOverScroller.fling(0, 0, xvt, yvt, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
            postOnAnimation();
        }
        void postOnAnimation() {
            if (mEatRunOnAnimationRequest) {
                mReSchedulePostAnimationCallback = true;
            } else {
                internalPostOnAnimation();
            }
        }
        private void internalPostOnAnimation() {
            removeCallbacks(this);
            ViewCompat.postOnAnimation(NSChildLayout3.this, this);
        }
        @Override
        public void run() {
            mReSchedulePostAnimationCallback = false;
            mEatRunOnAnimationRequest = true;
            final OverScroller scroller = mOverScroller;
            if (scroller.computeScrollOffset()) {
                final int x = scroller.getCurrX();
                final int y = scroller.getCurrY();
                int unconsumedX = x - mLastFlingX;
                int unconsumedY = y - mLastFlingY;
                mLastFlingX = x;
                mLastFlingY = y;
                int consumedX = 0;
                int consumedY = 0;
                // Nested Pre Scroll
                mReusableIntPair[0] = 0;
                mReusableIntPair[1] = 0;
                if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null,
                        TYPE_NON_TOUCH)) {
                    unconsumedX -= mReusableIntPair[0];
                    unconsumedY -= mReusableIntPair[1];
                }
                mReusableIntPair[0] = 0;
                mReusableIntPair[1] = 0;
                dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null,
                        TYPE_NON_TOUCH, mReusableIntPair);
                postOnAnimation();
            }
            mEatRunOnAnimationRequest = false;
            if (mReSchedulePostAnimationCallback) {
                internalPostOnAnimation();
            } else {
                stopNestedScroll(TYPE_NON_TOUCH);
            }
        }
    }
    // NestedScrollingChild
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }
    @Override
    public boolean isNestedScrollingEnabled() {
        return getScrollingChildHelper().isNestedScrollingEnabled();
    }
    @Override
    public boolean startNestedScroll(int axes, int type) {
        return getScrollingChildHelper().startNestedScroll(axes, type);
    }
    @Override
    public void stopNestedScroll(int type) {
        getScrollingChildHelper().stopNestedScroll(type);
    }
    @Override
    public boolean hasNestedScrollingParent(int type) {
        return getScrollingChildHelper().hasNestedScrollingParent(type);
    }
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, int[] offsetInWindow, int type) {
        return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow, type);
    }
    @Override
    public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) {
        getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
    }
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
                                           int type) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
                type);
    }
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
    }
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
    }
    private NestedScrollingChildHelper mScrollingChildHelper;
    private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mScrollingChildHelper == null) {
            mScrollingChildHelper = new NestedScrollingChildHelper(this);
        }
        return mScrollingChildHelper;
    }
}

结束

如果有其他成熟的方案,还请各位大佬引荐给我学习下,感谢!

参考文章

/post/684490…
/post/684490…
/post/684490…