NestedScrolling嵌套滑动基础版

753 阅读5分钟

代码详情可见:
github.com/stewForAni/…

顺便给练习项目求个Star🌟🌟🌟,万分感谢🙏🙏🙏:
github.com/stewForAni/…

WechatIMG382.jpeg


效果图

WechatIMG516.jpg

背景

一直想抽时间巩固一下嵌套滑动的知识点,尤其是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;
    }
}

结尾

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

参考文章

juejin.cn/post/684490…
juejin.cn/post/684490…
juejin.cn/post/684490…