一、设计理念与联动机制概述
- 核心组件
-
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。
- 联动效果核心原理
-
嵌套滚动协议:
- 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 位置,产生折叠、展开或吸顶效果。
- 典型场景
- 折叠工具栏:NestedScrollView 向上滑动时,AppBarLayout 折叠(隐藏);向下滚动时展开。
- 吸顶效果:AppBarLayout 内的 Toolbar 可固定在顶部,内容继续滑动。
- 复杂交互:如与 ViewPager2、RecyclerView 配合,实现动态头部动画。
二、源码分析与实现流程
以下结合源码,详细分析 AppBarLayout、CoordinatorLayout 和 NestedScrollView 的联动实现流程。
- 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 的滑动事件。
- 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; }
-
- 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)。
- 联动流程
以下是 NestedScrollView 滑动时,AppBarLayout 和 CoordinatorLayout 协作的详细流程:
-
用户滑动 NestedScrollView:
- NestedScrollView 的 onTouchEvent() 捕获 ACTION_MOVE,计算 deltaY。
- 调用 startNestedScroll(SCROLL_AXIS_VERTICAL) 通知 CoordinatorLayout。
-
CoordinatorLayout 协调:
- CoordinatorLayout 的 onStartNestedScroll() 遍历子 View,调用 AppBarLayout 的 Behavior.onStartNestedScroll()。
- AppBarLayout 的 Behavior 接受垂直滚动(SCROLL_AXIS_VERTICAL)。
-
预滚动处理:
-
NestedScrollView 调用 dispatchNestedPreScroll(),将 deltaY 分发给 CoordinatorLayout。
-
CoordinatorLayout 调用 AppBarLayout 的 Behavior.onNestedPreScroll():
- 计算偏移量(offsetChild()),调整 AppBarLayout 的 top 位置。
- 消耗部分 dy(记录在 consumed[1]),实现折叠/展开效果。
-
-
NestedScrollView 自身滚动:
- NestedScrollView 使用剩余 dy 调用 scrollBy(),更新自身滚动位置。
-
Fling 动画:
- NestedScrollView 的 ACTION_UP 触发 fling,调用 flingWithNestedDispatch()。
- CoordinatorLayout 和 AppBarLayout 的 Behavior 处理 fling 动画,确保平滑过渡。
-
动画与状态更新:
- AppBarLayout 的 setTopAndBottomOffset() 更新折叠状态。
- CoordinatorLayout 触发 onChildViewsChanged(),通知相关 Behavior 更新 UI。
三、优化机制
-
嵌套滚动协议:
- 通过 NestedScrollingParentHelper 和 NestedScrollingChildHelper 减少事件分发开销。
- 优先让父 View 消耗滚动,优化交互流畅性。
-
Behavior 模块化:
- AppBarLayout 的 Behavior 可扩展,支持自定义交互(如吸顶、渐变)。
- CoordinatorLayout 的 Behavior 机制解耦子 View 逻辑,易于维护。
-
高效偏移:
-
AppBarLayout 使用 setTopAndBottomOffset() 直接调整位置,避免重绘。
-
源码:
java
child.offsetTopAndBottom(offset - oldOffset);
-
-
Fling 优化:
-
NestedScrollView 和 AppBarLayout 共享 fling 动画,协调速度:
java
private void flingWithNestedDispatch(int velocityY) { dispatchNestedFling(0, velocityY, true); fling(velocityY); }
-
-
状态管理:
-
AppBarLayout 支持 CollapsingToolbarLayout 的状态保存(如折叠状态):
java
public void setCollapsed(boolean collapsed) { setTopAndBottomOffset(collapsed ? -getTotalScrollRange() : 0); }
-
四、实际使用场景与示例
- 示例代码
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>
- 使用场景
- 折叠工具栏:AppBarLayout 随 NestedScrollView 滑动折叠,Toolbar 可吸顶。
- 复杂嵌套:NestedScrollView 嵌套 RecyclerView,AppBarLayout 响应滑动。
- 动态头部:如图片渐变、标题缩放等效果,结合 CollapsingToolbarLayout。
- 注意事项
- Behavior 配置:确保 NestedScrollView 和 AppBarLayout 使用正确的 Behavior(@string/appbar_scrolling_view_behavior)。
- 子 View 复用:NestedScrollView 嵌套 RecyclerView 时,依赖 RecyclerView 的三层缓存优化性能。
- 性能优化:避免在 NestedScrollView 中加载超大数据集,建议使用 RecyclerView 替代。
五、总结
联动实现流程
- NestedScrollView 发起滚动:通过 onTouchEvent() 和 dispatchNestedPreScroll() 传递滑动事件。
- CoordinatorLayout 协调:调用 AppBarLayout 的 Behavior.onNestedPreScroll(),调整偏移量。
- AppBarLayout 响应:更新 topAndBottomOffset,实现折叠/展开效果。
- Fling 动画:通过 flingWithNestedDispatch() 协调父子 View 的动画。
源码级亮点
- 嵌套滚动协议:NestedScrollingParent2 和 NestedScrollingChild2 确保高效事件分发。
- Behavior 机制:解耦交互逻辑,AppBarLayout 的 Behavior 灵活处理滑动。
- 偏移优化:直接调整 View 位置,减少重绘。
- 与 AndroidX 集成:支持 CollapsingToolbarLayout、TabLayout 等现代组件。
与 ScrollView 的对比
- ScrollView:不支持嵌套滚动,无法与 AppBarLayout 联动。
- NestedScrollView:通过嵌套滚动协议,与 CoordinatorLayout 和 AppBarLayout 协作,实现复杂交互。