HeaderBehavior

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),而 flingRunnableprivate------这意味着同包子类(如 BaseBehavior)能直接拿到 scrollerabortAnimation()(在嵌套滚动抢占时常用),但无法直接操作 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;
}

关键点:viewOffsetHelperonLayoutChild 里才创建,因此 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(或 protectedlayoutChild),可供任意子类调用。HeaderBehavior 后续所有 setHeaderTopBottomOffset 最终都会落到 setTopAndBottomOffset

2.5 HeaderBehavior 复用父类偏移的两个对外钩子

java 复制代码
int getTopBottomOffsetForScrollingSibling() {
  return getTopAndBottomOffset();   // 默认直接返回父类 offset;子类可重写(叠加自身额外偏移)
}

此方法是 包级私有 ,是给 scroll() 取"当前真实偏移"用的扩展点。默认实现等同 getTopAndBottomOffset(),但允许子类(如处理 AppBarLayout 内部 child 额外位移的场景)覆写以提供不同的基准。


3. 拖拽手势

HeaderBehavior 重写了 onInterceptTouchEventonTouchEvent,实现"直接拖 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 分支执行清理(复位 isBeingDraggedactivePointerId,回收 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 - dydy > 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.clampandroidx.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
  • 先清旧 flingremoveCallbacks(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 FlingRunnableonFlingFinished

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)

注意 getMaxDragOffsetgetScrollRangeForDragFling 默认都基于 view.getHeight(),一个返回负、一个返回正,恰好分别用于 scroll()minOffsetfling()-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)

  • onInterceptTouchEventonTouchEvent(@Override,public)------ 子类可重写,但通常不需要。
  • 继承自 ViewOffsetBehavior 的 public 方法:setTopAndBottomOffsetgetTopAndBottomOffsetsetLeftAndRightOffsetgetLeftAndRightOffsetsetVerticalOffsetEnabled 等。

5.4 private(不可触达)

  • INVALID_POINTERisBeingDraggedactivePointerIdlastMotionYtouchSlopvelocityTrackerflingRunnableensureVelocityTracker()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 状态算
共用方法 scrollsetHeaderTopBottomOffsetsetTopAndBottomOffset 同上
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;
相关推荐
阿pin5 小时前
Android随笔-kotlin withContext
android·kotlin·withcontext
树码小子6 小时前
开启 IDEA 中的断言功能
android·java·intellij-idea
Android-Flutter7 小时前
android 动画详解
android·kotlin
小孔龙12 小时前
GraphicBuffer 跨进程共享
android
行业研究员12 小时前
Android内置GPS与腾讯地图定位技术对比分析
android·android定位·腾讯地图定位
Android-Flutter13 小时前
android WMS 详解(二)
android·kotlin
游戏开发爱好者814 小时前
App Store 上传 IPA 自动化,.p8 密钥认证与 CI/CD 接入实战
android·运维·ci/cd·小程序·uni-app·自动化·iphone
游戏开发爱好者815 小时前
iOS 推送怎么配置,APNs 推送证书、设备库与群发
android·ios·小程序·https·uni-app·iphone·webview
阿pin16 小时前
Android随笔-kotlin 高阶函数
android·kotlin·高阶函数