Android下NestedScrolling机制与CoordinatorLayout之源码分析

2023-11-16

1.CoordinatorLayout依赖库

旧版本导入CoordinatorLayout依赖

implementation 'com.android.support:design:28.0.0'

升级Android X后的依赖

implementation 'androidx.coordinatorlayout:coordinatorlayout:1.0.0'

2.NestedScrolling机制介绍

NestedScrolling提供了一套父View和子View滑动交互机制;要完成这样的交互,父View需要实现NestedScrollingParent接口,而子View要实现NestedScrollingChild接口,系统提供了NestedScrollingView控件就实现了这两个接口;

public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
        NestedScrollingChild3, ScrollingView {}

NestedScrolling(嵌套滑动),就是子View和父View在滑动过程中,互相通信决定某个滑动是子View处理合适,还是父View处理合适;所以Parent和Child之间存在相互调用,NestedScrollView遵循下面的调用关系:

 上图可以这么理解:

  • ACTION_DOWN的时候子View就要调用startNestedScrolling()方法来告诉父View自己要开始滑动了(实质上是寻找能够配合child进行嵌套滑动的parent),parent也会继续向上寻找能够配合自己滑动的parent,可以理解为在做一些准备工作;(确认是否处理这次滑动事件)
  • 父View会收到onStartNestedScroll回调从而决定是不是要配合子view做出响应;如果需要配合此方法返回true;继而onNestedScrollAccepted()回调会被调用;
  • 在滑动事件产生但是子View还没有处理前可以调用;dispatchNestedPreScroll(0,dy,consumed,offsetInWindow)这个方法把事件传给父View,这样父View就能在onNestedPreScroll()方法里面接收到子View的滑动信息,把处理完后的然后做出相应的处理结果通过consumed传递给子View;
  • dispatchNestedPreScroll()之后,child可以进行自己的滚动操作;
  • 如果父View需要在子View滑动后处理相关事件的话可以在子View的事件处理完成之后调用dispatchNestedScroll()然后父View会在onNestedScroll()收到回调;
  • 最后,滑动结束,调用onStopNestedScroll()表示本次处理结束;
  • 但是,如果滑动速度比较大,会触发fling,fling也分为preFling和fling连个阶段,处理过程和scroll基本差不多

NestedScrollingParentNestedScrollingChild都提供了对应非辅助类,NestedScrollingChildHelperNestedScrollingParentHelper辅助类,辅助类会负责处理大部分逻辑,他们之间的调用关系如下:

2.1NestedScrolling机制关键类说明

NestedScrollingChild/2/3:此接口定义NestedScrolling各状态监听回调方法,需要子View去实现NestedScrollingChild接口,然后借助NestedScrollingChildHelper辅助类实现NestedScrolling各状态派发给父视图处理;

NestedScrollingChildHelper:实现了Child和Parent的交互逻辑,将NestedScrolling操作先交给Parent处理;

NestedScrollingParent/2/3:此接口定义接收子View的NestedScrolling各状态监听回调方法,需要父ViewGroup实现NestedScrollingParent接口,然后借助NestedScrollingParentHelper辅助类实现NestedScrolling各状态派处理;

NestedScrollingParentHelper:帮助Parent实现和Child交互的逻辑;滑动动作是Child的主动发起,Parent就受滑动回调并作出响应;

 2.2实现NestedScrollingChild接口

为什么是NestedScrollingChild接口?

NestedScrollingChild定义一套嵌套滑动事件标准方法(从开始startNestedScroll到结束stopNestedScroll),提供给子View实现,子View负责实现NestedScrollingChild的方法,根据事件的动作类型触发嵌套滑动事件标准方法,最终会调用实现了NestedScrollingParent父ViewGroup处理; 简单理解就是我告诉父ViewGroup我要滑动了,你可以先做滑动处理,然后我在处理;

实际上NestedScrollingChildHelper辅助类已经实现好了Child和Parent交互,原来View的处理逻辑滑动事件逻辑上大体不需要改变;需要做的就是,如果准备开始滑动了,需要告诉Parent,Child要准备进入滑动状态了,调用startNestedScroll();Child在滑动之前,先问一个你的Parent是否需要滑动,也就是调用dispatchNestedPreScroll;如果父类消耗了部分滑动事件,Child需要重新计算一下父类消耗后剩余给Child的滑动距离余量;然后,Child自己进行余下的滑动;最后,如果滑动距离还有剩余,Child就再问一下,Parent是否需要在继续滑动你剩下的距离,也就是调用dispatchNestedScroll(), 大概就是这么一回事,当然还会有和Scroll类型的Fling系列方法,但我们这里可以先忽略一下;

NestedScrollViewNestedScrollingChild接口实现都是交给辅助类NestedScrollingChildHelper来处理的,是否需要进行额外的一些操作要根据实际情况来定;

//NestedScrollChild
public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
            int defStyleAttr) {
        //...
        mParentHelper = new NestedScrollingParentHelper(this);
        mChildHelper = new NestedScrollingChildHelper(this);

        // ...because why else would you be using this widget?
        setNestedScrollingEnabled(true);
    }

@Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }

    //在初始化滚动的时候操作,一般在MotionEvent#ACTION_DOWN的时候调用
    @Override
    public boolean startNestedScroll(int axes) {
        return startNestedScroll(axes, ViewCompat.TYPE_TOUCH);
    }

    @Override
    public void stopNestedScroll() {
        stopNestedScroll(ViewCompat.TYPE_TOUCH);
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH);
    }
    
    //参数和dispatchNestedPreScroll方法的返回有关联
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                offsetInWindow);
    }
    
    //在消费滚动事件之前调用,提供一个让ViewParent实现联合滚动的机会,因此ViewParent可以消费一部分或者全部的滑动事件,参数consumed会记录ViewParent所消费掉的事件
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

实现NestedScrollingChild接口挺简单的不是吗?但还需要我们决定什么时候进行调用,和调用哪些方法;

startNestedScroll和stopNestedScroll的调用

startNestedScroll配合stopNestedScroll使用,startNestedScroll会再接收到ACTION_DOWN的时候调用,stopNestedScroll会在接收到ACTION_UP|ACTION_CANCEL的时候调用,NestedScrollView中的伪代码是这样

onInterceptTouchEvent | onTouchEvent(MotionEvent ev){
    case MotionEvent.ACTION_DOWN:
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    break;
    case MotionEvent.ACTION_UP | ACTION_CANCEL:
        stopNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    break;
}

NestedScrollingChildHelper处理startNestedScroll方法,可以看出会调用Parent的onStartNestedScrollonStartNestedScrollAccepted方法,只要Parent愿意优先处理这次的滑动事件,在结束的时候Parent还会收到onStopNestedScroll回调;

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;
    }

  public void stopNestedScroll(@NestedScrollType int type) {
        ViewParent parent = getNestedScrollingParentForType(type);
        if (parent != null) {
            ViewParentCompat.onStopNestedScroll(parent, mView, type);
            setNestedScrollingParentForType(type, null);
        }
    }

dispatchNestedPreScroll的调用

在消费滚动事件之前调用,提供一个让Parent实现联合滚动的机会,因此Parent可以消费一部分或者全部的滑动事件,注意参数consumed会记录了Parent所消费掉的事件

onTouchEvent(MotionEvent ev){
//..
case MotionEvent.ACTION_MOVE:
//..
                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;    //计算偏移量
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                        ViewCompat.TYPE_TOUCH)) {
                    deltaY -= mScrollConsumed[1];    //减去被消费掉的事件
                    //...
                }
                //...
                break;
}

NestedScrollingChildHelper处理dispatchNestedPreScroll方法,会调用到上一步里记录的希望优先处理Scroll事件的Parent的onNestedPreScroll方法

 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;
                //...
                consumed[0] = 0;
                consumed[1] = 0;
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

                //...
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                //...
            }
        }
        return false;
    }

dispatchNestedScroll的调用

这个方法是在Child自己消费完成Scroll事件后调用的

onTouchEvent(MotionEvent ev){
//..
case MotionEvent.ACTION_MOVE:
//..
final int scrolledDeltaY = getScrollY() - oldY;// 计算这个Child View消耗掉的Scroll事件
final int unconsumedY = deltaY - scrolledDeltaY; //计算这个ChildView还没有消费掉Scroll事件

mScrollConsumed[1] = 0;

dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                            ViewCompat.TYPE_TOUCH, mScrollConsumed);

NestedScrollingChildHelper处理dispatchNestedScroll方法,会调用上一步里记录的希望优先处理Scroll事件的ParentonNestedScroll方法

  private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type, @Nullable int[] consumed) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                //...

                ViewParentCompat.onNestedScroll(parent, mView,
                        dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
                //...
                return true;
            } else if (offsetInWindow != null) {
                //...
            }
        }
        return false;
    }

2.3实现NestedScrollingParent接口

为什么定义NestedScrollingParent接口?

NestedScrollingParent定义了父ViewGroup需要实现嵌套滑动的接口回调,父ViewGroup实现NestedScrollingParent接口,实现了NestedScrollingChild的子view会调用实现NestedScrollingParent的父ViewGroup回调方法,最终可以在父ViewGroup中确认是否处理滑动事件,然后子View处理剩余滑动事件;

同样也有一个NestedScrollingParentHelper辅助类来帮助Parent实现和Child交互的逻辑;滑动动作时Child主动发起,Parent接收滑动回调并作出响应;从上面的Child分析可知,滑动开始的调用startNestedScroll(),Parent收到onStartNestedScroll()回调,决定是否需要配合Child一起进行处理滑动,如果需要配合,还会回调onNestedScrollAccepted(),每次滑动前Child先询问Parent是否需要滑动,即dispatchNestedPreScroll(),这就回调到Parent的onNestedPreScroll(),Parent可以在这个回调中消费掉Child的Scroll事件,也就是优先于Child滑动;

Child滑动以后,会调用dispatchNestedScroll(),回调到Parent的onNestedScroll(),这里就是Child滑动后,剩下的给Parent处理,也就是后于Child滑动;

最后,滑动结束Child调用stopNestedScroll(),回调Parent的onStopNestedScroll()表示本次处理结束;

现在我们来看看NestedScrollingParent的实现细节,这里以CoordinatorLayout来分析而不再是NestedScrollView,因为它才是这篇文章的主角;

在这之前,首先简单介绍下Behavior这对象,你可以在XML中定义它就会在CoordinatorLayout中解析实例化到目标子View的LayoutParams或者获取到CoordinatorLayout子View的LayoutParams对象通过setter方法注入,如果你自定义的Behavior希望实现NestedScroll效果,那么你需要关注重写以下这些方法:

  • onStartNestedScroll : boolean
  • onStopNestedScroll : void
  • onNestedScroll : void
  • onNestedPreScroll : void
  • onNestedFling : void
  • onNestedPreFling : void

你会发现以上这些方法对应了NestedScrollingParent接口的方法,只是在参数上有所增加,且都会在CoordiantorLayout 实现 NestedScrollingParent 接口的每个方法中作出相应回调,下面来简单走读下这部分代码:

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {
  //.....

//CoordiantorLayout的成员变量
private final NestedScrollingParentHelper mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);

   // 参数child:当前实现`NestedScrollingParent`的ViewParent包含触发嵌套滚动的直接子view对象
   // 参数target:触发嵌套滚动的view  (在这里如果不涉及多层嵌套的话,child和target)是相同的
   // 参数nestedScrollAxes:就是嵌套滚动的滚动方向了.垂直或水平方法
   //返回参数代表当前ViewParent是否可以触发嵌套滚动操作
   //CoordiantorLayout的实现上是交由子View的Behavior来决定,并回调了各个acceptNestedScroll方法,告诉它们处理结果
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,nestedScrollAxes);
                handled |= accepted;
                lp.acceptNestedScroll(accepted);
            } else {
                lp.acceptNestedScroll(false);
            }
        }
        return handled;
    }
    //onStartNestedScroll返回true才会触发这个方法
    //参数和onStartNestedScroll方法一样
    //按照官方文档的指示,CoordiantorLayout有一个NestedScrollingParentHelper类型的成员变量,并把这个方法交由它处理
    //同样,这里也是需要CoordiantorLayout遍历子View,对可以嵌套滚动的子View回调Behavior#onNestedScrollAccepted方法
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        mNestedScrollingDirectChild = child;
        mNestedScrollingTarget = target;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScrollAccepted(this, view, child, target, nestedScrollAxes);
            }
        }
    }

    //嵌套滚动的结束,做一些资源回收操作等...
    //为可以嵌套滚动的子View回调Behavior#onStopNestedScroll方法
    public void onStopNestedScroll(View target) {
        mNestedScrollingParentHelper.onStopNestedScroll(target);

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onStopNestedScroll(this, view, target);
            }
            lp.resetNestedScroll();
            lp.resetChangedAfterNestedScroll();
        }

        mNestedScrollingDirectChild = null;
        mNestedScrollingTarget = null;
    }
    //进行嵌套滚动
    // 参数dxConsumed:表示target已经消费的x方向的距离
    // 参数dyConsumed:表示target已经消费的y方向的距离
    // 参数dxUnconsumed:表示x方向剩下的滑动距离
    // 参数dyUnconsumed:表示y方向剩下的滑动距离
    // 可以嵌套滚动的子View回调Behavior#onNestedScroll方法
    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);
            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) {
            dispatchOnDependentViewChanged(true);
        }
    }
    //发生嵌套滚动之前回调
    // 参数dx:表示target本次滚动产生的x方向的滚动总距离
    // 参数dy:表示target本次滚动产生的y方向的滚动总距离
    // 参数consumed:表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离.
    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);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                mTempIntPair[0] = mTempIntPair[1] = 0;
                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) {
            dispatchOnDependentViewChanged(true);
        }
    }

    // @param velocityX 水平方向速度
    // @param velocityY 垂直方向速度
    // @param consumed 子View是否消费fling操作
    // @return true if this parent consumed or otherwise reacted to the fling
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY,consumed);
            }
        }
        if (handled) {
            dispatchOnDependentViewChanged(true);
        }
        return handled;
    }

    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY);
            }
        }
        return handled;
    }
    //支持嵌套滚动的方向
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }
}

你会发现CoordinatorLayout收到来自NestedScrollingChild的各种回调后,都是交由需要响应的Behavior来处理,所以这样可以得出一个结论,CoordinatorLayoutBehavior的一个代理类,所以Behavior实际上也是一个NestedScrollingParent,另外结合NestedScrollingChild实现的部分来看,你很容易就能搞懂这些方法参数的实际含义;

CoordinatorLayout,Behavior和NestedScrollingParent三者关系:

 2.4.NestedScroll总结

NestedScroll的机制简版是这样的,当子View在处理滑动事件之前,先告诉自己的父View是否需要先处理这次滑动事件,父View处理完之后,告诉子View它处理了多少滑动距离,剩下的还是交给子View自己来处理;

你也可以实现这样的一套机制,父View拦截所有事件,然后分发给需要的子View来处理,然后剩余的自己来处理;但是这样就做会使得逻辑处理更复杂,因为事件的传递本来就由外向内传递到子View,处理机制是由内向外的,由子View先来处理事件本来就是遵守默认规则的;

3.CoordinatorLayout的源码解析和自定义Behavior

上面再分析NestedScrollParent接口的时候已经简单提到了CoordinatorLayout这个控件,至于这个控件是用来做什么的?CoordinatorLayout内部有个Behavior对象,这个Behavior对象可以通过外部setter或者xml中指定的方式注入到CoordinatorLayout的某个子View的LayoutParamsBehavior对象定义了特定类型的视图交互逻辑,譬如FloatingActionButtonBehavior实现类,只要FloatingActionButtonCoordinatorLayout的子View,且设置的该Behavior(默认已经设置了),那么,这个FAB就会在Snackbar出现的时候上浮,而不至于被遮挡,而这种通过定义Behavior的方式可以控制View的某一类的行为,通常会比自定义View的方式更解耦更轻便,由此可知,BehaviorCoordinatorLayout的精髓所在;

3.1Behavior解析和实例化

 简单来看看Behavior是如何从xml中解析的,通过检测xxx:behavior属性,通过全限定名或者相对路径的形式指定路径,最后通过反射来新建实例,默认的构造器是Behavior(Context context, AttributeSet attrs),如果需要配置额外的参数,可以在外部构造好Behavior并通过setter的方式注入到LayoutParams或者获取到解析好的Behavior进行额外的参数设定;

LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);

            final TypedArray a = context.obtainStyledAttributes(attrs,
                    R.styleable.CoordinatorLayout_Layout);

            //...
            mBehaviorResolved = a.hasValue(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior);
            if (mBehaviorResolved) {
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_Layout_layout_behavior));
            }
            a.recycle();

            if (mBehavior != null) {
                // If we have a Behavior, dispatch that it has been attached
                mBehavior.onAttachedToLayoutParams(this);
            }
        }

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }

        final String fullName;
        if (name.startsWith(".")) {
            // Relative to the app package. Prepend the app package name.
            fullName = context.getPackageName() + name;
        } else if (name.indexOf('.') >= 0) {
            // Fully qualified package name.
            fullName = name;
        } else {
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                    ? (WIDGET_PACKAGE_NAME + '.' + name)
                    : name;
        }

        try {
            //...
            if (c == null) {
                final Class<Behavior> clazz =
                        (Class<Behavior>) Class.forName(fullName, false, context.getClassLoader());
                c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                c.setAccessible(true);
                //...
            }
            return c.newInstance(context, attrs);
        } catch (Exception e) {
            throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
        }
    }

3.2两种关系和两种形式

View之间的依赖关系

CoordinatorLayout的子View可以扮演着两种不同角色,一种是被依赖的,而另一种则是主动寻找依赖的View,被以来的View并不会感知到自己被依赖,被依赖的View也有可能是寻找依赖的View;

这种依赖关系的建立由CoordinatorLayout#LayoutParams来指定,假设此时两个View:A和B,那么有两种情况导致依赖关系

  • A的anchor是B
  • A的behavior对B有依赖

LayoutParams中关于依赖的判断的依据的代码如下

LayoutParams        
boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
            return dependency == mAnchorDirectChild
                    || shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))
                    || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
        }

依赖判断通过两个条件判断,一个生效即可,最容易理解的根据Behavior#layoutDependsOn方法指定,例如View依赖AppBarLayout

AppBarLayout#BaseBehavior   
 @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
      // We depend on any AppBarLayouts
      return dependency instanceof AppBarLayout;
    }

另外一个可以看到是通过mAnchorDirectChild来判断,首先要知道AnchorView的ID是通过setter或者xml的anchor属性形式指定,但是为了不需要每次都根据ID通过findViewById去解析出AnchorView,所以会使用mAnchorView变量缓存好,需要注意的是这个AnchorView不可以是CoordinatorLayout,另外也不可以是当前View的一个子View,变量mAnchorDirectChild记录的就是AnchorView的所属的ViewGroup或自身(当它直接ViewParent是CoordinatorLayout的时候),关于AnchorView的作用,也可以在FAB配合AppBarLayout使用的时候,AppBarLayout会做为FAB的AnchorView,就可以在AppBarLayout打开或者收缩状态的时候显示或者隐藏FAB,自己这方面的实践比较少,在这也可以先忽略并不影响后续分析,大家感兴趣的可以通过看相关代码一探究竟;

根据这种依赖关系,CoordinatorLayout中维护了一个mDependencySortedChildren列表,里面含有所有子View,按照依赖关系排序,被依赖着排在前面,会在每次测量前重新排序,确保处理的顺序是**被依赖的View会先被measure和layout**;

@Override
    @SuppressWarnings("unchecked")
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        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);

            //按照View之间的依赖关系,存储View
            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);
                }
            }
        }

        // mChildDag.getSortedList()会返回一个按照依赖关系排序后的View集合
        // 被依赖的View排在前边,没有被依赖的在后边
        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);
    }

很明显prepareChildren()就是完成CoordinatorLayout中子View按照依赖关系的排序,被依赖的View排在前面,并将结果保存在mDependencySortedChildren中,在每次测量前都会重新排序;

mDependencySortedChildren依赖DAG排序,有向无环图(Directed Acyclic Graph, DAG)是有向图的一种,字面意思的理解就是图中没有环。常常被用来表示事件之间的驱动依赖关系,管理任务之间的调度。

3.3依赖监听

这种依赖关系确定后又有什么作用呢?当然是在主动寻找依赖的View,在其依赖的View发生变化的时候,自己能够知道啦,也就是如果CoordinatorLayout内的A依赖B,在B的大小位置等发生状态变化的时候,A可以监听到,并作出响应,CoordinatorLayout又是怎么实现的呢?

CoordinatorLayout本身注册了两种监听器,ViewTreeObserver.OnPreDrawListeneronHierarchyChangeListener,一种是在绘制的之前进行回调,一种是在子View的层级结构发生变化的时候回调,有这两种监听就可以在接收到被依赖的View变化了;

监听提供依赖的视图的位置变化

OnPreDrawListenerCoordinatorLayout绘制之前回调,因为在layout之后,所以可以很容易判断到某个View的位置是否发生的变化

    class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
        }
    }

onChildViewsChanged方法,会遍历根据依赖关系排序好的子View集合,找到位置改变了的View,并回调依赖这个View的BehavioronDependentViewChanged方法

 final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        final Rect inset = acquireTempRect();
        final Rect drawRect = acquireTempRect();
        final Rect lastDrawRect = acquireTempRect();

        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
                // Do not try to update GONE child views in pre draw updates.
                continue;
            }

            // Check child views before for anchor
            //...
            // 获取child当前的绘画区域
            getChildRect(child, true, drawRect);

            //...

            if (type != EVENT_VIEW_REMOVED) {
                // Did it change? if not continue
                getLastChildRect(child, lastDrawRect);
                //比较前后两次位置变化,位置没有发生改变就进入下次循环得了
                if (lastDrawRect.equals(drawRect)) {
                    continue;
                }
                //记录上一次View的绘画位置Rect
                recordLastChildRect(child, drawRect);
            }

            // 如果发生改变了,往后面位置中找到依赖当前View的Behavior来进行回调
            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();
                //判断后面位置指定的Behavior的View是否依赖child视图    
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    //...
                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // Otherwise we dispatch onDependentViewChanged()
                            //回调对应指定Behavior的View提示View依赖的视图child发生位置变化了
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    //...
                }
            }
        }
        //...
    }

监听提供依赖的View的添加和移除

HierarchyChangeListener在View的添加和移除都会回调

private class HierarchyChangeListener implements OnHierarchyChangeListener {
        //...
        @Override
        public void onChildViewRemoved(View parent, View child) {
            onChildViewsChanged(EVENT_VIEW_REMOVED);
            //...
        }
    }

根据情况回调Behavior#onDependentViewRemoved

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        final Rect inset = acquireTempRect();
        final Rect drawRect = acquireTempRect();
        final Rect lastDrawRect = acquireTempRect();

        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
                // Do not try to update GONE child views in pre draw updates.
                continue;
            }

            // Check child views before for anchor
            //...
            // 获取child当前的绘画区域
            getChildRect(child, true, drawRect);

            //...

            if (type != EVENT_VIEW_REMOVED) {
                // Did it change? if not continue
                getLastChildRect(child, lastDrawRect);
                //比较前后两次位置变化,位置没有发生改变就进入下次循环得了
                if (lastDrawRect.equals(drawRect)) {
                    continue;
                }
                //记录上一次View的绘画位置Rect
                recordLastChildRect(child, drawRect);
            }

            // 如果发生改变了,往后面位置中找到依赖当前View的Behavior来进行回调
            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();
                //判断后面位置指定的Behavior的View是否依赖child视图    
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    //...
                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            //回调对应指定Behavior的View提示View依赖的视图child被移除了
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // Otherwise we dispatch onDependentViewChanged()
                            //回调对应指定Behavior的View提示View依赖的视图child发生位置变化了
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    //...
                }
            }
        }
        //...
    }

3.4自定义Behavior的两种目的

我们可以按照两种目的来实现自己的Behavior,当然也可以两种都实现啦;

  • 某个View监听另一个View的状态变化,例如大小、位置、显示状态等;
  • 某个View监听CoordinatorLayout内的NestedScrollingChild的接口实现类的滑动状态;

第一种情况需要重写layoutDependsOnOndependentViewChanged方法;

第二种情况需要重写onStartNestedScrollonNestedPreScroll系列方法(上面已经提到了);

对于第一种情况,我们之前分析依赖的监听的时候响应回调细节已经说完了,Behavior只需要在onDependentViewChanged做相应的处理就好;

对于第二种情况,我们在NestedScroll的那节也已经把相关回调细节说了;

3.5CoordinatorLayout的事件传递

CoordinatorLayout并不会直接处理触摸事件,而是尽可能的先交由子View的Behavior来处理,它的onInterceptTouchEvent和onTouchEvent两个方法最终都是调用performIntercept方法,用来分发不同的事件类型分发给对应的子View的Behavior处理;

//处理拦截或者自己的触摸事件  
private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        boolean newBlock = false;

        MotionEvent cancelEvent = null;

        final int action = ev.getActionMasked();

        final List<View> topmostChildList = mTempList1;
        //在5.0以上,按照z属性来排序,以下,则是按照添加顺序或者自定义的绘制顺序来排列
        getTopSortedChildren(topmostChildList);

        // Let topmost child views inspect first
        final int childCount = topmostChildList.size();
        for (int i = 0; i < childCount; i++) {
            final View child = topmostChildList.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior b = lp.getBehavior();
            //如有一个Behavior对事件进行了拦截,就发送Cancel事件给后续的所有的Behavior;假设之前还没有Intercept发生,那么所有的事件都平等地对所有含有behavior的view进行分发,现在intercept忽然出现,那么相应的我们就要对除了Intercept的view发出Cancel;
            if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
                // Cancel all behaviors beneath the one that intercepted.
                // If the event is "down" then we don't have anything to cancel yet.
                if (b != null) {
                    if (cancelEvent == null) {
                        final long now = SystemClock.uptimeMillis();
                        cancelEvent = MotionEvent.obtain(now, now,
                                MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                    }
                    switch (type) {
                        case TYPE_ON_INTERCEPT:
                            b.onInterceptTouchEvent(this, child, cancelEvent);
                            break;
                        case TYPE_ON_TOUCH:
                            b.onTouchEvent(this, child, cancelEvent);
                            break;
                    }
                }
                continue;
            }

            if (!intercepted && b != null) {
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        intercepted = b.onInterceptTouchEvent(this, child, ev);
                        break;
                    case TYPE_ON_TOUCH:
                        intercepted = b.onTouchEvent(this, child, ev);
                        break;
                }
                if (intercepted) {
                    mBehaviorTouchView = child;    //记录当前要处理触摸事件View
                }
            }

            // Don't keep going if we're not allowing interaction below this.
            // Setting newBlock will make sure we cancel the rest of the behaviors.
            final boolean wasBlocking = lp.didBlockInteraction();
            final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
            newBlock = isBlocking && !wasBlocking;
            if (isBlocking && !newBlock) {
                // Stop here since we don't have anything more to cancel - we already did
                // when the behavior first started blocking things below this point.
                break;
            }
        }

        topmostChildList.clear();

        return intercepted;
    }

3.6小结

以上,基本可以理清CoordinatorLayout的机制,一个View如何监听到依赖View的变化,和CoordinatorLayout中的NestedScrollingChild实现NestedScroll的机制,触摸事件又是如何被Behavior拦截和处理,另外还有测量和布局我在这里并没有提及,但基本就是按照依赖关系排序,遍历子View,询问他们的Behavior是否需要处理,大家可以翻翻源码,这样可以有更深刻的体会,有了这些知识,我们基本就可以根据需要来自定义自己的Behavior了,下面也带大家来实践下我是如何用自定义的Behavior实现相关功能的;

4.CoordinatorLayout的测量、布局

简单翻一下源码即可;

CoordinatorLayout容器在测量子视图时,首先获取子视图的Behavior,确认指定Behavior子视图是否自己代理测量视图,返回true,则CoordinatorLayout容器不负责指定Behavior子视图测量工作;

CoordinatorLayout容器在布局子视图时,和指定Behavior测量子视图的逻辑类似,指定Behavior子视图代理布局子视图;

参考

Android CoordinatorLayout之源码解析 - 简书

自定义Behavior的艺术探索-仿UC浏览器主页 - 简书

使用 CoordinatorLayout 实现复杂联动效果 - 简书

透过 NestedScrollView 源码解析嵌套滑动原理 - huansky - 博客园

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android下NestedScrolling机制与CoordinatorLayout之源码分析 的相关文章