CoordinatorLayout 学习(二) - RecyclerView和AppBarLayout的联动分析

  咱们都知道,若是想要使用CoordinatorLayout实现折叠布局,只有靠AppBarLayout才会生效。可是咱们不由有一个疑问,就是为何AppBarLayout可以与RecyclerView联动,它是怎么知道RecyclerView上滑仍是下滑的呢?这是本文分析的一个重点。   本文参考资料:android

  1. 针对 CoordinatorLayout 及 Behavior 的一次细节较真
  2. Android 源码分析 - 嵌套滑动机制的实现原理

  因为联动机制是创建在嵌套滑动的基础上,因此在阅读本文以前,建议熟悉一下Android中嵌套滑动的原理,有兴趣的同窗也能够参考我上面的文章。   本文打算采用由浅入深的方式来介绍联动机制,分别包括以下内容:数组

  1. CoordinatorLayout的分析
  2. Behavior的分析

1. CoordinatorLayout的分析

  在这里,咱们先分析一下CoordinatorLayout总体结构,包括三大流程,以及Behavior的相关调用。咱们都知道,在CoordinatorLayout中,Behavior是做为一个插件角色存在的,因此咱们有必要分析一下,CoordinatorLayout是怎么使用这个插件。熟悉插件的整个流程以后,后续咱们在自定义Behavior时就很是容易了。bash

(1). CoordinatorLayout的三大流程

  CoordinatorLayout的measure过程相较于其余View来讲,仍是稍微有一点特殊性。CoordinatorLayout做为协调者布局,天然须要处理各个View的依赖关系,全部View的依赖关系造成了图的数据结构,所以每一个View测量和布局均可能会受到其余View的影响,因此先测量哪些View,后测量哪些View,这里面须要有特殊的要求,不能经过简单的线性规则来进行。   所以,CoordinatorLayout的measure过程先要对图进行拓补排序,获得一个线性的数列,而后才能进行下面的操做。咱们先来看看CoordinatorLayoutonMeasure方法:数据结构

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 1. 获得一个图mChildDag,其中存储的是View之间的依赖关系;
        // 同时,还获得一个拓补排序的数组。
        prepareChildren();
        ensurePreDrawListener();
        // 测量每一个View
    }
复制代码

  整个过程咱们能够将他分析两步:app

  1. 构造依赖关系图,经过拓补排序获得一个数组。
  2. 根据拓补排序获得的数组顺序,来测量每一个View。

  在这个过程当中,咱们能够发现了Behavior的影子,咱们来看看代码:ide

final Behavior b = lp.getBehavior();
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }
复制代码

  从上面的代码中,咱们能够发现,View会尝试将测量工做交付给它的Behavior,若是Behavior不测量,而后再调用onMeasureChild方法进行测量,这样作什么好处呢?有一个很大的特色就是Behavior的高扩展性,在一些特殊的交互下,这些都是必须的。   这里我举一个例子,如图: 源码分析

  上图的布局很是的简单,这里就直接贴代码:

<androidx.coordinatorlayout.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/coordinator"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true">

        <View
                android:id="@+id/view"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="#5FF"
                android:minHeight="50dp"
                app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"/>

        <View
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="#500"
                android:minHeight="50dp"/>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
复制代码

  效果很是的明显,就是AppBarLayout第一个View会折叠,可是第二个View不会折叠,那么这个就影响到RecyclerView的测量了,正常来讲RecyclerView的高度应该等于CoordinatorLayout高度减去第二个View的高度,由于第二个View始终在屏幕当中。同理,若是AppBarLayout只有一个View,同时这个View还能折叠,那么RecyclerView的高度又不同了。像这种不固定的测量规则,交给每一个View的Behavior是最好的。   同理,布局阶段也是如此,首先会交给Behavior尝试着布局,而后CoordinatorLayout再布局,这里就不详细介绍了。布局

(2).事件的协调

  CoordinatorLayout被定义为协调者布局,天然要起到协调的做用,那么它在哪里就行协调的呢?最大的体现就是,将子View传递上来的嵌套滑动事件进行分发。我总结一下相关方法:动画

  1. 嵌套事件开始,会回调onStartNestedScroll方法。
  2. 嵌套滑动开始,会回调onNestedPreScroll方法。
  3. 嵌套滑动结束,会回调onNestedScroll方法。
  4. 嵌套滑动的Fling开始,会回调onNestedPreFling方法。
  5. 嵌套滑动的Fling结束,会回调onNestedFling方法。

  而CoordinatorLayout方法是怎么进行协调的呢?在每一个方法的实现里面,都经过每一个View的Behavior来分发,每一个Behavior在根据实际状况判断是否消费,消费多少。   咱们在自定义Behavior时,还有一个问题存在。就是若是咱们使用的自定义View,而后经过一个特殊的方法来滑动该View,在CoordinatorLayout里面将该View做为依赖的View都能随之移动,这种交互是怎么实现的呢?在这种状况下,咱们根本不是嵌套滑动来响应的,而是经过一个OnPreDrawListener接口来实现的,这个接口在View执行onDraw方法以前被回调。同理,在这种状况下,只能实现联动,不能实现更多复杂的UI交互。ui

2.Behavior的分析

  分析Behavior时,咱们先来看看它的基本结构,看看它有哪些方法,而且调用时机是什么。

方法名 做用或者调用时机
layoutDependsOn 判断两个是否存在依赖关系。
onDependentViewChanged 当一个View发生变化(包括位置变化等变化)时,依赖其的View的Behavior都会回调这个方法。
onDependentViewRemoved 当一个View被移除时,依赖其的View的Behavior都会回调这个方法。

  Behavior比较经常使用的方法就是如上的,其实还有嵌套滑动一些列的方法,这里就过多的解释。   单纯的看基类天然不能深刻理解这个类使用方式,咱们来看看它的实现类,主要是从两个方面来分析:

  1. AppBarLayout的几个Behavior
  2. RecyclerView经常使用的ScrollingViewBehavior

(1). AppBarLayout的Behavior分析

  AppBarLayoutBehavior是一个复杂的继承关系,咱们先来看看相关类图:

  整个继承关系如上类图,每一个类都负责其中一部分的功能,咱们来看看:

类名 做用
ViewOffsetBehavior ViewOffsetBehavior的内部,定义了两个方法,分别是setTopAndBottomOffsetsetLeftAndRightOffset,主要用来改变某个View的位置。
HeaderBehavior HeaderBehavior中,主要是实现了两个事件分发相关的方法。在这个类里面,主要处理AppBarLayout自己的事件,好比说,手指在AppBarLayout上面滑动。在这个类里面,有一个很是恶心的设计,就是若是在AppBarLayout上面Fling的话,会将全部的Fling吃掉,不会传递到RecyclerView上面去。我我的感受,Google爸爸的这个设计有问题,待会详细解释一下。
BaseBeHavior BaseBehavior中,主要是实现了嵌套滑动的相关方法。

  AppBarLayoutBehavior整个结构差很少介绍清楚了,下面我来解释一下,为何我以为HeaderBehavior的设计有问题。

首先,我以为不该该多出来HeaderBehavior这一层。HeaderBehavior主要做用是用来处理AppBarLayout的事件(传统事件),将事件处理放在HeaderBehavior里面有一个很大的缺陷,就是今后之后,AppBarLayout的子View不支持嵌套滑动,由于在AppBarLayout这一层就断了;其次,就是有一个很大的问题,Fling事件在HeaderBehavior里面所有消耗了,原本能够将未消耗的Fling事件传递给RecyclerView的,可是这样的设计却很难将未消耗的Fling传递出去。 个人建议是将这部分事件方法在AppBarLayout内部实现,其中既能保证嵌套滑动不断层,又能保证将未消耗的Fling事件传递到它的Parent中去。

  在这里,我重点分析HeaderBehaviorBaseBeHavior

(A). HeaderBehavior

  HeaderBehavior主要是对AppBarLayout的事件进行处理,这里咱们主要看fling事件,看看这里为何不能将fling事件传递给RecyclerView

case MotionEvent.ACTION_UP:
        if (velocityTracker != null) {
          velocityTracker.addMovement(ev);
          velocityTracker.computeCurrentVelocity(1000);
          float yvel = velocityTracker.getYVelocity(activePointerId);
          fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
        }
复制代码

  核心关键点就在fling方法的第二个参数和第三个参数,分别表示fling的最小距离和最大距离。由于最大距离是0,因此一旦AppBarLayout滑出屏幕,fling就中止了。   针对这个问题,有不少解决办法,本文先不作描述,后续我会专门的文章来解决这个问题。

(B). BaseBeHavior

  BaseBeHavior的做用是主要两个:

  1. 处理AppBarLayout的嵌套滑动。
  2. 负责AppBarLayout的测量和布局。

  这里专门分析嵌套滑动,不对测量和布局作分析,由于比较简单。在分析以前,咱们先来看AppBarLayout几个方法:

方法 做用或者调用时机
getDownNestedPreScrollRange 计算AppBarLayout能在RecyclerView向下滑动以前,能提早向下滑动的距离。很是直观的感觉是,一个View设置了SCROLL_FLAG_ENTER_ALWAYS时,当RecyclerView向下滑动时,该View首先向下滑动。该方法返回的值表示该View能向下滑动多少。
getUpNestedPreScrollRange 做用于getDownNestedPreScrollRange方法差很少,就是它表示向上能滑动的距离。
getDownNestedScrollRange 计算当RecyclerView滑动到顶部以后,AppBarLayout能向下滑动的距离。很是直观的感觉是,一个View设置了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED时,当RecyclerView滑动到顶部以后继续滑动时,此时该View会向下滑动。该方法返回的值表示该View能向下滑动多少。
getTotalScrollRange 该方法表示AppBarLayout能滑动的总距离,不区分方向。

  BaseBeHavior主要实现了嵌套滑动的onStartNestedScrollonNestedPreScrollonNestedScroll``onStopNestedScroll这几个方法。接下来,咱们来一一分析。   首先,咱们来看看onStartNestedScroll方法:

@Override
    public boolean onStartNestedScroll(
        CoordinatorLayout parent,
        T child,
        View directTargetChild,
        View target,
        int nestedScrollAxes,
        int type) {
      // Return true if we're nested scrolling vertically, and we either have lift on scroll enabled // or we can scroll the children. final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && (child.isLiftOnScroll() || canScrollChildren(parent, child, directTargetChild)); if (started && offsetAnimator != null) { // Cancel any offset animation offsetAnimator.cancel(); } // A new nested scroll has started so clear out the previous ref lastNestedScrollingChildRef = null; // Track the last started type so we know if a fling is about to happen once scrolling ends lastStartedType = type; return started; } 复制代码

  这个方法表示意思很是的简单,就是判断AppBarLayout是否须要处理嵌套滑动,其中判断条件分别是,滑动方向是垂直滑动,其次此时还有空间能够滑动。   而后,咱们再来看看onNestedPreScroll方法:

@Override
    public void onNestedPreScroll(
        CoordinatorLayout coordinatorLayout,
        T child,
        View target,
        int dx,
        int dy,
        int[] consumed,
        int type) {
      if (dy != 0) {
        int min;
        int 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;
        }
        if (min != max) {
          consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
        }
      }
      if (child.isLiftOnScroll()) {
        child.setLiftedState(child.shouldLift(target));
      }
    }
复制代码

   onNestedPreScroll方法要分为两种状况:1. RecyclerView向下滑动;2.RecyclerVIew向上滑动。这两种状况根据不一样的Flag,计算可以滑动的距离。   再次,就是onNestedScroll方法:

@Override
    public void onNestedScroll(
        CoordinatorLayout coordinatorLayout,
        T child,
        View target,
        int dxConsumed,
        int dyConsumed,
        int dxUnconsumed,
        int dyUnconsumed,
        int type,
        int[] consumed) {
      if (dyUnconsumed < 0) {
        // If the scrolling view is scrolling down but not consuming, it's probably be at // the top of it's content
        consumed[1] =
            scroll(coordinatorLayout, child, dyUnconsumed, -child.getDownNestedScrollRange(), 0);
      }
    }
复制代码

  这个方法的调用,只须要考虑到一种状况---RecyclerView向上滑动滑动,而且滑到了顶部,此时设置了SCROLL_FLAG_EXIT_UNTIL_COLLAPSEDFlag的View该滑动了。   最后就是onStopNestedScroll方法:

@Override
    public void onStopNestedScroll(
        CoordinatorLayout coordinatorLayout, T abl, View target, int type) {
      // onStartNestedScroll for a fling will happen before onStopNestedScroll for the scroll. This
      // isn't necessarily guaranteed yet, but it should be in the future. We use this to our // advantage to check if a fling (ViewCompat.TYPE_NON_TOUCH) will start after the touch scroll // (ViewCompat.TYPE_TOUCH) ends if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) { // If we haven't been flung, or a fling is ending
        snapToChildIfNeeded(coordinatorLayout, abl);
        if (abl.isLiftOnScroll()) {
          abl.setLiftedState(abl.shouldLift(target));
        }
      }

      // Keep a reference to the previous nested scrolling child
      lastNestedScrollingChildRef = new WeakReference<>(target);
    }
复制代码

  onStopNestedScroll方法主要是对设置FLAG_SNAP的View作动画。   到这里,咱们发现一个问题,那就是BaseBeHavior没有重写Fling相关方法,可是实际状况是AppBarLayout能成功响应RecyclerView的Fling事件,这个是怎么实现的呢?   最初,我觉得是BaseBehavior会监听RecyclerView的位置变化,经过onDependentViewChanged方法来响应Fling事件,结果发现BaseBehavior根本没有实现这个方法,那BaseBehavior方法是怎么实现的呢?   这个问题须要从RecyclerViewViewFlinger找答案。对于不熟悉RecyclerView的同窗来讲,我来解释一下,ViewFlinger究竟是什么。ViewFlinger主要是用来出来RecyclerView的Fling事件的。若是有同窗对他感兴趣的话,能够参考个人文章:RecyclerView 源码分析(二) - RecyclerView的滑动机制。在ViewFlinger中有以下一段代码:

if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null,
                        TYPE_NON_TOUCH)) {
                    unconsumedX -= mReusableIntPair[0];
                    unconsumedY -= mReusableIntPair[1];
                }
复制代码

  从这段代码里面,咱们能够发现,RecyclerView在Fling期间也会调用dispatchNestedPreScroll方法,从而调用到BaseBeHavioronNestedPreScroll方法,因此onNestedPreScroll方法会处理两部分的滑动距离,包括正常滑动和Fling滑动。

(2).RecyclerView的Behavior分析

  RecyclerViewBehavior继承结构与AppBarLayout的相似,咱们来看看类图:

  这其中, HeaderScrollingViewBehaviorScrollingViewBehavior方法含义以下:

类名 做用
HeaderScrollingViewBehavior 重写了onMeasureChild方法和onLayoutChild方法,主要负责RecyclerView的测量和布局。
ScrollingViewBehavior 重写了layoutDependsOn方法和onDependentViewChanged方法。主要是负责RecyclerViewAppBarLayout联动。

  接下来,咱们一一的来分析。

(A).HeaderScrollingViewBehavior

  在这里,咱们重点关注HeaderScrollingViewBehavior测量时如何考虑到AppBarLayout的有效高度,具体代码以下:

int height = availableHeight + getScrollRange(header);
        int headerHeight = header.getMeasuredHeight();
        if (shouldHeaderOverlapScrollingChild()) {
          child.setTranslationY(-headerHeight);
        } else {
          height -= headerHeight;
        }
复制代码

  咱们发现,在计算RecyclerView的高度时,还加上了AppBarLayout的能够滑动的距离。也就是说,当咱们首次进入界面时,表面上看RecyclerView布满屏幕,其实还有一部分在屏幕呢。   一样的,布局也是考虑到AppBarLayout的,这里就不分析了。

(B). ScrollingViewBehavior

  ScrollingViewBehavior主要负责RecyclerViewAppBarLayout的联动,关键代码在于onDependentViewChanged方法:

@Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
      offsetChildAsNeeded(child, dependency);
      updateLiftedStateIfNeeded(child, dependency);
      return false;
    }
复制代码

  具体的实现这里就不分析了,很是的简单。

3. 总结

  到这里,本文的介绍结束了,这里作本文的内容作一个简单的总结。

  1. CoordinatorLayout在测量阶段,会生成一个View的依赖图,而后对这个依赖图进行拓补排序获得一个数组,测量和layout的顺序都依据一个数组的。
  2. CoordinatorLayout测量和布局View的工做首先会交给每一个View的Behavior,若是不处理才本身处理。
  3. AppBarLayoutBehavior分为三层,分别是:ViewOffsetBehavior,方便改变View的位置;HeaderBehavior用来处理AppBarLayout自身的事件;BaseBeHavior用来处理嵌套滑动的事件。RecyclerViewBehavior也分为三层:第一层与AppBarLayout的同样;HeaderScrollingViewBehavior负责RecyclerView的测量和布局;ScrollingViewBehavior处理RecyclerViewAppBarLayout的联动。

  若是不出意外的话,下篇文章我将介绍怎么自定义Behavior和处理AppBarLayout的fling事件。

相关文章
相关标签/搜索