SuperAPI AI 工具集

collapsing-parallax-toolbar

前端与建站

查看项目

更新于 10-17 16:16

技能介绍

用途

这是一个用 Jetpack Compose 构建折叠视差工具栏(collapsing parallax toolbar)的实现方案。核心结构是:五个同级组件放在同一个 Box 中,其中四个读取同一个 ScrollState 且都不拥有它,第五个完全不读取滚动状态,而是由工具栏向上回报的布尔值驱动。

val scroll: ScrollState = rememberScrollState(0)
Box(modifier) {
    Header(scroll, headerHeightPx, imageUrl, backgroundColor, /* … */)   // artwork, parallax + fade
    Body(scroll, headerHeight) { content(color) }                        // the only scrollable
    Toolbar(scroll, headerHeightPx, toolbarHeightPx, onShow = { … })     // pinned bar, appears late
    Title(scroll, title, headerHeight, toolbarHeight)                    // travels between the two
    AnimatedVisibility(showBackButton) { /* floating back button */ }
}

主要能力

  • 只有 Body 可滚动,它用 Spacer 预留头部空间,而不是把头部包含在滚动容器内:
Column(modifier.verticalScroll(scroll)) {
    Spacer(Modifier.height(headerHeight))
    Box(Modifier.background(Color.Black)) { content() }
}
  • 头部完全位于可滚动区域之外,由 graphics layer 移动,因此一次滚动只花费一次绘制:
.graphicsLayer {
    translationY = -scroll.value.toFloat() / 2f     // half the content's rate — the parallax
    alpha = (-1f / headerHeightPx) * scroll.value + 1
}
  • 头部自身高度来自窗口形状而非平台:高大于宽时使用正方形框(窗口宽度),否则使用视口高度的一半,两者下限均为 250.dp。只有正方形情形才请求正方形源图,因为只有那里源图恰好填满框。

使用方式与陷阱

资料给出了若干实现要点:

  1. 翻转点用无 key 的 remember 记忆,会在首次组合时冻结。headerHeightPx 由窗口尺寸推导并在 resize 时变化,但无 key 的 remember 会在重组中保留旧值,导致固定栏出现偏早或偏晚。应加 key:remember(headerHeightPx, toolbarHeightPx)。旁边的 derivedStateOf 正确地保持无 key。

  2. 两个返回入口一次只出现一个。浮动返回按钮与固定栏的导航图标是不同组件,翻转时在两者之间交接:工具栏向上回报自身可见性(onShow = { show -> showBackButton = !show }),浮动按钮在栏动画进入时动画退出。两者必须由同一个派生布尔值驱动。

  3. 两段式标题路径是二次贝塞尔曲线,其中点被共享。每个轴都是对同一 fraction 的三次 lerp:

val collapseFraction = (scroll.value / (headerHeight.toPx() - toolbarHeight.toPx())).coerceIn(0f, 1f)
val titleYFirst  = lerp(headerHeight - titleHeightPx.toDp() - paddingMedium, headerHeight / 2, collapseFraction)
val titleYSecond = lerp(headerHeight / 2, toolbarHeight / 2 - titleHeightPx.toDp() / 2, collapseFraction)
val titleY       = lerp(titleYFirst, titleYSecond, collapseFraction)

lerp(lerp(A,B,t), lerp(B,C,t), t) 是 de Casteljau 构造法:从 A 到 C 的二次贝塞尔曲线,B 为控制点。若两个中间项不一致,控制点会移到它们的平均值,路径仍然平滑但不再经过预期的路径点,症状是标题沿着一条没人画出的线移动。应让中间项保持为被两段共同读取的单一表达式。

  1. 缩放标题需要补偿起始内边距。scaleX/scaleY 围绕元素中心缩放,标题向栏收缩时左边缘会向内收进其损失宽度的一半:
val titleExtraStartPadding = titleWidthPx.toDp() * (1 - scaleXY.value) / 2f
  1. 标题自测量,首帧从零计算。titleHeightPx 和 titleWidthPx 从 0f 开始,由 onGloballyPositioned 填充,它在首次布局之后运行,因此首帧以零高度和未补偿的内边距放置标题。这对只喂给变换的值没问题,但对必须立即正确的值不行——绝不要用自测量尺寸计算头部高度、翻转点或滚动目标。

  2. 过渡期间的可读性靠遮罩和绘制顺序,而非裁剪。头部带垂直渐变,中部透明、底部不透明,使标题在图像上有对比度;Body 的不透明背景在向上滚动时覆盖头部。标题在 Box 中声明于工具栏之后,因此绘制在上层并落入折叠栏内;重排同级组件会让它在折叠结束时消失。maxLines = 1 完成其余工作。

  3. alpha 斜坡未做钳制。它在恰好一个头部高度的滚动后到达零,并继续变为负值——只是因为 Body 的不透明背景已覆盖头部才不可见。在半透明 Body 上应做 coerce。

  4. 使用 graphics layer,而非 offset 或 padding。graphicsLayer 块中的 translationY 在绘制阶段读取 scroll.value,因此滚动帧完全跳过组合与布局。Modifier.offset { } 属于布局,滚动驱动的 Modifier.padding 每帧重新测量。

验证方式

资料提供了三条 grep 检查命令(需先将 TOOLBAR 设为你的工具栏文件名):

TOOLBAR=CollapsingToolbar.kt

# 1. Geometry in a keyless remember — anything derived from a window or element size must be keyed.
find . -name "$TOOLBAR" -not -path "*/build/*" -exec grep -Hn "remember {" {} +

# 2. Everything the scroll value drives. Each hit should be inside a graphicsLayer or a
#    derivedStateOf — a scroll read in plain composition means a recomposition per frame.
find . -name "$TOOLBAR" -not -path "*/build/*" -exec grep -Hn "scroll.value\|collapseFraction\|graphicsLayer" {} +

# 3. Exactly one scrollable, and the spacer that reserves the header's space right after it.
find . -name "$TOOLBAR" -not -path "*/build/*" -exec grep -Hn "verticalScroll\|rememberScrollState\|Spacer(Modifier.height" {} +

然后用长标题和短标题缓慢滚动,观察路径走向而非是否平滑——无论控制点多错它都是平滑的。折叠过半时标题必须位于头部中部附近,不得向内切入,不得在收缩时横向漂移,必须居中落入栏内。恰好停在翻转点:只有一个返回入口。最后调整窗口大小并再次滚动,检查固定栏是否出现在错误偏移。

适用场景与限制

适用于在 Jetpack Compose 中实现折叠头部、视差图像、标题在头部与固定栏之间移动的界面。资料未提供该方案的平台版本要求、依赖库、安装命令或收费信息。