很多应用的首页都会有一些嵌套滚动、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 嵌套