1.嵌套滑动
嵌套滑动是在父View包含子View的情况下,子View将自己的滑动状态告诉父View,父View根据自己的情况做出相应的动作(滑动)。本文仅对当RecyclerView启用嵌套滑动功能时在LinearLayoutManager布局情况下的下拉刷新来分析。它通常和SwipeRefreshLayout一起使用来达到效果,SwipeRefreshLayout作为RecyclerView的直接父类,在RecyclerView滑动到顶部继续下拉时会让SwipeRefreshLayout一起下拉出现刷新效果,但这是怎么做到的呢,我们就从以下几个比较重要的类和接口分析:
NestedScrollingChild:该接口由嵌套滑动的子类实现,它可以记录子类的各种滑动状态和数据,比如滑动的开始、结束、抛出、速度等,当我们使用时需要确定子View具体在什么地方实现此接口中的方法,RecyclerView作为SwipeRefreshLayout的子类就实现了这个接口,不过它实现的是NestedScrollingChild2接口,只是扩展了下NestedScrollingChild而已,之后我们会分析下RecyclerView中这些方法实现的时机。
NestedScrollingChildHelper:NestedScrollingChild接口的实现类会将接口中的逻辑实现交给NestedScrollingChildHelper来处理,看NestedScrollingChildHelper的源码我们会发现在里面会调用ViewParentCompat来判断系统版本,然后通知父View中相应的方法。同时,它也是兼容5.0以下版本的助手类。
NestedScrollingParent:嵌套滑动的父类应实现的接口,里面定义了子类各种滑动状态的通知方法,通过判断子类滑动的状态,让父类做出相应的操作。SwipeRefreshLayout实现了此接口。
NestedScrollingParentHelper:嵌套滑动的父类助手类,兼容5.0版本之前。
在下一篇文章中,我们会扩展一下SwipeRefreshlayout的功能让它同时支持RecyclerView和ListView的上拉加载,并且能在原生效果和传统的延展效果之间切换,有兴趣的朋友可以看一下(自定义SwipeRefreshLayout,一个能同时支持RecyclerView和ListView的下拉刷新和上拉加载的布局)
2.RecyclerView嵌套滑动分析
下面这张图描述了NestedScrollingChild接口中方法和NestedScrollingParent中接口中方法的调用关系,他们之间通过NestedScrollingChildHelper来沟通。
按照上图的顺序,我们看一下RecyclerView的嵌套滑动实现:
a.首先在RecyclerView的构造函数中有(代码有省略,具体可看源码)
public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
···省略···
if (Build.VERSION.SDK_INT >= 21) {
a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS,
defStyle, defStyleRes);
nestedScrollingEnabled = a.getBoolean(0, true);
a.recycle();
}
···省略···
//设置RecyclerView是否支持嵌套滑动
setNestedScrollingEnabled(nestedScrollingEnabled);
}
setNestedScrollingEnabled(nestedScrollingEnabled);
/**
* 设置RecyclerView是否能嵌套滑动
*/
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
/**
* 获取RecyclerView是否支持嵌套滑动
*/
@Override
public boolean isNestedScrollingEnabled() {
return getScrollingChildHelper().isNestedScrollingEnabled();
}
同时,在NestedScrollingChildHelper中会有如下代码
public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(mView);
}
mIsNestedScrollingEnabled = enabled;
}
以上代码设置了RecyclerView是否支持嵌套滑动,这是整个嵌套滑动的初始地方,在构造函数中就已经声明,同时,它的具体实现是在getScrollingChildHelper().isNestedScrollingEnabled()中,也就是NestedScrollingChildHelper中实现的。另外,以后的所有的NestedScrollingChild接口中的实现方法具体实现逻辑都调用了getScrollingChildHelper()来处理,也就是委托给了NestedScrollingChildHelper。
b.然后,我们看一下boolean startNestedScroll()方法是在何处调用的,可以看到有三处地方,分别是onInterceptTouchEvent()方法中的MotionEvent.ACTION_DOWN事件中、onTouchEvent()方法中的MotionEvent.ACTION_DOWN事件和MotionEvent.ACTION_UP事件中。
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
···省略···
switch (action) {
case MotionEvent.ACTION_DOWN:
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
break;
···省略···
}
return mScrollState == SCROLL_STATE_DRAGGING;
}
@Override
public boolean onTouchEvent(MotionEvent e) {
···省略···
switch (action) {
case MotionEvent.ACTION_DOWN: {
···省略···
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
···省略···
case MotionEvent.ACTION_UP: {
···省略···
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;
···省略···
}
···省略···
return true;
}
/*
* 抛出时
*/
public boolean fling(int velocityX, int velocityY) {
···省略···
if (!dispatchNestedPreFling(velocityX, velocityY)) {
···省略···
if (canScroll) {
···省略···
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
···省略···
return true;
}
}
return false;
}
/*
* 当开始滑动时
*/
@Override
public boolean startNestedScroll(int axes, int type) {
return getScrollingChildHelper().startNestedScroll(axes, type);
}
再来看一下NestedScrollingChildHelper中
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
startNestedScroll()方法中,会调用ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)和ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type)方法,其中的参数含义
p:父View,这里很明显获取到的是SwipeRefreshLayout
child:子View,这里为RecyclerView
axes:轴方向,X轴或Y轴,可沿着轴方向进行滑动
type:用户触摸导致的滑动、非用户触摸导致的滑动(TYPE_TOUCH、TYPE_NON_TOUCH)
再看下ViewParentCompat中相关的方法
static final ViewParentCompatBaseImpl IMPL;
//根据版本获取IMPL
static {
if (Build.VERSION.SDK_INT >= 21) {
IMPL = new ViewParentCompatApi21Impl();
} else if (Build.VERSION.SDK_INT >= 19) {
IMPL = new ViewParentCompatApi19Impl();
} else {
IMPL = new ViewParentCompatBaseImpl();
}
}
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
}
return false;
}
/*
* 5.0版本以下会调用这里的方法
*/
static class ViewParentCompatBaseImpl {
public boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes) {
if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
return false;
}
}
/*
* 5.0版本以上会调用这里的方法
*/
static class ViewParentCompatApi21Impl extends ViewParentCompatApi19Impl {
@Override
public boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes) {
try {
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onStartNestedScroll", e);
return false;
}
}
}
这里要小心,从代码中我们可以看到IMPL是根据不同版本获取并调用的,在5.0版本以上,NestedScrollingParent接口中所有的抽象方法在ViewGroup中都有同名方法的具体实现,所以在5.0版本以上,ViewParentCompat中默认调用的是ViewGroup中的onStartNestedScroll()方法,但在5.0版本以下,ViewGroup中并没有相应的方法,就会调用NestedScrollingParent接口中的具体实现方法。 如果我们没有主动实现NestedScrollingParent的onStartNestedScroll()抽象方法,那我们的代码就会在5.0版本以下运行时报错,这个下面还会再提到。
到现在,我们已经从RecyclerView源码中一路跟踪,找到了与它父View之间的调用关系,我们再看看剩下的一些方法的调用。
c.在调用完startNestedScroll()之后,手指继续触摸RecyclerView会调用dispatchNestedPreScroll()和dispatchNestedScroll()方法
@Override
public boolean onTouchEvent(MotionEvent e) {
...省略...
switch (action) {
...省略...
case MotionEvent.ACTION_MOVE: {
...省略...
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
...省略...
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
...省略...
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
boolean scrollByInternal(int x, int y, MotionEvent ev) {
...省略...
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
...省略...
return consumedX != 0 || consumedY != 0;
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow, int type) {
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
type);
}
这两个方法发生在RecyclerView的MotionEvent.ACTION_MOVE事件中,区别是,dispatchNestedPreScroll()方法先调用,返回一个经父View计算后的消费值,在RecyclerView中会用当前移动的距离减去这个消费值作为RecyclerView移动的偏移量,之后会把这个偏移量再经过计算后得到consumedX, consumedY, unconsumedX, unconsumedY作为参数传递给dispatchNestedScroll()方法。最后再通过NestedScrollingChildHelper类转交给ViewParentCompat,再回调父VIew的onNestedPreScroll()方法和onNestedScroll()方法,参考下面源码。
NestedScrollingChildHelper中
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
···省略···
if (dx != 0 || dy != 0) {
···省略···
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
}
···省略···
}
return false;
}
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
@NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
···省略···
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
···省略···
ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
}
···省略···
}
return false;
}
NestedScrollingChildHelper中
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
IMPL.onNestedPreScroll(parent, target, dx, dy, consumed);
}
}
public static void onNestedScroll(ViewParent parent, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedScroll(target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
IMPL.onNestedScroll(parent, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
}
}
onNestedPreScroll()方法参数含义:
int dx:RecyclerView水平滚动的距离
int dy:RecyclerView竖直滚动的距离
int[] consumed:需要父View消费的水平或竖直滚动距离,RecyclerView实际上滚动的距离是dx-consumed[0]和dy-consumed[1]
onNestedScroll()方法参数含义:
dxConsumed:表示RecyclerView在X轴方向上真实滚动的距离,只有当RecyclerView滚动到X轴两端无法再继续滚动时为0
dyConsumed:表示RecyclerView在Y轴方向上真实滚动的距离,只有当RecyclerView到达Y轴两端无法再滑动时为0
dxUnconsumed:当RecyclerView滑动到X轴两端无法再继续滑动时,如果这时继续滑动,此参数就是表示的当前手指X轴方向上的滑动距离
dyUnconsumed:当RecyclerView滑动到Y轴两端无法再滑动时,如果这时继续滑动,此参数就是表示的当前手指Y轴方向上的滑动距离
d.以上两步骤是手指放下并滑动时调用的方法,当我们手指离开时也会调用相应的方法dispatchNestedPreFling()和dispatchNestedFling(),在源码中是这样体现的
@Override
public boolean onTouchEvent(MotionEvent e) {
···省略···
switch (action) {
···省略···
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;
···省略···
}
···省略···
return true;
}
public boolean fling(int velocityX, int velocityY) {
···省略···
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
···省略···
}
return false;
}
可以看到,在抬起时,先调用dispatchNestedPreFling()方法作为抬起的预处理,将水平和竖直方向的抛出速度作为参数传递给父View,通过父View的返回值决定是否调用dispatchNestedFling()方法。同样,它们也是通过NestedScrollingChildHelper类和ViewParentCompat类实现的。
e.最终,当这些操作结束后,RecyclerView会调用stopNestedScroll()方法,并会通知父View调用onStopNestedScroll(),但stopNestedScroll()方法不仅仅会在最终结束时调用,再刚开始触摸时,如过我们有滑动的行为,也会调用一次此方法作为滑动开始前的标识。
结束语
以上就是RecyclerView嵌套滑动主要方法的总结,实际开发中,我们并不会使用全部的方法,只需要选择合适的就行,嵌套滑动无非就是将子View的滑动状态告诉父View,让父View做出对应的滑动操作,他们之间的沟通是通过两个类和两个接口实现的。下一篇文章中,我们会利用这些类和接口实现一个非常简单好用的下拉刷新上拉加载的RecyclerView.
传送门:自定义SwipeRefreshLayout,一个能同时支持RecyclerView和ListView的下拉刷新和上拉加载的布局