详细讲解 AppBarLayout、CoordinatorLayout 和 NestedScrollView 如何协作实现联动效果

271 阅读7分钟

一、设计理念与联动机制概述

  1. 核心组件
  • CoordinatorLayout:

    • 继承自 ViewGroup,是 AndroidX 提供的协调布局容器。
    • 通过 Behavior 机制,协调子 View 的交互(如 AppBarLayout 和 NestedScrollView 的联动)。
    • 实现 NestedScrollingParent2,支持嵌套滚动协议。
    • 源码位置:androidx.coordinatorlayout.widget.CoordinatorLayout。
  • AppBarLayout:

    • 继承自 LinearLayout,用于实现可折叠的工具栏或头部区域。
    • 配合 CoordinatorLayout.Behavior(如 AppBarLayout.Behavior),响应滑动事件,实现折叠/展开效果。
    • 源码位置:androidx.appcompat.widget.AppBarLayout。
  • NestedScrollView:

    • 继承自 ScrollView,实现 NestedScrollingParent2 和 NestedScrollingChild2,支持嵌套滚动。
    • 与 AppBarLayout 协作,通过嵌套滚动协议传递滑动事件。
    • 源码位置:androidx.core.widget.NestedScrollView。
  1. 联动效果核心原理
  • 嵌套滚动协议:

    • Android 5.0(API 21)引入的嵌套滚动机制(NestedScrollingParent 和 NestedScrollingChild),通过 NestedScrollingParentHelper 和 NestedScrollingChildHelper 协调父子 View 的滚动。
    • ViewPager2、RecyclerView 和 NestedScrollView 作为 NestedScrollingChild,发起滚动事件;CoordinatorLayout 作为 NestedScrollingParent,响应并分发事件。
  • Behavior 机制:

    • CoordinatorLayout 通过 Behavior(CoordinatorLayout.Behavior)定义子 View 的交互行为。
    • AppBarLayout 的 Behavior(AppBarLayout.Behavior)处理 NestedScrollView 的滑动事件,动态调整 AppBarLayout 的偏移量,实现折叠/展开。
  • 联动效果:

    • 当 NestedScrollView 滑动时,CoordinatorLayout 捕获滑动事件,通知 AppBarLayout 的 Behavior。
    • AppBarLayout 根据滑动距离调整其 top 位置,产生折叠、展开或吸顶效果。
  1. 典型场景
  • 折叠工具栏:NestedScrollView 向上滑动时,AppBarLayout 折叠(隐藏);向下滚动时展开。
  • 吸顶效果:AppBarLayout 内的 Toolbar 可固定在顶部,内容继续滑动。
  • 复杂交互:如与 ViewPager2、RecyclerView 配合,实现动态头部动画。

二、源码分析与实现流程

以下结合源码,详细分析 AppBarLayout、CoordinatorLayout 和 NestedScrollView 的联动实现流程。

  1. CoordinatorLayout 的核心实现

CoordinatorLayout 是联动的核心,负责协调子 View 的交互。

(1) 嵌套滚动处理

  • 接口实现:CoordinatorLayout 实现 NestedScrollingParent2:

    java

    public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {
        private final NestedScrollingParentHelper mNestedScrollingParentHelper;
        private final List<View> mNestedScrollingV2ConsumedCompat = new ArrayList<>();
    }
    
  • 关键方法:

    • onStartNestedScroll():判断是否接受嵌套滚动:

      java

      @Override
      public boolean onStartNestedScroll(View child, View target, int axes, int type) {
          boolean handled = false;
          for (int i = 0; i < getChildCount(); i++) {
              View view = getChildAt(i);
              LayoutParams lp = (LayoutParams) view.getLayoutParams();
              Behavior behavior = lp.getBehavior();
              if (behavior != null) {
                  boolean accepted = behavior.onStartNestedScroll(this, view, child, target, axes, type);
                  handled |= accepted;
              }
          }
          return handled;
      }
      
    • onNestedPreScroll():父 View 优先消耗滚动:

      java

      @Override
      public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
          int xConsumed = 0, yConsumed = 0;
          for (int i = 0; i < getChildCount(); i++) {
              View view = getChildAt(i);
              LayoutParams lp = (LayoutParams) view.getLayoutParams();
              Behavior behavior = lp.getBehavior();
              if (behavior != null) {
                  int[] tempConsumed = mNestedScrollingV2ConsumedCompat.get(i);
                  behavior.onNestedPreScroll(this, view, target, dx, dy, tempConsumed, type);
                  xConsumed += tempConsumed[0];
                  yConsumed += tempConsumed[1];
              }
          }
          consumed[0] = xConsumed;
          consumed[1] = yConsumed;
      }
      
    • 逻辑:

      • 遍历子 View,调用其 Behavior 的 onStartNestedScroll() 和 onNestedPreScroll()。
      • 如果子 View(如 AppBarLayout)的 Behavior 接受滚动(axes & SCROLL_AXIS_VERTICAL),则处理滚动事件并记录消耗量。

(2) Behavior 分发

  • CoordinatorLayout 使用 Behavior 定义子 View 的交互逻辑:

    java

    public static abstract class Behavior<V extends View> {
        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child,
                View directTargetChild, View target, int axes, int type) {
            return false;
        }
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child,
                View target, int dx, int dy, int[] consumed, int type) {}
    }
    
  • AppBarLayout 的 Behavior(AppBarLayout.Behavior)处理 NestedScrollView 的滑动事件。

  1. AppBarLayout 的核心实现

AppBarLayout 是一个垂直 LinearLayout,支持折叠和展开效果。

(1) Behavior 实现

  • AppBarLayout.Behavior:

    • 继承自 HeaderBehavior,处理滚动和 fling:

      java

      public static class Behavior extends HeaderBehavior<AppBarLayout> {
          private int mOffsetDelta;
          private int mOffsetToChildIndexOnLayout = -1;
      
          @Override
          public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
                  View directTargetChild, View target, int nestedScrollAxes, int type) {
              return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
                      && child.isLiftOnScroll();
          }
      
          @Override
          public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                  View target, int dx, int dy, int[] consumed, int type) {
              if (dy != 0) {
                  int oldOffset = getTopAndBottomOffset();
                  int newOffset = offsetChild(child, dy);
                  consumed[1] = newOffset - oldOffset;
              }
          }
      }
      
  • 核心逻辑:

    • onStartNestedScroll():检查是否为垂直滚动,且 AppBarLayout 是否支持折叠(isLiftOnScroll)。
    • onNestedPreScroll():根据滑动距离(dy)调整 AppBarLayout 的偏移量(setTopAndBottomOffset())。

(2) 偏移量管理

  • setTopAndBottomOffset():

    • 调整 AppBarLayout 的垂直位置:

      java

      boolean setTopAndBottomOffset(int offset) {
          mTotalScrollRange = -1; // 标记需要重新计算滚动范围
          if (mOffset != offset) {
              mOffset = offset;
              child.refreshDrawableState();
              return true;
          }
          return false;
      }
      
  • 偏移效果:

    • AppBarLayout 的子 View(如 Toolbar)随偏移量上移或下移,实现折叠/展开。

    • 滚动范围由 getTotalScrollRange() 计算:

      java

      public int getTotalScrollRange() {
          if (mTotalScrollRange != -1) {
              return mTotalScrollRange;
          }
          int range = 0;
          for (int i = 0; i < getChildCount(); i++) {
              View child = getChildAt(i);
              LayoutParams lp = (LayoutParams) child.getLayoutParams();
              if (lp.mScrollFlags != 0) {
                  range += child.getHeight() + lp.topMargin + lp.bottomMargin;
              }
          }
          mTotalScrollRange = range;
          return range;
      }
      
  1. NestedScrollView 的核心实现

NestedScrollView 作为内容容器,发起嵌套滚动。

(1) 嵌套滚动发起

  • 接口实现:实现 NestedScrollingChild2:

    java

    public class NestedScrollView extends ScrollView implements NestedScrollingChild2 {
        private final NestedScrollingChildHelper mChildHelper;
    
        @Override
        public boolean startNestedScroll(int axes, int type) {
            return mChildHelper.startNestedScroll(axes, type);
        }
    
        @Override
        public void dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
            mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
        }
    }
    
  • 核心逻辑:

    • startNestedScroll():通知父 View(如 CoordinatorLayout)开始嵌套滚动。
    • dispatchNestedPreScroll():将滚动距离分发给父 View,优先让父 View(如 AppBarLayout)消耗。

(2) 滚动处理

  • onTouchEvent():

    java

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mVelocityTracker.addMovement(ev);
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                int deltaY = (int) (mLastMotionY - ev.getY());
                int[] consumed = new int[2];
                dispatchNestedPreScroll(0, deltaY, consumed, null, ViewCompat.TYPE_TOUCH);
                deltaY -= consumed[1]; // 剩余滚动量
                if (deltaY != 0) {
                    scrollBy(0, deltaY);
                }
                break;
            case MotionEvent.ACTION_UP:
                int velocityY = (int) mVelocityTracker.getYVelocity();
                if (Math.abs(velocityY) > mMinimumVelocity) {
                    flingWithNestedDispatch(-velocityY);
                }
                break;
        }
        return true;
    }
    
  • 逻辑:

    • 捕获触摸事件,计算滑动距离(deltaY)。
    • 通过 dispatchNestedPreScroll() 优先让父 View(如 AppBarLayout)消耗滚动。
    • 剩余滚动量用于自身滚动(scrollBy)。
  1. 联动流程

以下是 NestedScrollView 滑动时,AppBarLayout 和 CoordinatorLayout 协作的详细流程:

  1. 用户滑动 NestedScrollView:

    • NestedScrollView 的 onTouchEvent() 捕获 ACTION_MOVE,计算 deltaY。
    • 调用 startNestedScroll(SCROLL_AXIS_VERTICAL) 通知 CoordinatorLayout。
  2. CoordinatorLayout 协调:

    • CoordinatorLayout 的 onStartNestedScroll() 遍历子 View,调用 AppBarLayout 的 Behavior.onStartNestedScroll()。
    • AppBarLayout 的 Behavior 接受垂直滚动(SCROLL_AXIS_VERTICAL)。
  3. 预滚动处理:

    • NestedScrollView 调用 dispatchNestedPreScroll(),将 deltaY 分发给 CoordinatorLayout。

    • CoordinatorLayout 调用 AppBarLayout 的 Behavior.onNestedPreScroll():

      • 计算偏移量(offsetChild()),调整 AppBarLayout 的 top 位置。
      • 消耗部分 dy(记录在 consumed[1]),实现折叠/展开效果。
  4. NestedScrollView 自身滚动:

    • NestedScrollView 使用剩余 dy 调用 scrollBy(),更新自身滚动位置。
  5. Fling 动画:

    • NestedScrollView 的 ACTION_UP 触发 fling,调用 flingWithNestedDispatch()。
    • CoordinatorLayout 和 AppBarLayout 的 Behavior 处理 fling 动画,确保平滑过渡。
  6. 动画与状态更新:

    • AppBarLayout 的 setTopAndBottomOffset() 更新折叠状态。
    • CoordinatorLayout 触发 onChildViewsChanged(),通知相关 Behavior 更新 UI。

三、优化机制

  1. 嵌套滚动协议:

    • 通过 NestedScrollingParentHelper 和 NestedScrollingChildHelper 减少事件分发开销。
    • 优先让父 View 消耗滚动,优化交互流畅性。
  2. Behavior 模块化:

    • AppBarLayout 的 Behavior 可扩展,支持自定义交互(如吸顶、渐变)。
    • CoordinatorLayout 的 Behavior 机制解耦子 View 逻辑,易于维护。
  3. 高效偏移:

    • AppBarLayout 使用 setTopAndBottomOffset() 直接调整位置,避免重绘。

    • 源码:

      java

      child.offsetTopAndBottom(offset - oldOffset);
      
  4. Fling 优化:

    • NestedScrollView 和 AppBarLayout 共享 fling 动画,协调速度:

      java

      private void flingWithNestedDispatch(int velocityY) {
          dispatchNestedFling(0, velocityY, true);
          fling(velocityY);
      }
      
  5. 状态管理:

    • AppBarLayout 支持 CollapsingToolbarLayout 的状态保存(如折叠状态):

      java

      public void setCollapsed(boolean collapsed) {
          setTopAndBottomOffset(collapsed ? -getTotalScrollRange() : 0);
      }
      

四、实际使用场景与示例

  1. 示例代码

xml

<androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="200dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <androidx.appcompat.widget.Toolbar
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />

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

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <!-- 内容 -->
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
  1. 使用场景
  • 折叠工具栏:AppBarLayout 随 NestedScrollView 滑动折叠,Toolbar 可吸顶。
  • 复杂嵌套:NestedScrollView 嵌套 RecyclerView,AppBarLayout 响应滑动。
  • 动态头部:如图片渐变、标题缩放等效果,结合 CollapsingToolbarLayout。
  1. 注意事项
  • Behavior 配置:确保 NestedScrollView 和 AppBarLayout 使用正确的 Behavior(@string/appbar_scrolling_view_behavior)。
  • 子 View 复用:NestedScrollView 嵌套 RecyclerView 时,依赖 RecyclerView 的三层缓存优化性能。
  • 性能优化:避免在 NestedScrollView 中加载超大数据集,建议使用 RecyclerView 替代。

五、总结

联动实现流程

  1. NestedScrollView 发起滚动:通过 onTouchEvent() 和 dispatchNestedPreScroll() 传递滑动事件。
  2. CoordinatorLayout 协调:调用 AppBarLayout 的 Behavior.onNestedPreScroll(),调整偏移量。
  3. AppBarLayout 响应:更新 topAndBottomOffset,实现折叠/展开效果。
  4. Fling 动画:通过 flingWithNestedDispatch() 协调父子 View 的动画。

源码级亮点

  • 嵌套滚动协议:NestedScrollingParent2 和 NestedScrollingChild2 确保高效事件分发。
  • Behavior 机制:解耦交互逻辑,AppBarLayout 的 Behavior 灵活处理滑动。
  • 偏移优化:直接调整 View 位置,减少重绘。
  • 与 AndroidX 集成:支持 CollapsingToolbarLayout、TabLayout 等现代组件。

与 ScrollView 的对比

  • ScrollView:不支持嵌套滚动,无法与 AppBarLayout 联动。
  • NestedScrollView:通过嵌套滚动协议,与 CoordinatorLayout 和 AppBarLayout 协作,实现复杂交互。