我们先来看一下动图,直观的感受下什么是嵌套滚动?
嵌套就说明是一层套着一层,存在两个滑动行为。上图中,当我们滑动下面红色的UI控件时,先滑动的却是其父容器,当父容器滑动到一定程度后,下面红色的UI控件才开始滑动。那么嵌套滑动机制比ViewGroup的事件分发机制有什么优越之处呢?
假设我们按照传统的事件分发去理解,首先我们滑动的是下面的红色的UI控件,而移动却是其父容器,所以按照传统的方式,肯定是父容器拦截了内部的红色的UI控件的事件。但是,当父容器滑动到一定程度时,红色的UI控件又开始滑动了,中间整个过程是没有间断的。从正常的事件分发(不手动调用分发事件,不手动去发出事件)角度去做是不可能的,因为当父容拦截之后,是没有办法再把事件交给红色的UI控件的。事件分发,只要拦截了,当前手势接下来的事件都会交给父容器(拦截者)来处理。
因此按照事件分发机制是很难达到上面的效果的,要实现上面的效果我们就要使用嵌套滑动机制。
父容器要实现NestedScrollingParent接口。
当子视图调用 startNestedScroll(View, int) 后调用该方法。实现这个方法来声明支持嵌套滚动,如果返回 true,那么这个视图将要配合子视图嵌套滚动。当嵌套滚动结束时会调用到 onStopNestedScroll(View)。nestedScrollAxes表示滑动的方向,水平或者竖直,通过该参数选择仅支持水平滑动或者竖直滑动。
如果 onStartNestedScroll 返回 true ,然后执行该方法,这个方法里可以做一些初始化。
子视图开始滚动前会调用这个方法。这时候父布局(也就是当前的 NestedScrollingParent 的实现类)可以通过这个方法来配合子视图同时处理滚动事件。
target 滚动的子视图
dx 绝对值为手指在x方向滚动的距离,dx<0 表示手指在屏幕向右滚动
dy 绝对值为手指在y方向滚动的距离,dy<0 表示手指在屏幕向下滚动
consumed 一个数组,值用来表示父布局消耗了多少距离,未消耗前为[0,0], 如果父布局想处理滚动事件,就可以在这个方法的实现中为consumed[0],consumed[1]赋值。分别表示x和y方向消耗的距离。如父布局想在竖直方向(y)完全拦截子视图,那么让 consumed[1] = dy,就把手指产生的触摸事件给拦截了,子视图便响应不到触摸事件了 。
这个方法表示子视图正在滚动,并且把滚动距离回调用到该方法,前提是 onStartNestedScroll 返回了 true。
响应嵌套滚动结束。当一个嵌套滚动结束后(如MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL)会调用该方法,在这里可有做一些收尾工作,比如变量重置
手指在屏幕快速滑触发Fling前回调,如果前面 onNestedPreScroll 中父布局消耗了事件,那么这个也会被触发,返回true表示父布局完全处理 fling 事件
target 滚动的子视图
velocityX x方向的速度(px/s)
velocityY y方向的速度
子视图fling 时回调,父布局可以选择监听子视图的 fling。true 表示父布局处理 fling,false表示父布局监听子视图的fling
返回当前 NestedScrollingParent 的滚动方向。
子控件要实现NestedScrollingChild接口。
设置嵌套滑动是否能用
判断嵌套滑动是否可用
开始嵌套滑动, axes 表示方向轴,有横向和竖向
停止嵌套滑动
判断是否有父View 支持嵌套滑动
在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离
dx x轴上滑动的距离
dy y轴上滑动的距离
consumed 父view消费掉的scroll长度
offsetInWindow 子View的窗体偏移量
子view处理scroll后调用
dxConsumed x轴上被消费的距离(横向)
dyConsumed y轴上被消费的距离(竖向)
dxUnconsumed x轴上未被消费的距离 dyUnconsumed y轴上未被消费的距离 * @param offsetInWindow 子View的窗体偏移量
滑行时调用
velocityX x 轴上的滑动速率
velocityY y 轴上的滑动速率
consumed 是否被消费
进行滑行前调用
velocityX x 轴上的滑动速率
velocityY y 轴上的滑动速率
上面NestedScrollingChild中的大部分方法都可以使用NestedScrollingHelper来实现。具体看后面的例子
上面介绍NestedScrollingPrent有很多方法,其实实现3个就可以了:
1)、onStartNestedScroll该方法,一定要按照自己的需求返回true,该方法决定了当前控件是否能接收到其内部View(非并非是直接子View)滑动时的参数。假设你只涉及到纵向滑动,这里可以根据nestedScrollAxes这个参数,进行纵向判断。
2)、onNestedPreScroll该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2。
3)、onNestedFling你可以捕获对内部View的fling事件,如果return true则表示拦截掉内部View的事件。
具体代码如下:
public class NestedScrollParent extends LinearLayout implements NestedScrollingParent {
private static final String TAG = NestedScrollingParent.class.getSimpleName();
private View head;
private int headHeight;
private Scroller scroller;
public NestedScrollParent(Context context) {
super(context);
init();
}
public NestedScrollParent(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public NestedScrollParent(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
scroller = new Scroller(getContext());
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
head = getChildAt(0);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
headHeight = head.getMeasuredHeight();
}
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes) {
}
@Override
public void onStopNestedScroll(@NonNull View target) {
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
int scrollY = getScrollY();
boolean isHiddeTop = dy > 0 && scrollY < headHeight;
boolean isShowTop = dy < 0 && getScrollY() > 0 && !target.canScrollVertically(-1);
if (isHiddeTop || isShowTop) {
scrollTo(0, dy + scrollY);
consumed[1] = dy;
}
}
@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) {
if (getScrollY() >= headHeight) {
return false;
}
fling((int) velocityY);
return true;
}
@Override
public int getNestedScrollAxes() {
return 0;
}
public void scrollTo(int x, int y) {
if (y > headHeight) {
y = headHeight;
} else if (y < 0) {
y = 0;
}
if (y != getScrollY()) {
super.scrollTo(x, y);
}
}
public void fling(int velocityY) {
scroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, headHeight);
invalidate();
}
}
NestedScrollingChild的具体实现基本都交给NestedScrollingChildHelper实现了
public class NestedScrollChild extends AppCompatTextView implements NestedScrollingChild {
private static final String TAG = NestedScrollChild.class.getSimpleName();
private NestedScrollingChildHelper nestedHelper;
public NestedScrollChild(Context context) {
super(context);
init();
}
public NestedScrollChild(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public NestedScrollChild(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
nestedHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
nestedHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return true;
}
@Override
public boolean startNestedScroll(int axes) {
return nestedHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
nestedHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return nestedHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
return nestedHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
return nestedHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return nestedHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return nestedHelper.dispatchNestedPreFling(velocityX, velocityY);
}
int mLastTouchX;
int mLastTouchY;
int[] mScrollConsumed = new int[2];
int[] mScrollOffset = new int[2];
@Override
public boolean onTouchEvent(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastTouchY = (int) (e.getRawY() + 0.5f);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
case MotionEvent.ACTION_MOVE:
int x = (int) (e.getRawX() + 0.5f);
int y = (int) (e.getRawY() + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
Log.i("tag", "child:dy:" + dy + ",mLastTouchY:" + mLastTouchY + ",y;" + y);
mLastTouchY = y;
mLastTouchX = x;
// 分发滑动,如果父类一点儿也没有消费会返回false
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
// 减去父类消耗的
dy -= mScrollConsumed[1];
if (dy == 0) {
return true;
}
}
int scrollY = getScrollY();
Log.i("tag", "scrollY:" + scrollY);
if (scrollY >= 0) {
Log.i("tag", "scrollBy:" + scrollY);
scrollTo(0, dy + getScrollY());
}
break;
}
return true;
}
@Override
public void scrollTo(int x, int y) {
if (y > getMeasuredHeight()) {
y = getMeasuredHeight();
} else if (y < 0) {
y = 0;
}
super.scrollTo(x, y);
}
}
重点还是在onTouchEvent中的事件分发。ACTION_DOWN时要调用startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL),然后会触发NestedScrollingParent的onStartNestedScroll。 ACTION_MOVE时要先调用dispatchNestedPreScroll,然后会触发NestedScrollingParent的onNestedPreScroll,通知父容器判断是否消耗滑动,然后返回消耗掉得距离。子View再滑动剩下的距离,子View滑动后还有剩余时调用dispatchNestedScroll,然后会触发NestedScrollingParent的onNestedScroll继续消费剩余距离。
大致执行流程如下:
1)、子view 需要滑动的时候例如 ACTION_DOWN 的时候就要调用 startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL | ViewCompat.SCROLL_AXIS_VERTICAL) 方法来告诉父 view 自己要开始滑动了(实质上是寻找能够配合 child 进行嵌套滚动的 parent)。
2)、父 view 会收到 onStartNestedScroll 回调从而决定是不是要配合子view做出响应。如果需要配合,此方法会返回 true。继而 onStartNestedScroll()回调会被调用。
3)、在滑动事件产生但是子 view 还没处理前可以调用 dispatchNestedPreScroll(0,dy,consumed,offsetInWindow) 这个方法把事件传给父 view 这样父 view 就能在onNestedPreScroll 方法里面收到子 view 的滑动信息,然后做出相应的处理把处理完后的结果通过 consumed 传给子 view。
4)、dispatchNestedPreScroll()之后,child可以进行自己的滚动操作。
5)、如果父 view 需要在子 view 滑动后处理相关事件的话,可以在子 view 的事件处理完成之后调用 子 view 的dispatchNestedScroll, 然后父 view 会在 onNestedScroll 收到回调。
6)、最后,滑动结束,调用 onStopNestedScroll() 表示本次处理结束。
1、嵌套滑动机制并没有改变原有的事件分发机制,嵌套滑动机制是在原有的事件分发机制上实现的。
2、自定义NestedScrollParent在onNestedPreScroll()方法中判断是否处理滑动,如果需要处理就在onNestedPreScroll()滑动即可。