这几天学了一些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;
}
这里可以看到,在AppBarLayout Behavior这个类的onStar