关于CoordinatorLayout AppBarLayout原理的一些分析
这几天学了一些CoordinatorLayout、AppBarLayout配合使用的一些方法,之前还写了一篇CoordinatorLayout Behavior一些笔记,通过这几天对源码的阅读,现在对CoordinatorLayout、AppBarLayout这部分的内容有了更深一层的理解,接下来我就把我所理解的源码简单的分析一下。
一、 NestedScrolling机制
CoordinatorLayout、AppBarLayout分别实现了NestedScrolling机制中需要的接口和接口中的一些方法,如果大家对NestedScrolling不是很了解,可以先去网上了解一下,这里我简单说明一下这个机制的原理:Nested这个单词的意思是“嵌套”,这个机制其实就是嵌套滑动的一种处理机制,它和之前只能单一View消耗滑动事件的处理机制不同,它会在子View处理滑动事件时,先将滑动事件传递到父View中,询问父View是否需要消耗滑动事件,如果父View需要消耗滑动事件,子View会将此次x,y滑动的距离先传递到父View中,父View会先消耗滑动事件,如果父View没消耗全部的滑动距离,子View会消耗剩余的滑动距离,如果剩余的滑动距离大于子View剩余需要的滑动距离(例如RecyclerView距离自身Content滑动到顶部的距离只有10,但是此次滑动距离dy有50,父View消耗了30,剩余20大于RecyclerView剩余需要滑动的距离),子View会把剩下的滑动距离再次传递给父View,由父View去消耗。
我推荐两篇我觉得还挺不错的文章可以帮助理解这个机制:Android NestedScrolling机制完全解析 带你玩转嵌套滑动和android NestedScroll嵌套滑动机制完全解析-原来如此简单
二、可以实现的效果
说了这么多,这个机制到底可以实现什么样的效果呢,其实就是滑动起来非常的顺滑,例如,我在界面中放了一个RecyclerView,RecyclerView上面放了一个AppBarLayout包裹的ImageView,当我滑动这个界面时,不会像原来那种机制需要在RecyclerView滑动到顶部时,需要抬起手指进行下次滑动才能把RecyclerView上面的View滑出屏幕以外,效果图如下:
三、原理分析
下面开始进行我对源码阅读的分析理解,这里主要分成两个部分,主要是RecyclerView、CoordinatorLayout 、AppBarLayout如何实现了NestedScrolling机制。
先简要概括一下总体的中心思想,根据上文对NestedScrolling的介绍,这里的RecyclerView就是子View,CoordinatorLayout就是父View,AppBarLayout是父View在判断是否消耗事件,在判断方法中主要依据的View。主要的过程都是在RecyclerView的onTouchEvent中,分别在Down和Move事件中完成了整个机制的流程。这里说一句题外话,为什么ListView、GirdView不能实现这种效果?因为这两个View并没有实现NestedScrolling机制中相关的方法,可以看一下RecyclerView源码,我们会发现RecycerView定义如下:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild
1.RecyclerView中MotionEvent.ACTION_DOWN做了哪些事儿?
这里我先来一张流程图:
这里所做的一件事儿,就是子View在滑动事件开始时,传递给父View,父View会去判断是否需要消耗此次事件,下面就是源码的分析
//RecyclerView
@Override
public boolean onTouchEvent(MotionEvent e){
//.................
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//调用NestedScrollingChildHelper
startNestedScroll(nestedScrollAxis);
} break;
//..........
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
上面的startNestedScroll方法就会调用到NestedScrollingChildHelper中的startNestedScroll方法。Helper中该方法的实现如下:
//NestedScrollingChildHelper
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
//通过while循环,不断的去判断是否有View的ParentView需要消耗这次滑动事件
while (p != null) {
//判断parent是否需要消耗
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
//父View消耗滑动事件
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
//循环结束,没有发现需要消耗的View
return false;
}
在这个方法中,所做的事儿只有一件,去循环遍历并且询问这个View的ParentView和ParentView的ParentView是否需要消耗这次事件,如果有消耗的返回true否则返回false,这里判断的方法使用了ViewParentCompat.onStartNestedScroll(p, child, mView, axes),这个方法实现很简单,里面仅仅是调用了我们传入的参数p的onStartNestedScroll方法,在我的事例中,p就是CoordinatorLayout,所以我们可以直接查看CoordinatorLayout中onStartNestedScroll方法的实现
//CoordinatorLayout
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
boolean handled = false;
final int childCount = getChildCount();
//仍然是遍历子View,判断是否有View需要消耗
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
//判断behavior是否为空
if (viewBehavior != null) {
//获取View是否消耗滑动事件
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
handled |= accepted;
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
这里我们可以看到,当父View也就是CoordinatorLayout判断是否消耗滑动事件的方式也很简单,就是遍历自己的子View,如果子View有消耗就返回true,这里使用的是 “|=” 只要有子View需要接收便是true,接着在当前例子中,ImageView包裹在AppBarLayout,那么在这个函数遍历中,就会获取到AppBarLayout的Behavior,并且调用AppBarLayout的中Behavior的onStartNestedScroll方法,就是上面时序图的最后一个LifeLine,AppBarLayout的中Behavior的onStartNestedScroll实现如下:
//AppBarLayout$Behavior
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
View directTargetChild, View target, int nestedScrollAxes) {
// Return true if we're nested scrolling vertically, and we have scrollable children
// and the scrolling view is big enough to scroll
final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& child.hasScrollableChildren()
&& parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
if (started && mOffsetAnimator != null) {
// Cancel any offset animation
mOffsetAnimator.cancel();
}
// A new nested scroll has started so clear out the previous ref
mLastNestedScrollingChildRef = null;
return started;
}
这里可以看到,在AppBarLayoutBehavior,我在CoordinatorLayout代码中并没有找到,后来忽然发现,原来是用了注解的形式在AppBarLayout开头声明了
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout
这样到此为止,我们第一个阶段的分析就完成了,当RecyclerView发生了MotionEvent.ACTION_DOWN事件时,经历了NestedScrollingChildHelper->ViewParentCompat->CoordinatorLayout->AppBarLayout来完成NestedScrolling机制中的第一步,子View在滑动事件发生时,告知父View是否需要消耗事件
2.RecyclerView中MotionEvent.ACTION_MOVE做了哪些事儿?
同样这里也首先来一张流程图:
这里做的事儿,就是在父View需要处理滑动事件时,先将滑动事件传递到父View,然后拿到剩下未消耗的距离自己消耗,如果在自己消耗后还有剩余,那么在传递给父View,下面开始一步一步的分析源码
//RecyclerView
@Override
public boolean onTouchEvent(MotionEvent e) {
//...............
switch (action) {
//..................
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id " +
mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
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;
//调用dispatchNestedPreScroll方法并且mScrollConsumed数组记录消耗
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
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];
//scrollByInternal传递剩余消耗
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;
}
这里首先是在子View消耗事件之前,通过调用dispatchNestedPreScroll方法,如果父View消耗事件,则子View的dx,dy会减去已经消耗掉的,dispatchNestedPreScroll主要调用了NestedScrollingChildHelper的dispatchNestedPreScroll方法,我们看一下实现
//NestedScrollingChildHelper
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
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) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
//这里调用了onNestedPreScroll方法询问父View是否消耗
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
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;
}
这里依然使用了ViewParentCompat的方法,ViewParentCompat.onNestedPreScroll方法依然是调用我们传入的父View的onNestedPreScroll方法,这里我的父View依然还是CoordinatorLayout,这里没用循环遍历,是因为之前我们已经在ViewParentCompat.startNestedScroll遍历中保存了mNestedScrollingParent为CoordinatorLayout,所以我们下一步可以直接查看CoordinatorLayout的onNestedPreScroll
//CoordinatorLayout
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
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) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
//传递给子View进行消耗
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
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;
}
}
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
可以看到,CoordinatorLayout对于是否消耗事件,依然是传递给子View去消耗,在我们例子中的这个布局下能够消耗掉这个事件的View就是AppBarLayout,这样事件就又传递给了CoordinatorLayout的子View去消耗,消耗完了以后,可以看到下面还调用了onChildViewsChanged这方法,这个方法的作用是做一些和Behavior相关的操作,有关这部分内容可以看我的上篇文章CoordinatorLayout Behavior一些笔记,这样对于viewBehavior.onNestedPreScroll这里,我们需要查看AppBarLayout$Behavior中的实现:
//AppBarLayout$Behavior
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dx, int dy, int[] consumed) {
if (dy != 0 && !mSkipNestedPreScroll) {
int min, max;
if (dy < 0) {
// We're scrolling down
min = -child.getTotalScrollRange();
max = min + child.getDownNestedPreScrollRange();
} else {
// We're scrolling up
min = -child.getUpNestedPreScrollRange();
max = 0;
}
//AppbarLayout只消耗dy的事件,将消耗的事件赋值给consumed[1]并且scroll自身内容
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
}
}
到这里,如果AppBarLayout需要消耗滑动事件的话,就会消耗并且滚动自己的内容。大家有没有好奇一点,整个过程并没有返回值,那么RecyclerView是如何通过一大堆调用拿到AppBarLayout的消耗呢?其实很简单,就是Java中传递数组时,和C++中按值传递不一样,Java中非基本类型的传递类似于C++中按引用传递,还记得我们RecyclerView中调用dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)这个方法么?这里mScrollConsumed数组就是传递到onNestedPreScroll中的 int[] consumed,所以只要给 int[] consumed赋值,就可以在RecyclerView拿到消耗的dx,dy,分别对应mScrollConsumed[0]和mScrollConsumed[1],接着在AppBarLayout处理完以后,我们还是看上面的时序图,会发现,NestedScrollingChildHelper中是需要有返回值的,需要RecyclerView判断父View是否消耗了滑动事件,我们可以看上面NestedScrollingChildHelper的dispatchNestedPreScroll方法中在子View处理消耗事件后,会return consumed[0] != 0 || consumed[1] != 0;
// NestedScrollingChildHelper
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
//........................
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
这样这个父View的消耗就在子View滑动之前完成了,接着就是子View的滑动,并且如果还有没消耗完的滑动距离会传递给父View让父View去处理,这里的过程主要就是在RecyclerView的scrollByInternal方法中了:
//RecyclerView
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
consumePendingUpdateOperations();
if (mAdapter != null) {
eatRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
resumeRequestLayout(false);
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
//dispatchNestedScroll方法传递consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset给父View
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// 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];
} else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
return consumedX != 0 || consumedY != 0;
}
scrollByInternal在onTouchEvent中会被调用,scrollByInternal通过调用dispatchNestedScroll把事件传递给父View,其实仍然是调用了NestedScrollingChildHelper的dispatchNestedScroll,该方法的实现:
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
ViewParentCompat.onNestedScroll方法依然是调用了CoordinatorLayout的onNestedScroll方法,实现如下:
//CoordinatorLayout
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
final int childCount = getChildCount();
boolean accepted = false;
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
accepted = true;
}
}
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
这也依然一样,CoordinatorLayout会循环遍历,交给子View去处理,这里仍然还是AppBarLayout$Behavior的onNestedScroll:
//AppBarLayout$Behavior
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
if (dyUnconsumed < 0) {
// If the scrolling view is scrolling down but not consuming, it's probably be at
// the top of it's content
scroll(coordinatorLayout, child, dyUnconsumed,
-child.getDownNestedScrollRange(), 0);
// Set the expanding flag so that onNestedPreScroll doesn't handle any events
mSkipNestedPreScroll = true;
} else {
// As we're no longer handling nested scrolls, reset the skip flag
mSkipNestedPreScroll = false;
}
}
到这未消耗的事件就又传递到AppBarLayout了,这里的注释很清晰:If the scrolling view is scrolling down but not consuming, it’s probably be at the top of it’s content,翻译一下,就是如果scrolling view 向下滚动,但是没有消耗滚动事件,可能是已经滑倒了顶部,例如RecyclerView已经滑倒了第一个Item,然后AppBarLayout就会消耗剩余的事件在scroll方法中
到此,整个流程就已经清晰明了了,整个Scrolling机制在CoordinatorLayout AppBarLayout中就是这么实现的,整体的流程的概览就可以参照上面的两个时序图,对整个源码的分析和理解后,以后在使用起来我们就会更加的得心应手。