仿小红书嵌套滑动效果

1,850 阅读3分钟

小红书效果

分析

  • 思路一:可以通过自定义view来实现类似 (比较麻烦,参考这里
  • 思路二:借助 NestScrolling 机制实现 (比较简单,关于嵌套滑动概述参考这里

下面就用思路二来分析一下如何实现

滑动效果分析

  • recyclerView 上滑时,如果手指没有滑过 recyclerView 的顶部,那么 recyclerView 自己消费滑动事件;否则开始嵌套滑动(整体上滑)。当手指抬起时候如果滑动距离大于 50dp ,通过惯性滑动滑到顶部,否则滑回到初始位置
  • recyclerView 下滑时,如果recyclerView的第一个 item 不是完全可见,那么 recyclerView 自己消费滑动事件;否则开始嵌套滑动(整体下滑)。当手指抬起时候如果滑动距离大于 50dp ,通过惯性滑动滑到底部,否则滑回到初始位置

实现

NestedRecyclerView(NestScrollChild)

public class NestedRecyclerView extends RecyclerView {


    private float topPadding = Utils.dp2Px(50);
    private float scrollSlop = Utils.dp2Px(50); //滑动超过这个距离松手后自动自动滑动到顶部
    private float downY;
    private float moveY;
    private float deltaMoveY;
    private float deltaDownMoveY;
    private float lastMoveY;
    public OverScroller mScroller;
    private int firstCompletelyVisiblePosition;

    public NestedRecyclerView(@NonNull Context context) {
        this(context, null);
    }

    public NestedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    private void init(Context context) {
        mScroller = new OverScroller(context);
    }


    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("NestedRecyclerView", "ACTION_DOWN");
                downY = event.getRawY();
                lastMoveY = event.getRawY();
                break;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("NestedRecyclerView", "ACTION_DOWN");
                downY = event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                moveY = event.getRawY();

                deltaMoveY = moveY - lastMoveY;
                deltaDownMoveY = moveY - downY;

                //向上滑动 手指滑动到 recyclerview 顶部或者以上  触发嵌套滑动
                //event.getY() <= 0 且 getTop() - ((View) getParent()).getScrollY() > topPadding
                if (deltaMoveY < 0) {
                    if (event.getY() <= 0) {

                        if (getTop() - ((View) getParent()).getScrollY() > topPadding) {
                            setNestedScrollingEnabled(true);
                            startNestedScroll(SCROLL_AXIS_VERTICAL);
                        } else {
                            setNestedScrollingEnabled(false);
                        }
                    } else {
                        setNestedScrollingEnabled(false);
                    }
                } else {
                    //向下滑动
                    //先向下滑动 recyclerview,等待 recyclerview 全部展现后触发嵌套滑动
                    GridLayoutManager layoutManager = ((GridLayoutManager) getLayoutManager());
                    firstCompletelyVisiblePosition = layoutManager.findFirstCompletelyVisibleItemPosition();
                    if (firstCompletelyVisiblePosition == 0 && ((View) getParent()).getScrollY() > 0) {
                        setNestedScrollingEnabled(true);
                        startNestedScroll(SCROLL_AXIS_VERTICAL);
                    } else {
                        setNestedScrollingEnabled(false);
                    }
                }

                lastMoveY = moveY;
                break;
            case MotionEvent.ACTION_UP:
                int parentScrollY = ((View) getParent()).getScrollY();
                int startY = parentScrollY;
                int dy;
                if (deltaDownMoveY < 0) {
                    //向上滑动切滑动距离超过 scrollSlop 滑动到顶部
                    //向上滑动切滑动距离不超过 scrollSlop 滑回到底部

                    if (parentScrollY >= scrollSlop) {
                        dy = (int) (getTop() - ((View) getParent()).getScrollY() - topPadding);
                    } else {
                        dy = -parentScrollY;
                    }
                } else {
                    //向下滑动切滑动距离超过 scrollSlop 滑动到底部
                    //向下滑动切滑动距离不超过 scrollSlop 滑回到顶部
                    if (getTop() - ((View) getParent()).getScrollY() > scrollSlop + topPadding) {
                        dy = -parentScrollY;
                    } else {
                        dy = (int) (getTop() - ((View) getParent()).getScrollY() - topPadding);
                    }
                }
                mScroller.startScroll(0, startY, 0
                        , dy, 300);
                ((View) getParent()).invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

    public OverScroller getScroller() {
        return mScroller;
    }
}

由于RecyclerView默认实现了NestedScrollingChild2,不需要我们自己实现NestedScrollingChild。

重写RecyclerView的onTouchEvent方法,在ACTION_MOVE里面合适的时候开启/禁止弹性滑动,具体逻辑参考前面流程图和代码注释。

在ACTION_UP的时候通过 Scroller 实现弹性滑动的效果,关键是计算出 dy,代码注释里面写的很清楚。

关于 Scroller 的使用参考这里

LinearLayoutNestScrollParent(NestScrollParent)

public class LinearLayoutNestScrollParent extends LinearLayout implements NestedScrollingParent {
    private NestedScrollingParentHelper mParentHelper;
    private View imgView;
    private NestedRecyclerView recyclerview;

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

    public LinearLayoutNestScrollParent(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mParentHelper = new NestedScrollingParentHelper(this);
    }


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        imgView = findViewById(R.id.img);
        recyclerview = findViewById(R.id.rv_photos);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int imgHeight = imgView.getMeasuredHeight();
        //重新测量高度
        int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() + imgHeight, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, newHeightMeasureSpec);
    }


    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        if (target instanceof RecyclerView) {
            return true;
        }
        return false;
    }


    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(View target) {
        mParentHelper.onStopNestedScroll(target);
    }

    //先于 child 滚动
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        scrollBy(0, dy);//滚动
        consumed[1] = dy;//告诉child我消费了多少
    }

    //后于child滚动
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }

    //返回值:是否消费了fling
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    //返回值:是否消费了fling
    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        return mParentHelper.getNestedScrollAxes();
    }


    @Override
    public void computeScroll() {
        OverScroller scroller = recyclerview.getScroller();
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            invalidate();
        }
    }
}

实现NestedScrollingParent接口,只要实现onNestedScroll这个方法,全部消费 NestedScrollingChild传来的 dy 。

另外需要注意的onMeasure方法,重新测量高度,否则上滑时候RecyclerView下面部分会出现空白。

布局文件

<?xml version="1.0" encoding="utf-8"?>
<cn.feng.xhsimageview.views.LinearLayoutNestScrollParent xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <cn.feng.xhsimageview.views.NestedRecyclerView
        android:id="@+id/rv_photos"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginTop="2dp"
        android:layout_weight="1" />

</cn.feng.xhsimageview.views.LinearLayoutNestScrollParent>