CoordinatorLayout-协调布局,用于实现自定义的嵌套滑动效果。RecyclerView(NestedScrollingChild2)会把滑动事件传递给CoordinatorLayout(NestedScrollingParent2),CoordinatorLayout最后会把滑动事件传递给其直接子View中设置了layout_behavior的控件,用户只需通过自定义CoordinatorLayout.Behavior然后在各个回调函数中去实现自己的逻辑即可。下边列出嵌套滑动中常用的回调函数:
NestedScrollingChild2:
NestedScrollingParent2:
RecyclerView.Behavior
首先RecyclerView接收到ACTION_DOWN事件,然后开始回调startNestedScroll
:
public boolean onTouchEvent(MotionEvent e) {
switch (action) {
case MotionEvent.ACTION_DOWN: {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
}
}
嵌套滑动相关的回调都是通过一个NestedScrollingChildHelper
来进行代理的:
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) {
//兼容性处理,后续分析都看作直接调用CoordinatorLayout相关函数
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;
}
首先会检查是否有支持嵌套滑动的Parent,它在NestedScrollingChildHelper
内部保存:
public class NestedScrollingChildHelper {
private ViewParent mNestedScrollingParentTouch;
}
第一次调用一定返回false。然后回到上边的while循环,这里主要就是去找到支持嵌套滑动的Parent,并赋值给mNestedScrollingParentTouch
,这里有几个注意点:
onStartNestedScroll(View child, View target, int axes,int type)
中返回true的,那么都可以支持嵌套滑动。在这里会回调CoordinatorLayout的onStartNestedScroll(View child, View target, int axes,int type)
:
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
这里逻辑也很清晰,遍历子View,从LayoutParams上找到layout_behavior,如果有那么就会把事件传递给自定义的 Behavior.onStartNestedScroll
,这里也有几个注意点:
Behavior.onStartNestedScroll
默认返回false,也就是我们在自定义Behavior时,需要在该方法中决定是否要支持嵌套滑动。一般是如下实现方式:
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild,
View target, int axes, int type) {
if (target instanceof RecyclerView) {
return true;
}
return false;
}
整个DOWN事件处理完后,其实就是做了一件事,确定是否有Parent支持嵌套滑动,并记录它,以此决定是否要分发后续的嵌套滑动事件,那么接下来看看滑动事件是如何分发的:
onTouchEvent(MotionEvent e) {
switch (action) {
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
if (dispatchNestedPreScroll(dx, dy, mReusableIntPair, mScrollOffset, TYPE_TOUCH)) {
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
}
}
}
这里调用了dispatchNestedPreScroll(dx, dy, mReusableIntPair, mScrollOffset, TYPE_TOUCH)
,其内部是调用NestedScrollingChildHelper.dispatchNestedPreScroll()
,直接看源码:
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
consumed = getTempNestedScrollConsumed();
}
consumed[0] = 0;
consumed[1] = 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;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
首先会检查是否设置了可以处理嵌套滑动的Parent(设置Parent的地方就在之前DOWN事件时调用的NestedScrollingChildHelper.startNestedScroll
),最终还是回调了CoordinatorLayout.onNestedPreScroll()
:
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
//记录子View中消耗最多的滑动距离
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
: Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
: Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
//记录消耗的位移量,也就是用户先于Recycler消耗掉了多少位移量
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
遍历子View,对非GONE状态的View,获取它的Behavior,调用Behavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type)
,默认是空实现,自定义Behavior时,重写该方法去实现自己的滑动逻辑,这里消耗掉的位移量,需要手动设置consumed数组去记录,否则RecyclerView还是会消耗所有的滑动位移。这里有一个注意点是:
onChildViewsChanged(EVENT_NESTED_SCROLL)
函数,内部去处理一些依赖关系的事件传递。为了不影响主流程的分析,这里先跳过放到后边单独分析。 CoordinatorLayout消耗完滑动事件后,继续回到RecyclerView的MOVE事件中,更新滑动量后,调用自身的scrollByInternal(int x, int y, MotionEvent ev)
函数来处理剩余的滑动量:
//保留核心代码
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0;
int unconsumedY = 0;
int consumedX = 0;
int consumedY = 0;
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
scrollStep(x, y, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
return consumedX != 0 || consumedY != 0;
}
首先调用scrollStep(x, y, mReusableIntPair)
计算内部消耗的滑动偏移量,存放于mReusableIntPair
数组中,而scrollStep(x, y, mReusableIntPair)
内部是调用了LayoutManager去计算滑动消耗量的,这里就不深究内部计算逻辑了。计算完内部消耗的滑动量之后,记录一下已经消耗的和暂未消耗的滑动量,再调用dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset, TYPE_TOUCH, mReusableIntPair)
去分发滑动事件,最终还是回调的NestedScrollingChildHelper.dispatchNestedScrollInternal()
:
//保留核心代码
dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed,
int[] offsetInWindow,int type, int[] consumed) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
ViewParentCompat.onNestedScroll(parent, mView,
dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
return true;
}
}
return false;
}
首先还是检查是否有能处理嵌套滑动的Parent,然后调用CoordinatorLayout.onNestedScroll()
:
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
final int childCount = getChildCount();
boolean accepted = false;
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, type);
accepted = true;
}
}
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
遍历子View,对处于非GONE状态下的View获取它的Behavior,然后调用behavior.onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
默认还是空实现,自定义Behavior可以重写该方法去继续消耗RecyclerView没有消耗掉的滑动事件。
嵌套滑动最后一个步骤-停止滑动的事件是在ACTION_UP事件中触发的,它的分发流程和大致相同从RecyclerView -> CoordinatorLayout -> Behavior。
最后给出整体的嵌套滑动的流程图:
在自定义Behavior时有两个方法也经常用到:
在CoordinatorLayout的onMeasure方法内对它的子view进行一个依赖关系的计算prepareChildren():
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);
mChildDag.addNode(view);
// Now iterate again over the other children, adding any dependencies to the graph
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
if (lp.dependsOn(this, view, other)) {
if (!mChildDag.contains(other)) {
// Make sure that the other node is added
mChildDag.addNode(other);
}
// Now add the dependency to the graph
mChildDag.addEdge(other, view);
}
}
}
// Finally add the sorted graph list to our list
mDependencySortedChildren.addAll(mChildDag.getSortedList());
// We also need to reverse the result since we want the start of the list to contain
// Views which have no dependencies, then dependent views after that
Collections.reverse(mDependencySortedChildren);
}
假如CoordinatorLayout内部有4个TextView,它们有如下的依赖关系:
在CoordinatorLayout内部先会以map
建立依赖关系,然后通过深度优先算法进行遍历,结果存放于变量List< View> mDependencySortedChildren
中,可以看到源码中对mDependencySortedChildren
进行了一个逆序操作,这是为了保证第一个View一定是没有任何依赖的那一个,便于后续依赖关系事件的传递。
维护好了上诉的依赖关系后,会在CoordinatorLayout的以下5个方法中都调用onChildViewsChanged(int type)
函数:
onNestedPreScroll()
滑动之前onNestedScroll()
滑动之后onNestedFling
惯性滑动之后OnPreDrawListener.onPreDraw()
在整个ViewTree准备绘制前HierarchyChangeListener.onChildViewRemoved()
View从ViewTree中移除时这几种情况View的位置和大小有可能发生改变,下边给出onChildViewsChanged()
的源码:
//保留核心代码
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
}
}
}
}
两层for循环,两两View之间通过调用b.layoutDependsOn(this, checkChild, child)
判断是否有依赖关系,如果有那么通过b.onDependentViewChanged(this, checkChild, child)
去通知对应的View,完成依赖关系的事件传递。在自定义Behavior中可以重写该方法,去实现相应的业务逻辑。
给出实际案例