HeaderBehavior 源码实现文档(Material 1.10.0)
源码:
com/google/android/material/appbar/HeaderBehavior.java(约 281 行)父类参考:
com/google/android/material/appbar/ViewOffsetBehavior.java间接依赖:
com/google/android/material/appbar/ViewOffsetHelper.java
1. 类定位与继承链
java
abstract class HeaderBehavior<V extends View> extends ViewOffsetBehavior<V>
完整继承链:
CoordinatorLayout.Behavior<V>
▲
│
ViewOffsetBehavior<V> // 引入 ViewOffsetHelper,提供"绝对偏移"能力
▲
│
HeaderBehavior<V> // 抽象类:负责"位于滚动视图上方的 header"的拖拽 + fling
▲
│
( AppBarLayout.Behavior 通过 BaseBehavior<V extends AppBarLayout> 间接继承本类 )
HeaderBehavior 是 包级私有 abstract 类 (abstract class,无 public 修饰符),只能被 com.google.android.material.appbar 包内的类继承。它的语义正如类注释所写:
The Behavior for a view that sits vertically above scrolling a view. See HeaderScrollingViewBehavior.
即「垂直叠放在滚动视图上方的 header」的通用 Behavior 基类。AppBarLayout.Behavior 内部的 BaseBehavior 正是通过继承 HeaderBehavior 拿到「直接拖 header」与「fling」能力的,再叠加嵌套滑动逻辑。
类本身只声明了两个构造器(无参 + (Context, AttributeSet)),其余能力都由一组 包级私有方法 + final 方法 提供,可见性策略非常克制。
关键字段
java
private static final int INVALID_POINTER = -1;
@Nullable private Runnable flingRunnable; // fling 逐帧回调
OverScroller scroller; // 包级私有:子类(BaseBehavior)可直接访问/abort
private boolean isBeingDragged;
private int activePointerId = INVALID_POINTER;
private int lastMotionY;
private int touchSlop = -1; // 懒初始化哨兵
@Nullable private VelocityTracker velocityTracker;
注意 scroller 是 包级私有 (非 private),而 flingRunnable 是 private------这意味着同包子类(如 BaseBehavior)能直接拿到 scroller 调 abortAnimation()(在嵌套滚动抢占时常用),但无法直接操作 flingRunnable。
2. 父类 ViewOffsetBehavior 与 ViewOffsetHelper(间接行为)
HeaderBehavior 自身 不直接操作 View 的位置 ,它只负责算"目标偏移量",真正的"把 View 平移过去"由父类 ViewOffsetBehavior + ViewOffsetHelper 完成。这一层是理解所有 offset 行为的基础。
2.1 ViewOffsetBehavior 字段
java
class ViewOffsetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
private ViewOffsetHelper viewOffsetHelper;
private int tempTopBottomOffset = 0; // helper 尚未创建时的暂存
private int tempLeftRightOffset = 0;
}
关键点:viewOffsetHelper 在 onLayoutChild 里才创建,因此 setTopAndBottomOffset 可能在 helper 就绪前被调用------此时偏移会先写入 tempTopBottomOffset,等 helper 建好后回灌。这避免了"set 调用早于布局"导致偏移丢失。
2.2 onLayoutChild:保偏移不丢的核心
java
@Override
public boolean onLayoutChild(parent, child, layoutDirection) {
layoutChild(parent, child, layoutDirection); // 默认委托给 parent.onLayoutChild
if (viewOffsetHelper == null) {
viewOffsetHelper = new ViewOffsetHelper(child);
}
viewOffsetHelper.onViewLayout(); // 记录"本次 layout 后的原始 top/left"
viewOffsetHelper.applyOffsets(); // 用上次的 offsetTop/offsetLeft 重新施加偏移
// helper 建好前积攒的 temp 偏移在此一次性生效
if (tempTopBottomOffset != 0) {
viewOffsetHelper.setTopAndBottomOffset(tempTopBottomOffset);
tempTopBottomOffset = 0;
}
if (tempLeftRightOffset != 0) {
viewOffsetHelper.setLeftAndRightOffset(tempLeftRightOffset);
tempLeftRightOffset = 0;
}
return true;
}
为什么每次 layout 都要 onViewLayout() + applyOffsets()?因为 CoordinatorLayout 每次 layout 都会把子 View 重新摆回 layout 位置(view.getTop() 会回到 layout 原点),先前用 offsetTopAndBottom 施加的平移会"被冲掉"。applyOffsets() 根据缓存的 offsetTop 重新平移一次,保证 视觉偏移在多次 layout 之间不丢。
2.3 ViewOffsetHelper:绝对偏移的真正实现
java
class ViewOffsetHelper {
private final View view;
private int layoutTop; // onViewLayout 时记录的"layout 原始 top"
private int layoutLeft;
private int offsetTop; // 累计的"目标绝对偏移"
private int offsetLeft;
private boolean verticalOffsetEnabled = true;
private boolean horizontalOffsetEnabled = true;
void onViewLayout() {
layoutTop = view.getTop();
layoutLeft = view.getLeft();
}
void applyOffsets() {
// 关键:参数是"相对当前 getTop() 还差多少",而不是绝对 offset
ViewCompat.offsetTopAndBottom(view, offsetTop - (view.getTop() - layoutTop));
ViewCompat.offsetLeftAndRight(view, offsetLeft - (view.getLeft() - layoutLeft));
}
public boolean setTopAndBottomOffset(int offset) {
if (verticalOffsetEnabled && offsetTop != offset) {
offsetTop = offset;
applyOffsets();
return true; // 偏移确有变化才返回 true
}
return false;
}
// setLeftAndRightOffset 同理
}
要点:
ViewCompat.offsetTopAndBottom(view, delta)是一个 增量平移 (在现有getTop()基础上再加delta),而不是设绝对位置。applyOffsets()用offsetTop - (view.getTop() - layoutTop)把"目标绝对偏移"换算成"当前还需追加的增量"------其中view.getTop() - layoutTop是"已经施加过的偏移部分"。这一步正是 "绝对偏移语义"建立在"增量 API 之上" 的关键换算。setTopAndBottomOffset在偏移未变化或方向被禁用时返回false,调用方据此判断是否真的消费了滚动量。
2.4 ViewOffsetBehavior 对外暴露的接口
java
public boolean setTopAndBottomOffset(int offset); // helper 在则委托,否则存 temp
public boolean setLeftAndRightOffset(int offset);
public int getTopAndBottomOffset(); // = helper.offsetTop,helper 无则 0
public int getLeftAndRightOffset();
public void setVerticalOffsetEnabled(boolean);
public boolean isVerticalOffsetEnabled();
public void setHorizontalOffsetEnabled(boolean);
public boolean isHorizontalOffsetEnabled();
protected void layoutChild(parent, child, layoutDirection); // 默认委托 parent.onLayoutChild
这些都是 public(或 protected 的 layoutChild),可供任意子类调用。HeaderBehavior 后续所有 setHeaderTopBottomOffset 最终都会落到 setTopAndBottomOffset。
2.5 HeaderBehavior 复用父类偏移的两个对外钩子
java
int getTopBottomOffsetForScrollingSibling() {
return getTopAndBottomOffset(); // 默认直接返回父类 offset;子类可重写(叠加自身额外偏移)
}
此方法是 包级私有 ,是给 scroll() 取"当前真实偏移"用的扩展点。默认实现等同 getTopAndBottomOffset(),但允许子类(如处理 AppBarLayout 内部 child 额外位移的场景)覆写以提供不同的基准。
3. 拖拽手势
HeaderBehavior 重写了 onInterceptTouchEvent 与 onTouchEvent,实现"直接拖 header 跟手"。注意:它 只处理"手指直接落在 header 上"这一路径,列表的嵌套滑动走另一条路(见第 6 节)。
3.1 onInterceptTouchEvent
java
public boolean onInterceptTouchEvent(parent, child, ev) {
// 懒初始化 touchSlop(首次进入)
if (touchSlop < 0) {
touchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
}
// ------ 快速路径:已经在拖拽中 ------
if (ACTION_MOVE && isBeingDragged) {
if (activePointerId == INVALID_POINTER) return false; // 没有有效指针 → 不拦截
int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) return false;
int y = (int) ev.getY(pointerIndex);
if (Math.abs(y - lastMotionY) > touchSlop) {
lastMotionY = y;
return true; // 越过 touchSlop → 拦截,转入 onTouchEvent
}
}
// ------ DOWN:判断是否进入拖拽态 ------
if (ACTION_DOWN) {
activePointerId = INVALID_POINTER;
int x = (int) ev.getX(), y = (int) ev.getY();
isBeingDragged = canDragView(child) // ① 子类授权可拖
&& parent.isPointInChildBounds(child, x, y); // ② 按下点落在 child 范围内
if (isBeingDragged) {
lastMotionY = y;
activePointerId = ev.getPointerId(0);
ensureVelocityTracker(); // 创建 VelocityTracker
// 若 fling 仍在进行,立即打断并"抓住"视图
if (scroller != null && !scroller.isFinished()) {
scroller.abortAnimation();
return true;
}
}
}
if (velocityTracker != null) velocityTracker.addMovement(ev); // 持续喂事件给 tracker
return false; // 默认不拦截
}
逐项说明:
canDragView(child):包级私有 hook,默认return false。也就是说 基类 HeaderBehavior 默认不允许直接拖 ------必须由子类(BaseBehavior)重写为true才会进入拖拽态。parent.isPointInChildBounds(child, x, y):CoordinatorLayout 提供的工具,判定按下点是否在child的矩形内。注意用的是parent的坐标系(即ev.getX/Y是相对 parent 的)。- touchSlop 判定 :只有位移绝对值超过
scaledTouchSlop才认为"开始拖",这是 Android 通用约定,避免误触。 activePointerId/lastMotionY:多指场景下固定追踪一个主指针;lastMotionY在 DOWN 和每次合格 MOVE 时更新,作为后续dy计算基准。ensureVelocityTracker():VelocityTracker.obtain()创建(池化复用),后续在 UP 时算初速度。- 打断 fling :如果按下时 scroller 还在跑(上一次 fling 没结束),直接
abortAnimation()并拦截,让用户"接住"正在飞行的 header。
3.2 onTouchEvent
java
public boolean onTouchEvent(parent, child, ev) {
boolean consumeUp = false;
switch (ev.getActionMasked()) {
case ACTION_MOVE:
int i = ev.findPointerIndex(activePointerId);
if (i == -1) return false;
int y = (int) ev.getY(i);
int dy = lastMotionY - y; // 手指向下→dy>0→header 应向下移(offset 增大)
lastMotionY = y;
scroll(parent, child, dy, getMaxDragOffset(child), 0); // 跟手:maxDrag..0 为 offset 区间
break;
case ACTION_POINTER_UP: // 多指抬起,切换追踪指针
int newIndex = ev.getActionIndex() == 0 ? 1 : 0;
activePointerId = ev.getPointerId(newIndex);
lastMotionY = (int) (ev.getY(newIndex) + 0.5f);
break;
case ACTION_UP:
if (velocityTracker != null) {
consumeUp = true;
velocityTracker.addMovement(ev);
velocityTracker.computeCurrentVelocity(1000); // 单位 px/s
float yvel = velocityTracker.getYVelocity(activePointerId);
fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel); // min=-height, max=0
}
// $FALLTHROUGH ------ 注意此处故意向下贯通到 CANCEL 的清理逻辑
case ACTION_CANCEL:
isBeingDragged = false;
activePointerId = INVALID_POINTER;
if (velocityTracker != null) {
velocityTracker.recycle(); // 必须回收,避免泄漏
velocityTracker = null;
}
break;
}
if (velocityTracker != null) velocityTracker.addMovement(ev);
return isBeingDragged || consumeUp;
}
要点:
- ACTION_MOVE :核心是
scroll(parent, child, dy, getMaxDragOffset(child), 0)。getMaxDragOffset返回-view.getHeight()(见 4.5),故 offset 区间为[-height, 0]:0= 完全展开,-height= 完全被滚出屏幕(向上推)。dy取的是lastMotionY - y(注意方向:手指上滑→y 减小→dy>0→offset 减小→header 上移)。 - ACTION_UP :仅在
velocityTracker != null时触发 fling(即此前确实进入了拖拽态)。fling的 minOffset 为-getScrollRangeForDragFling(child),maxOffset 为0------getScrollRangeForDragFling默认返回view.getHeight()。 $FALLTHROUGH:UP 处理完后 故意穿透 到 CANCEL 分支执行清理(复位isBeingDragged、activePointerId,回收velocityTracker)。这是源码里的真实注释标记。- ACTION_CANCEL:纯粹的复位 + 回收。
- 返回值 :
return isBeingDragged || consumeUp;------ 正在拖或刚刚消费了 UP,都返回 true。 - velocityTracker.recycle() :必须回收,
VelocityTracker是池化对象,不回收会造成内存泄漏。
4. 核心方法
4.1 scroll(...) ------ final,把"增量 dy"换成"绝对 offset"
java
final int scroll(CoordinatorLayout coordinatorLayout, V header,
int dy, int minOffset, int maxOffset) {
return setHeaderTopBottomOffset(
coordinatorLayout, header,
getTopBottomOffsetForScrollingSibling() - dy, // 当前偏移 - dy = 新的目标偏移
minOffset, maxOffset);
}
final:子类不可重写。这是把"增量滚动量dy"翻译成"绝对 offset"的统一入口,所有拖拽与嵌套滚动都复用它。- 方向 :
offset_new = offset_current - dy。dy > 0(内容上滑)→ 新 offset 更小 → header 向上收。 - 返回值 :
setHeaderTopBottomOffset返回的consumed,即本次实际被消费的像素量,供嵌套滚动的"分发消费量"使用。
4.2 setHeaderTopBottomOffset ------ 3 参重载(无限区间)
java
int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset) {
return setHeaderTopBottomOffset(parent, header, newOffset,
Integer.MIN_VALUE, Integer.MAX_VALUE);
}
包级私有。把 min/max 设成整型极值,等价于"不钳制"------专供 FlingRunnable.run() 调用(fling 的钳制已由 OverScroller.fling 自己完成了)。
4.3 setHeaderTopBottomOffset ------ 5 参重载(带钳制)
java
int setHeaderTopBottomOffset(CoordinatorLayout parent, V header,
int newOffset, int minOffset, int maxOffset) {
final int curOffset = getTopAndBottomOffset(); // 父类 ViewOffsetBehavior 的当前 offset
int consumed = 0;
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
// 有滚动范围,且当前 offset 在区间内,才允许设置新值
newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
setTopAndBottomOffset(newOffset); // 委托父类(→ ViewOffsetHelper → offsetTopAndBottom)
consumed = curOffset - newOffset; // 消费量 = 变化量(带符号)
}
}
return consumed;
}
要点:
- 前置门槛 :
minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset。若minOffset == 0(区间退化为[0, MAX])直接跳过------这是源码约定,避免"无范围"情况下误改 offset。若当前 offset 已越界,也跳过(外部不应在越界态继续推)。 MathUtils.clamp(androidx.core.math.MathUtils):把newOffset钳到[minOffset, maxOffset],防止过冲。setTopAndBottomOffset(newOffset):调用父类ViewOffsetBehavior的 public 方法,最终走ViewOffsetHelper.applyOffsets()→ViewCompat.offsetTopAndBottom,完成真正的平移。consumed = curOffset - newOffset:返回"被消费的滚动量"。注意符号------若 header 被向上推(newOffset 更小),consumed > 0,与 dy 同号。
4.4 fling(...) ------ final,启动惯性滚动
java
final boolean fling(CoordinatorLayout coordinatorLayout, @NonNull V layout,
int minOffset, int maxOffset, float velocityY) {
if (flingRunnable != null) { // 取消上一个未完成的 fling
layout.removeCallbacks(flingRunnable);
flingRunnable = null;
}
if (scroller == null) {
scroller = new OverScroller(layout.getContext());
}
scroller.fling(
0, getTopAndBottomOffset(), // 起始 x=0, y=当前 offset
0, Math.round(velocityY), // x 速度 0, y 速度取整
0, 0, // x 范围 [0,0](不关心 x)
minOffset, maxOffset); // y 范围 [minOffset, maxOffset],scroller 内部自钳
if (scroller.computeScrollOffset()) { // 立即算第一帧
flingRunnable = new FlingRunnable(coordinatorLayout, layout);
ViewCompat.postOnAnimation(layout, flingRunnable);
return true;
} else {
onFlingFinished(coordinatorLayout, layout); // 速度不足以起步 → 直接收尾
return false;
}
}
要点:
final:子类不可重写 fling 启动逻辑;要定制"fling 结束行为"请重写onFlingFinished。- 先清旧 fling :
removeCallbacks(flingRunnable)+ 置 null,保证同一时刻只有一个 fling 在跑。 - OverScroller.fling :起始 y 用当前 offset;y 范围传
[minOffset, maxOffset](在onTouchEvent的 UP 里就是[-height, 0])。OverScroller内部会自动钳制最终落点,所以FlingRunnable调的是 3 参(无钳制)版本的setHeaderTopBottomOffset。 - 第一帧判定 :
computeScrollOffset()立即返回 false 表示初速度太小(被 clamp 后没动),则直接走onFlingFinished(基类 no-op,BaseBehavior重写为 snap 到顶/底)。
4.5 FlingRunnable 与 onFlingFinished
java
private class FlingRunnable implements Runnable {
private final CoordinatorLayout parent;
private final V layout;
FlingRunnable(CoordinatorLayout parent, V layout) { ... }
@Override public void run() {
if (layout != null && scroller != null) {
if (scroller.computeScrollOffset()) { // 还在滚
setHeaderTopBottomOffset(parent, layout, scroller.getCurrY()); // 3 参版(无钳制)
ViewCompat.postOnAnimation(layout, this); // 自驱动下一帧
} else {
onFlingFinished(parent, layout); // 滚完收尾
}
}
}
}
逐帧驱动:每帧取 scroller.getCurrY() 设为新 offset,并 postOnAnimation 自我续命,直到 computeScrollOffset() 返回 false。onFlingFinished 默认是 no-op:
java
void onFlingFinished(CoordinatorLayout parent, V layout) {
// no-op
}
BaseBehavior(AppBarLayout 用的)会重写它做 snap:根据当前 offset 与速度决定吸附到完全展开(0)还是完全收起(-height)。
4.6 默认 hook 实现一览
| 方法(包级私有) | 默认返回 | 语义 |
|---|---|---|
boolean canDragView(V view) |
false |
是否允许直接手指拖 header;基类默认禁止 |
int getMaxDragOffset(@NonNull V view) |
-view.getHeight() |
拖拽时 offset 的下界(向上最多推到完全离屏) |
int getScrollRangeForDragFling(@NonNull V view) |
view.getHeight() |
fling 的可滚动范围(取正数,fling 时取负作 minOffset) |
注意 getMaxDragOffset 与 getScrollRangeForDragFling 默认都基于 view.getHeight(),一个返回负、一个返回正,恰好分别用于 scroll() 的 minOffset 与 fling() 的 -minOffset。
4.7 ensureVelocityTracker()(private)
java
private void ensureVelocityTracker() {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
}
}
仅一处调用:onInterceptTouchEvent 的 DOWN 分支进入拖拽态时。
5. 可见性与与扩展点总览
下表是 HeaderBehavior 全部成员的真实修饰符(重要:基类默认几乎不开放任何对外可重写点,所有"行为差异"都通过包级私有 hook 注入):
5.1 final(禁止重写)
| 方法 | 可见性 | 说明 |
|---|---|---|
scroll(...) |
final(包级私有) |
增量 dy → 绝对 offset 的统一入口 |
fling(...) |
final(包级私有) |
启动 OverScroller + FlingRunnable |
5.2 包级私有 hook(仅同包子类可重写)
| 方法 | 默认实现 | 子类(BaseBehavior)典型重写 |
|---|---|---|
setHeaderTopBottomOffset(parent, header, offset) |
转 5 参 | 一般不重写 |
setHeaderTopBottomOffset(... 5 参) |
钳制 + 委托父类 + 返回 consumed | 一般不重写 |
getTopBottomOffsetForScrollingSibling() |
返回 getTopAndBottomOffset() |
子类可叠加额外偏移 |
onFlingFinished(parent, layout) |
no-op | 重写为 snap 吸附 |
canDragView(V) |
false |
重写为 true(开启直接拖) |
getMaxDragOffset(V) |
-view.getHeight() |
视情况调整 |
getScrollRangeForDragFling(V) |
view.getHeight() |
视情况调整 |
5.3 public / @Override(来自父类或 Behavior)
onInterceptTouchEvent、onTouchEvent(@Override,public)------ 子类可重写,但通常不需要。- 继承自
ViewOffsetBehavior的 public 方法:setTopAndBottomOffset、getTopAndBottomOffset、setLeftAndRightOffset、getLeftAndRightOffset、setVerticalOffsetEnabled等。
5.4 private(不可触达)
INVALID_POINTER、isBeingDragged、activePointerId、lastMotionY、touchSlop、velocityTracker、flingRunnable、ensureVelocityTracker()、FlingRunnable内部类。
5.5 包级私有字段
OverScroller scroller------同包子类(如BaseBehavior)可直接访问,用于在嵌套滚动场景下abortAnimation()抢占 fling。
设计含义:由于几乎所有 hook 都是包级私有,
HeaderBehavior实际上是"对包外封闭、对包内可塑"的基类 。第三方想自定义 header 行为不能直接继承它(包外不可见),只能在 Material 包内通过BaseBehavior这类桥接子类扩展。这保证了 fling/scroll 的统一语义不被任意破坏。
6. 与嵌套滑动的关系(两条路径)
HeaderBehavior 本身 不实现任何 NestedScrollingParent 回调 (onStartNestedScroll / onNestedPreScroll / onNestedScroll / onStopNestedScroll / onNestedPreFling / onNestedFling 都不在本类里)。这些回调由它的子类 BaseBehavior(即 AppBarLayout.Behavior 的核心)实现。
因此存在两条独立的"滚动 header"路径:
路径 A:直接拖 header(本类负责)
手指落在 header 上
│
▼
onInterceptTouchEvent (HeaderBehavior)
│ canDragView && isPointInChildBounds && touchSlop
▼
onTouchEvent (HeaderBehavior)
│
├─ ACTION_MOVE → scroll(parent, child, dy, -height, 0)
│ → setHeaderTopBottomOffset(... 5 参) ──┐
│ │
└─ ACTION_UP → fling(parent, child, -height, 0, yvel) │
→ FlingRunnable.run() │
→ setHeaderTopBottomOffset(... 3 参) ──┤
▼
setTopAndBottomOffset (ViewOffsetBehavior)
│
▼
ViewOffsetHelper.applyOffsets
│
▼
ViewCompat.offsetTopAndBottom
路径 B:列表嵌套滑动(子类 BaseBehavior 负责)
RecyclerView/ NestedScrollView 触发嵌套滚动
│
▼
BaseBehavior.onNestedPreScroll / onNestedScroll (HeaderBehavior 不参与)
│
▼
同样调用 scroll(...) ←── 因为 scroll 是 final 且包级私有,同包子类直接复用
│
▼
setHeaderTopBottomOffset → setTopAndBottomOffset → ViewOffsetHelper ...
两条路径 共用 的统一入口是 final scroll(...) 与 setHeaderTopBottomOffset(...)。这就是为什么本类把这两个方法做成 final:保证无论"手指直接拖"还是"列表嵌套滚动",最终改变 header 位置的语义、钳制区间、返回的 consumed 计算都完全一致。差异只在于 触发时机与 dy 的来源:
| 维度 | 直接拖(路径 A) | 嵌套滚动(路径 B) |
|---|---|---|
| 触发者 | onTouchEvent |
onNestedPreScroll / onNestedScroll |
| dy 来源 | lastMotionY - y(手指位移) |
子 View 上报的 scroll[1] |
| 区间 | [-height, 0](getMaxDragOffset、0) |
由 BaseBehavior 根据 AppBarLayout 状态算 |
| 共用方法 | scroll → setHeaderTopBottomOffset → setTopAndBottomOffset |
同上 |
| fling | onTouchEvent.UP 调本类 fling |
onNestedPreFling / onNestedFling 由子类自行处理(仍可复用本类 final fling) |
这种"触摸一条路、嵌套滑动一条路,最终在 scroll/setHeaderTopBottomOffset 汇合"的设计,正是 Material AppBar 既能响应"直接拽顶栏"又能响应"列表里上滑"的根本机制------而 HeaderBehavior 只承包了前者 + 提供汇合点。
附:方法签名速查(按源码出现顺序)
java
// 构造
public HeaderBehavior()
public HeaderBehavior(Context context, AttributeSet attrs)
// 触摸(@Override)
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev)
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev)
// offset 设置(包级私有)
int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset)
int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset, int minOffset, int maxOffset)
int getTopBottomOffsetForScrollingSibling()
// final:滚动 / fling 入口
final int scroll(CoordinatorLayout coordinatorLayout, V header, int dy, int minOffset, int maxOffset)
final boolean fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset, int maxOffset, float velocityY)
// hook(包级私有,默认实现)
void onFlingFinished(CoordinatorLayout parent, V layout) // no-op
boolean canDragView(V view) // false
int getMaxDragOffset(@NonNull V view) // -view.getHeight()
int getScrollRangeForDragFling(@NonNull V view) // view.getHeight()
// private
private void ensureVelocityTracker()
private class FlingRunnable implements Runnable // 逐帧驱动
字段速查:
java
private static final int INVALID_POINTER = -1;
@Nullable private Runnable flingRunnable;
OverScroller scroller; // 包级私有
private boolean isBeingDragged;
private int activePointerId = INVALID_POINTER;
private int lastMotionY;
private int touchSlop = -1;
@Nullable private VelocityTracker velocityTracker;