前言
最近产品提了个需求,要把商品列表做成类似淘宝的样式
淘宝
一般遇到这种需求,我们首先会想到的是,拦截TouchEvent,然后自己来处理滑动,这种方法虽然行得通,但是代码写起来非常恶心,且滑动冲突会比较多,使用NestedScrolling API会简单优雅很多。
先上效果图
Touch嵌套
fling嵌套
API分析
NestedScrollingParent
Parent接口共有以下几个方法
public interface NestedScrollingParent { //当子View开始滑动时,会触发这个方法,判断接下来是否进行嵌套滑动, //返回false,则表示不使用嵌套滑动 boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes); //onStartNestedScroll如果返回true,那么接下来就会调用这个方法,用来做一些初始化操作,一般可以忽略 void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes); //嵌套滑动结束时会触发这个方法 void onStopNestedScroll(@NonNull View target); //子View滑动时会触发这个方法,dyConsumed代表子View滑动的距离,dyUnconsumed代表子View本次滑动未消耗的距离,比如RecyclerView滑到了边界,那么会有一部分y未消耗掉 void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed); //子View开始滑动时,会触发这个回调,dy表示滑动的y距离,consumed数组代表父View要消耗的距离,假如consumed[1] = dy,那么子View就不会滑动了 void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed); //当子View fling时,会触发这个回调,consumed代表速度是否被子View消耗掉,比如RecyclerView滑动到了边界,那么它显然没法消耗本次的fling boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed); //当子View要开始fling时,会先询问父View是否要拦截本次fling,返回true表示要拦截,那么子View就不会惯性滑动了 boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY); //表示目前正在进行的嵌套滑动的方向,值有ViewCompat.SCROLL_AXIS_HORIZONTAL 或者ViewCompat.SCROLL_AXIS_VERTICAL或者SCROLL_AXIS_NONE @ScrollAxis int getNestedScrollAxes(); }
NestedScrollingChild
public interface NestedScrollingChild { //设置当前子View是否支持嵌套滑动 void setNestedScrollingEnabled(boolean enabled); //当前子View是否支持嵌套滑动 boolean isNestedScrollingEnabled(); //开始嵌套滑动,对应Parent的onStartNestedScroll boolean startNestedScroll(@ScrollAxis int axes); //停止本次嵌套滑动,对应Parent的onStopNestedScroll void stopNestedScroll(); //true表示这个子View有一个支持嵌套滑动的父View boolean hasNestedScrollingParent(); //通知父View子View开始滑动了,对应父View的onNestedScroll方法 boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow); //通知父View即将开始滑动了,对应父View的onNestedPreScroll方法 boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow); //通知父View开始Fling了,对应Parent的onNestedFling方法 boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); //通知父View要开始fling了,对应Parent的onNestedPreFling方法 boolean dispatchNestedPreFling(float velocityX, float velocityY); }
整体流程描述如下(以RecyclerView为例):
child.ACTION_DOWN
-> child.startNestedScroll
-> parent.onStartNestedScroll (如果返回false,则流程终止)
-> parent.onNestedScrollAccepted
-> child.ACTION_MOVE
-> child.dispatchNestedPreScroll
-> parent.onNestedPreScroll
-> child.ACTION_UP
-> chid.stopNestedScroll
-> parent.onStopNestedScroll
-> child.fling
-> child.dispatchNestedPreFling
-> parent.onNestedPreScroll
-> child.dispatchNestedFling
-> parent.onNestedFling
有兴趣的朋友可以直接查看 RecyclerView 的源码
子View向上传递事件时,是循环向上的,即 Parent 不需要是 Child 的直接 ViewParent,具体可以看代码,以startNestedScroll为例
public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = getParent(); View child = this; while (p != null) { try { if (p.onStartNestedScroll(child, this, axes)) { mNestedScrollingParent = p; p.onNestedScrollAccepted(child, this, axes); return true; } } catch (AbstractMethodError e) { Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " + "method onStartNestedScroll", e); // Allow the search upward to continue } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
具体实现
页面结构
页面结构
事件拦截
RV 嵌套 RV 时,内层 RV 是无法滑动的,然而,当外层RV在Fling时,如果我们触摸到子RV,那么会有一定概率导致子RV接收到Touch事件并开始滚动,所以我们需要同时拦截内层和外层的RV的事件。大概思路如下:
当向下滑动时,判断TabLayout是否置顶,如果未置顶,则滑动外层RV;如果TabLayout已经置顶,则滑动子RV
当向上滑动时,判断TabLayout是否置顶,如果未置顶,则滑动外层RV;如果TabLayout已经置顶,则判断子RV能否向上滑动,如果可以,则滑动子RV,否则滑动外层RV
具体处理为,我们在外层RV之上嵌套一层自定义的FrameLayout,并开启外层RV和内层RV的嵌套滑动功能,那么我们就能在FrameLayout中接收到RV传递上来的scroll和fling事件
滚动处理
public class NestedScrollLayout extends FrameLayout { private View mChildView; /** * 最外层的RecyclerView */ private RecyclerView mRootList; /** * 子RecyclerView */ private RecyclerView mChildList; @Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes) { //这里表示只有在纵向滑动时,我们才拦截事件 return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL; } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) { stopScroller(); //mChildView表示TabLayout和ViewPager的父View,比如说我们用一个LinearLayout包裹住TabLayout和ViewPager if (mChildView == null) { return; } if (target == mRootList) { onParentScrolling(mChildView.getTop(), dy, consumed); } else { onChildScrolling(mChildView.getTop(), dy, consumed); } } /** * 父列表在滑动 * * @param childTop * @param dy * @param consumed */ private void onParentScrolling(int childTop, int dy, int[] consumed) { //列表已经置顶 if (childTop == 0) { if (dy > 0 && mChildList != null) { //还在向下滑动,此时滑动子列表 mChildList.scrollBy(0, dy); consumed[1] = dy; } else { if (mChildList != null && mChildList.canScrollVertically(dy)) { consumed[1] = dy; mChildList.scrollBy(0, dy); } } } else { if (childTop < dy) { consumed[1] = dy - childTop; } } } private void onChildScrolling(int childTop, int dy, int[] consumed) { if (childTop == 0) { if (dy < 0) { //向上滑动 if (!mChildList.canScrollVertically(dy)) { consumed[1] = dy; mRootList.scrollBy(0, dy); } } } else { if (dy < 0 || childTop > dy) { consumed[1] = dy; mRootList.scrollBy(0, dy); } else { //dy大于0 consumed[1] = dy; mRootList.scrollBy(0, childTop); } } } /** * 表示我们只接收纵向的事件 * @return */ @Override public int getNestedScrollAxes() { return ViewCompat.SCROLL_AXIS_VERTICAL; } }
ViewGroup默认实现了Parent接口,这里我们不需要再implement一次
Fling处理
当列表开始 Fling 时,我们将会接收到相应的回调,这里我们需要自己处理惯性滑动,使用 OverScroller 来替我们模拟Fling
public class NestedScrollLayout extends FrameLayout { /** * 用来处理Fling */ private OverScroller mScroller; private int mLastY; @Override public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) { return false; } @Override public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) { mLastY = 0; this.mScroller.fling(0, 0, (int) velocityX, (int) velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); invalidate(); return true; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { int currY = mScroller.getCurrY(); int dy = currY - mLastY; mLastY = currY; if (dy != 0) { onFling(dy); } invalidate(); } super.computeScroll(); } private void onFling(int dy) { if (mChildView != null) { //子列表有显示 int top = mChildView.getTop(); if (top == 0) { if (dy > 0) { if (mChildList != null && mChildList.canScrollVertically(dy)) { mChildList.scrollBy(0, dy); } else { stopScroller(); } } else { if (mChildList != null && mChildList.canScrollVertically(dy)) { mChildList.scrollBy(0, dy); } else { mRootList.scrollBy(0, dy); } } } else { if (dy > 0) { if (top > dy) { mRootList.scrollBy(0, dy); } else { mRootList.scrollBy(0, top); } } else { if (mRootList.canScrollVertically(dy)) { mRootList.scrollBy(0, dy); } else { stopScroller(); } } } } else { if (!mRootList.canScrollVertically(dy)) { stopScroller(); } else { mRootList.scrollBy(0, dy); } } } }
到这里为止,我们要的效果已经实现了,mChildView 和子RV何时赋值,参考Demo即可。
新版API
你以为这样就完了?
快闪开,我要开始装逼了
谷歌在 26.1.0 的 support 包中加入了两个新的 API
这两个接口各自继承了NestedScrollingParent和NestedScrollingChild
public interface NestedScrollingParent2 extends NestedScrollingParent { boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type); void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type); void onStopNestedScroll(@NonNull View target, @NestedScrollType int type); void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type); void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type); }
public interface NestedScrollingChild2 extends NestedScrollingChild { boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type); void stopNestedScroll(@NestedScrollType int type); boolean hasNestedScrollingParent(@NestedScrollType int type); boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type); boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type); }
在新的API中去掉了 fling 回调,并且增加了 type 参数,type分为两种
//表示当前事件是由用户手指触摸产生的 public static final int TYPE_TOUCH = 0; //表示当前事件不是用户手指触摸产生的,一般是fling public static final int TYPE_NON_TOUCH = 1;
Parent2具体流程如下:
child.ACTION_DOWN
-> child.startNestedScroll (TYPE_TOUCH)
-> parent.onStartNestedScroll (TYPE_TOUCH) (如果返回false,则流程终止)
-> parent.onNestedScrollAccepted (TYPE_TOUCH)
-> child.ACTION_MOVE
-> child.dispatchNestedPreScroll (TYPE_TOUCH)
-> parent.onNestedPreScroll (TYPE_TOUCH)
-> child.ACTION_UP
-> chid.stopNestedScroll (TYPE_TOUCH)
-> parent.onStopNestedScroll (TYPE_TOUCH)
-> child.fling
-> child.startNestedScroll (TYPE_NON_TOUCH)
-> parent.onStartNestedScroll (TYPE_NON_TOUCH) (如果返回false,则流程终止)
-> parent.onNestedScrollAccepted (TYPE_NON_TOUCH)
-> child.dispatchNestedPreScroll (TYPE_NON_TOUCH)
-> parent.onNestedPreScroll (TYPE_NON_TOUCH)
-> child.dispatchNestedScroll (TYPE_NON_TOUCH)
-> parent.onNestedScroll (TYPE_NON_TOUCH)
-> child.stopNestedScroll (TYPE_NON_TOUCH)
-> parent.onStopNestedScroll (TYPE_NON_TOUCH)
如上所示,当 RV 开始 Fling 时,每一帧 Fling 的距离,都会通知到 Parent2,由 Parent2 判断是否拦截处理,那么我们就不需要自己使用 OverScroller 来模拟惯性滑动了,代码可以更少。具体实现如下:
public class NestedScrollLayout2 extends FrameLayout implements NestedScrollingParent2 { private View mChildView; /** * 最外层的RecyclerView */ private RecyclerView mRootList; /** * 子RecyclerView */ private RecyclerView mChildList; private NestedViewModel mScrollViewModel; private int mAxes; public NestedScrollLayout2(@NonNull Context context) { super(context); } public NestedScrollLayout2(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public void setTarget(LifecycleOwner target) { if (target instanceof FragmentActivity) { mScrollViewModel = ViewModelProviders.of((FragmentActivity) target).get(NestedViewModel.class); } else if (target instanceof Fragment) { mScrollViewModel = ViewModelProviders.of((Fragment) target).get(NestedViewModel.class); } else { throw new IllegalArgumentException("target must be FragmentActivity or Fragment"); } mScrollViewModel.getChildView().observe(target, new Observer<View>() { @Override public void onChanged(@Nullable View view) { mChildView = view; } }); mScrollViewModel.getChildList().observe(target, new Observer<View>() { @Override public void onChanged(@Nullable View view) { mChildList = (RecyclerView) view; } }); } public void setRootList(RecyclerView recyclerView) { mRootList = recyclerView; } @Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) { return axes == ViewCompat.SCROLL_AXIS_VERTICAL; } @Override public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) { mAxes = axes; } @Override public void onStopNestedScroll(@NonNull View target, int type) { mAxes = SCROLL_AXIS_NONE; } @Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { if (mChildView == null) { return; } if (target == mRootList) { onParentScrolling(mChildView.getTop(), dy, consumed); } else { onChildScrolling(mChildView.getTop(), dy, consumed); } } /** * 父列表在滑动 * * @param childTop * @param dy * @param consumed */ private void onParentScrolling(int childTop, int dy, int[] consumed) { //列表已经置顶 if (childTop == 0) { if (dy > 0 && mChildList != null) { //还在向下滑动,此时滑动子列表 mChildList.scrollBy(0, dy); consumed[1] = dy; } else { if (mChildList != null && mChildList.canScrollVertically(dy)) { consumed[1] = dy; mChildList.scrollBy(0, dy); } } } else { if (childTop < dy) { consumed[1] = dy - childTop; } } } private void onChildScrolling(int childTop, int dy, int[] consumed) { if (childTop == 0) { if (dy < 0) { //向上滑动 if (!mChildList.canScrollVertically(dy)) { consumed[1] = dy; mRootList.scrollBy(0, dy); } } } else { if (dy < 0 || childTop > dy) { consumed[1] = dy; mRootList.scrollBy(0, dy); } else { //dy大于0 consumed[1] = dy; mRootList.scrollBy(0, childTop); } } } @Override public int getNestedScrollAxes() { return mAxes; } }
有人可能会问,既然有新 API,为啥还要用 OverScroller。
作者:Mr_villain
链接:https://www.jianshu.com/p/20efb9f65494
共同学习,写下你的评论
评论加载中...
作者其他优质文章