创建 Elastic Header 动画效果(Apple TV)

了解如何创建类似 Apple TV 应用中的视差头部效果(Parallax Header)/弹性头部效果(Elastic Header)。

创建 Elastic Header 动画效果(Apple TV)

在 Apple TV 移动应用中,随着用户滑动,顶部图片会以不同速度移动产生深度感和拉伸感。

0:00
/0:04

苹果 TV 应用中的设计效果

这种效果在 UI 设计中,通常被称为"视差头部效果"(Parallax Header Effect)或"弹性头部效果"(Elastic Header Effect)。

  • 图片顶部始终固定在屏幕顶端
  • 随着手指往下滑动,图片在纵轴和横轴上均会被轻微放大。

GeometryReader + Frame

创建 Elastic Header Effect 的常见方式是使用 GeometryReader 结合 .frame 修饰器:

  • 通过 GeometryReader 获取用户滑动时的偏移量。
  • 通过偏移量,动态计算 .frame() 的高度和宽度,从而实现图片放大效果。
  • 还可以通过偏移量,动态计算 .offset,实现图片拉伸效果。

示例代码结构是这样的:

var body: some View {
    GeometryReader { proxy in
        let offset = offset(for: proxy)
        let heightModifier = heightModifier(for: proxy)
        let blurRadius = min(
            heightModifier / 20,
            max(10, heightModifier / 20)
        )
        content()
            .edgesIgnoringSafeArea(.horizontal)
            .frame(
                width: proxy.size.width,
                height: proxy.size.height + heightModifier
            )
            .offset(y: offset)
            .blur(radius: blurRadius)
    }.frame(height: defaultHeight)
}

在 iOS18 上(WWDC24),苹果为 ScrollView 添加了众多功能。

通过结合使用 ScrollGeometry.onScrollPhaseChange() 修饰器,现在可以更加简单的实现 Elastic Header Effect 效果。

ScrollGeometry | Apple Developer Documentation
A type that defines the geometry of a scroll view.
onScrollPhaseChange(_:) | Apple Developer Documentation
Adds an action to perform when the scroll phase of the first scroll view in the hierarchy changes.

ScrollGeometry + onScrollPhaseChange

创建基本布局

创建基本的布局效果,有几个关键点:

  • 使用 ScrollView() 添加滑动功能,使用 .scrollIndicators(.hidden) 隐藏滚动条。
  • 使用 ZStack 创建布局,封面图片作为下层,其他文字部分作为上层展示。不使用.overlay()是为了添加上滑时滚动视差效果。
  • 使用  .ignoresSafeArea(.container, edges: .top),让图片对齐屏幕顶部区域,忽略安全区。

基础布局代码结构如下:

ScrollView {
    VStack(alignment: .center, spacing: 16) {
        ZStack(alignment: .bottomLeading) {
            // 封面图片
            Image(bookmark.coverImage)
                .padding(.horizontal, -16)
            
            MovieInfoView(bookmark: bookmark)
        }
    }
    .frame(maxWidth: .infinity)
    .padding(.horizontal, 16)
}
.ignoresSafeArea(.container, edges: .top)
.scrollIndicators(.hidden)

这将能够创建类似下面这样的效果:

0:00
/0:04

添加 ScrollGeometry 变量

首先,我们需要创建一个 ScrollGeometry 对象,通过它来获取滚动数据(内容偏移量、滚动边界、滚动内容大小等),这是所有后续添加动画效果的基础。

// Scroll Animation Properties
@State private var scrollProperties: ScrollGeometry = .init(
    contentOffset: .zero,
    contentSize: .zero,
    contentInsets: .init(),
    containerSize: .zero
)

创建一个 scrollProperties 变量,它是一个 ScrollGeometry 对象。