前言
级联滑动在现在的app设计中越来越常见,在android的老版本中,并没有添加对级联滑动的支持,但是如今,几乎所有view都会默认实现了级联滑动的功能。
除了在源码中实现了级联滑动,为了兼容,android还在support中添加了级联滑动的接口,实际上实现方案和源码相同。为了方便分析,我们就从support入手,来看一下级联滑动的实现。
NestedScrollingChild
public interface NestedScrollingChild {
// 参数enabled:true表示view使用嵌套滚动,false表示禁用.
public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
// 参数axes:表示滚动的方向如:ViewCompat.SCROLL_AXIS_VERTICAL(垂直方向滚动)和
// ViewCompat.SCROLL_AXIS_HORIZONTAL(水平方向滚动)
// 返回值:true表示本次滚动支持嵌套滚动,false不支持
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean hasNestedScrollingParent();
// 参数dxConsumed: 表示view消费了x方向的距离长度
// 参数dyConsumed: 表示view消费了y方向的距离长度
// 参数dxUnconsumed: 表示滚动产生的x滚动距离还剩下多少没有消费
// 参数dyUnconsumed: 表示滚动产生的y滚动距离还剩下多少没有消费
// 参数offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
// 参数dx: 表示view本次x方向的滚动的总距离长度
// 参数dy: 表示view本次y方向的滚动的总距离长度
// 参数consumed: 表示父布局消费的距离,consumed[0]表示x方向,consumed[1]表示y方向
// 参数offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
// 这个是滑动的就不详细分析了
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
来说 NestedScrollingChild
。如果你有一个可以滑动的 View,需要被用来作为嵌入滑动的子 View,就必须实现本接口。在此 View 中,包含一个 NestedScrollingChildHelper
辅助类。NestedScrollingChild
接口的实现,基本上就是调用本 Helper 类的对应的函数即可,因为 Helper 类中已经实现好了 Child 和 Parent 交互的逻辑。原来的 View 的处理 Touch 事件,并实现滑动的逻辑大体上不需要改变。
需要做的就是,如果要准备开始滑动了,需要告诉 Parent,你要准备进入滑动状态了,调用 startNestedScroll()
。你在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用 dispatchNestedPreScroll()
。如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。然后,你自己进行余下的滑动。最后,如果滑动距离还有剩余,你就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用 dispatchNestedScroll()
。
NestedScrollingParent
public interface NestedScrollingParent {
// 参数child:ViewParent包含触发嵌套滚动的view的对象
// 参数target:触发嵌套滚动的view (在这里如果不涉及多层嵌套的话,child和target)是相同的
// 参数nestedScrollAxes:就是嵌套滚动的滚动方向了.
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
// 参数target:同上
// 参数dxConsumed:表示target已经消费的x方向的距离
// 参数dyConsumed:表示target已经消费的x方向的距离
// 参数dxUnconsumed:表示x方向剩下的滑动距离
// 参数dyUnconsumed:表示y方向剩下的滑动距离
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
// 参数dx:表示target本次滚动产生的x方向的滚动总距离
// 参数dy:表示target本次滚动产生的y方向的滚动总距离
// 参数consumed:表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离.
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
}
作为一个可以嵌入 NestedScrollingChild 的父 View,需要实现 NestedScrollingParent
,这个接口方法和 NestedScrollingChild
大致有一一对应的关系。同样,也有一个 NestedScrollingParentHelper
辅助类来默默的帮助你实现和 Child 交互的逻辑。滑动动作是 Child 主动发起,Parent 就收滑动回调并作出响应。
从上面的 Child 分析可知,滑动开始的调用 startNestedScroll()
,Parent 收到 onStartNestedScroll()
回调,决定是否需要配合 Child 一起进行处理滑动,如果需要配合,还会回调 onNestedScrollAccepted()
。
每次滑动前,Child 先询问 Parent 是否需要滑动,即 dispatchNestedPreScroll()
,这就回调到 Parent 的 onNestedPreScroll()
,Parent 可以在这个回调中“劫持”掉 Child 的滑动,也就是先于 Child 滑动。
Child 滑动以后,会调用 onNestedScroll()
,回调到 Parent 的 onNestedScroll()
,这里就是 Child 滑动后,剩下的给 Parent 处理,也就是 后于 Child 滑动。
最后,滑动结束,调用 onStopNestedScroll()
表示本次处理结束。
其实,除了上面的 Scroll 相关的调用和回调,还有 Fling 相关的调用和回调,处理逻辑基本一致。
NestedScrollView
NestedScrollView 作为级联滑动最典型的实现,我们可以通过它来详细了解这中间的流程。
public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
NestedScrollingChild, ScrollingView {
..........
}
首先该类的定义,它同时继承了Parent和Child,原因在于NestedScrollView中嵌套NestedScrollView是非常多见的,这个时候它既是child也是parent。这会对我们代码分析造成一定障碍,但是影响并不会很大。
忽略构造,初始化等过程,我们此文的目标是滚动,所以直接来看第一个和滚动相关的方法
第一段代码实际上并不是最开始起作用的,因为我们第一个事件一定是DOWN,所以需要先看一下对于DOWN的处理
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
/*
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged. We need to call computeScrollOffset() first so that
* isFinished() is correct.
*/
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
如果点击的位置不在child中,我们intercept方法直接返回false,表示不拦截事件(这个时候就算我们不拦截,其实也会传入onTouch的吧)。
如果当前正在滚动(正在滚动,又遇到手指按下……可能的情况就是在flinged的时候按下)那么isBeingDragged为true表示会拦截后面的事件,否则为false不拦截。然后调用startNestedScroll。
疑问,如果该scrollview是parent的,那么它调用这个方法会不会出问题?来看下代码:
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
//递归查找parent,如果parent的onStartNestedScroll返回true,表示找到了父NestedScrolling
// 并且调用它的onNestedScrollAccepted方法
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
这是childHelper的最终调用。如果已经有NestedScrollingParent了,那么直接返回true。如果该view允许级联滑动(NestedScrollView在初始化时就设置为允许级联了。)版本寻找它的父NestedScrolling。方法就是递归调用父控件的onStartNestedScroll(与当前版本相关,所以用到了ViewParentCompat.)
在NestedScrollView嵌套的环境下,实际上就是调用父NestedScrollView的onStartNestedScroll方法
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}
支持y轴,返回true。然后再调用onNestedScrollAccepted方法。一般情况下,我们只会调用parentHelper的对应方法(实际上就是记录一下滚动轴,没有其他操作),但是由于NestedScrollView同事继承了parent和child,所以会继续调用startNestedScroll用于多层(大于两层)scrollView的级联。
假设假设子view不会拦截onTouch事件,所以还是会回传到child的onTouch方法中。onTouch方法中关于DOWN事件的操作我们本篇不关心,无非是停止滚动动画之类的,我们关心的是,onTouch方法会返回true!也就是child的onIntercept方法将不再调用。
接下去就是Move事件了
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/
/*
* Shortcut the most recurring case: the user is in the dragging
* state and he is moving his finger. We want to intercept this
* motion.
*/
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
..........
}
第一关,不是isBeingDragged,所以忽略,继续往下走。
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop
&& (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
...
}
至少在NestedScrollView嵌套的情况下
(getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0
永远不成立,所以这段代码且略过。
所以接下去还是child的onTouch方法。
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
....
break;
首先会调用dispatchNestedPreScroll方法,该方法意义在开篇有说。在NestedScrollView中,该方法仅仅只是级联调用,在本例情况下(双NestedScrollView嵌套)实际上最终并没有什么效果,返回false。
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = getScrollY();
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
|| (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollByCompat will call onOverScrolled, which
// calls onScrollChanged if applicable.
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
ensureGlows();
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex)
/ getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
如果大于一个滚动阈值,那么就会进入下面操作了。
requestDisallowInterceptTouchEvent,表示禁用父控件的onIntercept方法。所以最后的事件实际上只有最底层的NestedScrollView能够去处理,parent连onIntercept都不会调用(需要主要,如果当前的点击不是发生在子NestedScrollView的控件上的话实际上就不会涉及级联滚动的,相当于单层scrollview而已)。
并且把isBegingDragged设置为true,表示需要开始滚动了。
主要回去调用dispatchNestedScroll方法,通知父控件消费滚动距离。