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