Android Tab吸顶 嵌套滚动通用实现方案✅

很多应用的首页都会有一些嵌套滚动、Tab吸顶的布局,尤其是一些生鲜类应用,例如 朴朴超市、大润发优鲜、盒马等等。

在 Android 里面,滚动吸顶方式通常可以通过 CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout + NestedScrollView 来实现,但是 AppBarLayoutBehavior fling 无法传递到 NestedScrollView,快速来回滑动偶尔也会有些抖动,导致滚动不流畅。

另外对于头部是一些动态列表的,还是更希望通过 RecyclerView 来实现,那么嵌套的方式变为:RecyclerView + ViewPager + RecyclerView,那么就需要处理好 RecyclerView 的滑动冲突问题。

如果 ViewPager 的 RecyclerView 内部还嵌套一层 ViewPager,例如一些广告Banner图,那么事件处理也会更加复杂。本文将介绍一种通用的嵌套滚动方案,既可以实现Tab的吸顶,又可以单纯实现的两个垂直 RecyclerView 嵌套(主要场景是:尾部的recyclerview可以实现容器级别的复用,例如往多个列表页的尾部嵌套一个相同样式的推荐商品列表,如下图所示)。

代码库地址:github.com/smuyyh/Nest...

目前已应用到线上,如有一些好的建议欢迎交流交流呀~~

核心思路:

  • 父容器滑动到底部之后,触摸事件继续交给子容器滑动
  • 子容器滚动到顶部之后,触摸事件继续交给父容器滑动
  • fling 在父容器和子容器之间传递
  • Tab 在屏幕中间,切换 ViewPager 之后,如果子容器不在顶部,需要优先处理滑动

代码实现:

ParentRecyclerView

因为触摸事件首先分发到父容器,所以核心的协调逻辑主要由父容器实现,子容器只需要处理 fling 传递即可。

java 复制代码
public class ParentRecyclerView extends RecyclerView {

    private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();

    /**
     * fling时的加速度
     */
    private int mVelocity = 0;

    private float mLastTouchY = 0f;

    private int mLastInterceptX;
    private int mLastInterceptY;

    /**
     * 用于向子容器传递 fling 速度
     */
    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
    private int mMaximumFlingVelocity;
    private int mMinimumFlingVelocity;

    /**
     * 子容器是否消耗了滑动事件
     */
    private boolean childConsumeTouch = false;
    /**
     * 子容器消耗的滑动距离
     */
    private int childConsumeDistance = 0;

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

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

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

    private void init() {
        ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
        mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();

        addOnScrollListener(new OnScrollListener() {
            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == SCROLL_STATE_IDLE) {
                    dispatchChildFling();
                }
            }
        });
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mVelocity = 0;
                mLastTouchY = ev.getRawY();
                childConsumeTouch = false;
                childConsumeDistance = 0;

                ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
                if (isScrollToBottom() && (childRecyclerView != null && !childRecyclerView.isScrollToTop())) {
                    stopScroll();
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                childConsumeTouch = false;
                childConsumeDistance = 0;
                break;
            default:
                break;
        }

        try {
            return super.dispatchTouchEvent(ev);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (isChildConsumeTouch(event)) {
            // 子容器如果消费了触摸事件,后续父容器就无法再拦截事件
            // 在必要的时候,子容器需调用 requestDisallowInterceptTouchEvent(false) 来允许父容器继续拦截事件
            return false;
        }
        // 子容器不消费触摸事件,父容器按正常流程处理
        return super.onInterceptTouchEvent(event);
    }

    /**
     * 子容器是否消费触摸事件
     */
    private boolean isChildConsumeTouch(MotionEvent event) {
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        if (event.getAction() != MotionEvent.ACTION_MOVE) {
            mLastInterceptX = x;
            mLastInterceptY = y;
            return false;
        }
        int deltaX = x - mLastInterceptX;
        int deltaY = y - mLastInterceptY;
        if (Math.abs(deltaX) > Math.abs(deltaY) || Math.abs(deltaY) <= mTouchSlop) {
            return false;
        }

        return shouldChildScroll(deltaY);
    }

    /**
     * 子容器是否需要消费滚动事件
     */
    private boolean shouldChildScroll(int deltaY) {
        ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
        if (childRecyclerView == null) {
            return false;
        }
        if (isScrollToBottom()) {
            // 父容器已经滚动到底部 且 向下滑动 且 子容器还没滚动到底部
            return deltaY < 0 && !childRecyclerView.isScrollToBottom();
        } else {
            // 父容器还没滚动到底部 且 向上滑动 且 子容器已经滚动到顶部
            return deltaY > 0 && !childRecyclerView.isScrollToTop();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        if (isScrollToBottom()) {
            // 如果父容器已经滚动到底部,且向上滑动,且子容器还没滚动到顶部,事件传递给子容器
            ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
            if (childRecyclerView != null) {
                int deltaY = (int) (mLastTouchY - e.getRawY());
                if (deltaY >= 0 || !childRecyclerView.isScrollToTop()) {
                    mVelocityTracker.addMovement(e);
                    if (e.getAction() == MotionEvent.ACTION_UP) {
                        // 传递剩余 fling 速度
                        mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                        float velocityY = mVelocityTracker.getYVelocity();
                        if (Math.abs(velocityY) > mMinimumFlingVelocity) {
                            childRecyclerView.fling(0, -(int) velocityY);
                        }
                        mVelocityTracker.clear();
                    } else {
                        // 传递滑动事件
                        childRecyclerView.scrollBy(0, deltaY);
                    }

                    childConsumeDistance += deltaY;
                    mLastTouchY = e.getRawY();
                    childConsumeTouch = true;
                    return true;
                }
            }
        }

        mLastTouchY = e.getRawY();

        if (childConsumeTouch) {
            // 在同一个事件序列中,子容器消耗了部分滑动距离,需要扣除掉
            MotionEvent adjustedEvent = MotionEvent.obtain(
                    e.getDownTime(),
                    e.getEventTime(),
                    e.getAction(),
                    e.getX(),
                    e.getY() + childConsumeDistance, // 更新Y坐标
                    e.getMetaState()
            );

            boolean handled = super.onTouchEvent(adjustedEvent);
            adjustedEvent.recycle();
            return handled;
        }

        if (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL) {
            mVelocityTracker.clear();
        }

        try {
            return super.onTouchEvent(e);
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }

    @Override
    public boolean fling(int velX, int velY) {
        boolean fling = super.fling(velX, velY);
        if (!fling || velY <= 0) {
            mVelocity = 0;
        } else {
            mVelocity = velY;
        }
        return fling;
    }

    private void dispatchChildFling() {
        // 父容器滚动到底部后,如果还有剩余加速度,传递给子容器
        if (isScrollToBottom() && mVelocity != 0) {
            // 尽量让速度传递更加平滑
            float mVelocity = NestedOverScroller.invokeCurrentVelocity(this);
            if (Math.abs(mVelocity) <= 2.0E-5F) {
                mVelocity = (float) this.mVelocity * 0.5F;
            } else {
                mVelocity *= 0.46F;
            }
            ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
            if (childRecyclerView != null) {
                childRecyclerView.fling(0, (int) mVelocity);
            }
        }
        mVelocity = 0;
    }

    public ChildRecyclerView findNestedScrollingChildRecyclerView() {
        if (getAdapter() instanceof INestedParentAdapter) {
            return ((INestedParentAdapter) getAdapter()).getCurrentChildRecyclerView();
        }
        return null;
    }

    public boolean isScrollToBottom() {
        return !canScrollVertically(1);
    }

    public boolean isScrollToTop() {
        return !canScrollVertically(-1);
    }

    @Override
    public void scrollToPosition(final int position) {
        if (position == 0) {
            // 父容器滚动到顶部,从交互上来说子容器也需要滚动到顶部
            ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
            if (childRecyclerView != null) {
                childRecyclerView.scrollToPosition(0);
            }
        }

        super.scrollToPosition(position);
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return target instanceof ChildRecyclerView;
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
        //1、当前ParentRecyclerView没有滑动底,且dy> 0,即下滑
        boolean isParentCanScroll = dy > 0 && !isScrollToBottom();
        //2、当前ChildRecyclerView滑到顶部了,且dy < 0,即上滑
        boolean isChildCanNotScroll = dy < 0 && (childRecyclerView == null || childRecyclerView.isScrollToTop());
        if (isParentCanScroll || isChildCanNotScroll) {
            smoothScrollBy(0, dy);
            consumed[1] = dy;
        }
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return true;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
        // 1、当前ParentRecyclerView没有滑动底,且向下滑动,即下滑
        boolean isParentCanFling = velocityY > 0f && !isScrollToBottom();
        // 2、当前ChildRecyclerView滑到顶部了,且向上滑动,即上滑
        boolean isChildCanNotFling = velocityY < 0 && (childRecyclerView == null || childRecyclerView.isScrollToTop());

        if (!isParentCanFling && !isChildCanNotFling) {
            return false;
        }
        fling(0, (int) velocityY);
        return true;
    }
}

ChildRecyclerView

子容器主要处理 fling 传递,以及滑动到顶部时,允许父容器继续拦截事件。

java 复制代码
public class ChildRecyclerView extends RecyclerView {

    private ParentRecyclerView mParentRecyclerView = null;

    /**
     * fling时的加速度
     */
    private int mVelocity = 0;

    private int mLastInterceptX;

    private int mLastInterceptY;

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

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

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

    private void init() {
        setOverScrollMode(OVER_SCROLL_NEVER);

        addOnScrollListener(new OnScrollListener() {
            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == SCROLL_STATE_IDLE) {
                    dispatchParentFling();
                }
            }
        });
    }

    private void dispatchParentFling() {
        ensureParentRecyclerView();
        // 子容器滚动到顶部,如果还有剩余加速度,就交给父容器处理
        if (mParentRecyclerView != null && isScrollToTop() && mVelocity != 0) {
            // 尽量让速度传递更加平滑
            float velocityY = NestedOverScroller.invokeCurrentVelocity(this);
            if (Math.abs(velocityY) <= 2.0E-5F) {
                velocityY = (float) this.mVelocity * 0.5F;
            } else {
                velocityY *= 0.65F;
            }
            mParentRecyclerView.fling(0, (int) velocityY);
            mVelocity = 0;
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            mVelocity = 0;
        }

        int x = (int) ev.getRawX();
        int y = (int) ev.getRawY();
        if (ev.getAction() != MotionEvent.ACTION_MOVE) {
            mLastInterceptX = x;
            mLastInterceptY = y;
        }

        int deltaX = x - mLastInterceptX;
        int deltaY = y - mLastInterceptY;

        if (isScrollToTop() && Math.abs(deltaX) <= Math.abs(deltaY) && getParent() != null) {
            // 子容器滚动到顶部,继续向上滑动,此时父容器需要继续拦截事件。与父容器 onInterceptTouchEvent 对应
            getParent().requestDisallowInterceptTouchEvent(false);
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean fling(int velocityX, int velocityY) {
        if (!isAttachedToWindow()) return false;
        boolean fling = super.fling(velocityX, velocityY);
        if (!fling || velocityY >= 0) {
            mVelocity = 0;
        } else {
            mVelocity = velocityY;
        }
        return fling;
    }

    public boolean isScrollToTop() {
        return !canScrollVertically(-1);
    }

    public boolean isScrollToBottom() {
        return !canScrollVertically(1);
    }

    private void ensureParentRecyclerView() {
        if (mParentRecyclerView == null) {
            ViewParent parentView = getParent();
            while (!(parentView instanceof ParentRecyclerView)) {
                parentView = parentView.getParent();
            }
            mParentRecyclerView = (ParentRecyclerView) parentView;
        }
    }
}

效果

有 Tab

无 Tab,两个 RecyclerView 嵌套

相关推荐
Themberfue8 分钟前
基础算法之双指针--Java实现(下)--LeetCode题解:有效三角形的个数-查找总价格为目标值的两个商品-三数之和-四数之和
java·开发语言·学习·算法·leetcode·双指针
深山夕照深秋雨mo16 分钟前
在Java中操作Redis
java·开发语言·redis
努力的布布22 分钟前
SpringMVC源码-AbstractHandlerMethodMapping处理器映射器将@Controller修饰类方法存储到处理器映射器
java·后端·spring
xujinwei_gingko22 分钟前
Spring MVC 常用注解
java·spring·mvc
PacosonSWJTU27 分钟前
spring揭秘25-springmvc03-其他组件(文件上传+拦截器+处理器适配器+异常统一处理)
java·后端·springmvc
PacosonSWJTU29 分钟前
spring揭秘26-springmvc06-springmvc注解驱动的web应用
java·spring·springmvc
原野心存1 小时前
java基础进阶——继承、多态、异常捕获(2)
java·java基础知识·java代码审计
进阶的架构师1 小时前
互联网Java工程师面试题及答案整理(2024年最新版)
java·开发语言
黄俊懿1 小时前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构
木子02041 小时前
java高并发场景RabbitMQ的使用
java·开发语言