我正在参加「掘金·启航计划」
SwiftUI 官方教程:SwiftUI Tutorials 仅是几个体现 SwiftUI 简单使用的小 demo 而已,简单易学,循序渐进,先看完可以对 SwiftUI 有一个大概的认知。
五:Animating Views and Transitions
Drawing and Animation - Animating Views and Transitions 为视图和过渡添加动画效果
使用 SwiftUI 时,你可以单独对视图或视图状态的更改进行动画处理,无论效果位于何处。SwiftUI 为你处理这些组合、重叠和可中断动画的所有复杂性。
在本节中,将对一个视图进行动画处理,该视图包含一个图表,用于跟踪用户在使用 Landmark 应用程序时进行的徒步旅行。使用 animation(_:) 修饰符,你将看到为视图添加动画效果是多么容易。
Add Hiking Data to the App
在添加动画之前,你需要对某些内容进行动画处理。在本节中,你将导入徒步旅行数据并对其进行建模,然后添加一些预构建的视图,以便在图表中静态显示该数据。
将 hikeData.json 文件从下载文件的 "Resources" 文件夹拖到项目的 "Resources" 文件夹中。请务必选择 "Copy items if needed",然后再点按 "Finish" 按钮。
使用菜单项 "File > Nes > File",在项目的 "Model" 文件夹中创建一个名为 Hike.swift 的新 Swift 文件。
与 "Landmark" 结构体一样,"Hike" 结构体也遵循 Codable 协议,并且具有与相应数据文件中的键匹配的属性。
import Foundation
struct Hike: Codable, Hashable, Identifiable {
var id: Int
var name: String
var distance: Double
var difficulty: Int
var observations: [Observation]
static var formatter = LengthFormatter()
var distanceText: String {
Hike.formatter.string(fromValue: distance, unit: .kilometer)
}
struct Observation: Codable, Hashable {
var distanceFromStart: Double
var elevation: Range<Double>
var pace: Range<Double>
var heartRate: Range<Double>
}
}
将 hikes 数组加载到模型对象中。
由于你在最初加载 hikes 数据后永远不会修改它,因此你无需使用 @Published 属性对其进行标记。
final class ModelData: ObservableObject {
...
var hikes: [Hike] = load("hikeData.json")
}
将 "Hikes" 文件夹从下载文件的 "Resources" 文件夹拖到项目的 "Views" 文件夹中。请务必选择 "Copy items if needed",然后再点按 "Finish" 按钮。
熟悉新 Views。它们协同工作以显示加载到模型中的 hike 数据。
在 HikeView.swift 中,打开实时预览并尝试显示和隐藏图表。
请务必在本教程中使用实时预览,以便可以试验每个步骤的结果。
Add Animations to Individual Views
使用 animation(_:) 在 equatable 视图上使用修饰符,SwiftUI 会对视图的可动画属性所做的任何更改进行动画处理。视图的颜色、不透明度、旋转、大小和其他属性都是可设置动画的。当视图非 equatable 时,可以使用 animation(_:value:) 用于在指定值更改时启动动画的修饰符。
在 HikeView.swift 中,通过添加一个动画修饰符来打开按钮旋转的动画,该修改器从 showDetail 值的更改开始。
...
Button {
showDetail.toggle()
} label: {
Label("Graph", systemImage: "chevron.right.circle")
.labelStyle(.iconOnly)
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.padding()
.animation(.easeInOut, value: showDetail) // 动画
}
...
通过在图形可见时放大按钮来添加另一个可动画更改。动画修饰符应用于其包装视图中的所有可动画更改。
Button {
showDetail.toggle()
} label: {
Label("Graph", systemImage: "chevron.right.circle")
.labelStyle(.iconOnly)
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.scaleEffect(showDetail ? 1.5 : 1) // 动画
.padding()
.animation(.easeInOut, value: showDetail) // 动画
}
将动画类型从 easeInOut 更改为 spring()。SwiftUI 包括带有预定义或自定义缓动的基本动画,以及弹簧和流体动画。你可以调整动画的速度、设置动画开始前的延迟或指定动画重复。
Button {
showDetail.toggle()
} label: {
Label("Graph", systemImage: "chevron.right.circle")
.labelStyle(.iconOnly)
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.scaleEffect(showDetail ? 1.5 : 1)
.padding()
.animation(.spring(), value: showDetail) // .easeInOut 修改为 .spring()
}
尝试通过在 scaleEffect 修改器上方添加另一个动画修改器来关闭旋转动画。实验性质:试一试 SwiftUI。尝试组合不同的动画效果,看看有什么可能性。
Button {
showDetail.toggle()
} label: {
Label("Graph", systemImage: "chevron.right.circle")
.labelStyle(.iconOnly)
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.animation(nil, value: showDetail) // 修改
.scaleEffect(showDetail ? 1.5 : 1)
.padding()
.animation(.spring(), value: showDetail)
}
请先删除这两个动画修饰符,然后再转到下一节。
Animate the Effects of State Changes
现在你已经了解了如何将动画应用到各个视图,是时候在你更改状态值的地方添加动画了。
在这里,你将动画应用于当用户点击按钮并切换 showDetail 状态属性时发生的所有更改。
将调用 showDetail.toggle() 包裹到 withAnimation 函数调用中。受 showDetail 属性影响的两个视图 —— 显示按钮和 HikeDetail 视图 —— 现在都有动画过渡。
Button {
// showDetail.toggle() 调用包裹到 withAnimation 调用中
withAnimation {
showDetail.toggle()
}
} label: {
Label("Graph", systemImage: "chevron.right.circle")
.labelStyle(.iconOnly)
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.scaleEffect(showDetail ? 1.5 : 1)
.padding()
}
放慢动画速度,看看 SwiftUI 动画是如何被中断的。
将四秒长的基本动画传递给 withAnimation 函数。
你可以将相同类型的动画传递给传递给动画的 animation(_:value:) 修饰语。
withAnimation(.easeInOut(duration: 4)) {
showDetail.toggle()
}
尝试在动画中间打开和关闭图形视图,在 Live Preview 中进行预览。
在继续下一节之前,请通过删除调用的输入参数来还原 withAnimation 函数以使用默认动画。
Customize View Transitions
默认情况下,视图通过淡入和淡出在屏幕上和屏幕外进行过渡。你可以使用 transition(_:) 修饰符自定义此过渡。
向有条件可见的 HikeView 添加一个 transition(_:) 修饰符。现在,图形通过滑入和滑出视线而出现和消失。
...
if showDetail {
HikeDetail(hike: hike)
.transition(.slide)
}
...
提取刚刚作为 AnyTransition 的静态属性添加的过渡,并在视图的过渡修饰符中访问新属性。当你扩展自定义过渡时,这可以使你的代码保持干净。
extension AnyTransition {
static var moveAndFade: AnyTransition {
AnyTransition.slide
}
}
...
if showDetail {
HikeDetail(hike: hike)
.transition(.moveAndFade)
}
...
切换到使用 move(edge:) 过渡,以便图形从同一侧滑入和滑出。
extension AnyTransition {
static var moveAndFade: AnyTransition {
AnyTransition.move(edge: .trailing)
}
}
使用 asymmetric(insertion:removal:) 修饰符为视图出现和消失提供不同的过渡。
extension AnyTransition {
static var moveAndFade: AnyTransition {
// AnyTransition.slide
// AnyTransition.move(edge: .trailing)
.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .scale.combined(with: .opacity)
)
}
}
Compose Animations for Complex Effects
当你单击条形下方的按钮时,图形会在三组不同的数据集之间切换。在本节中,你将使用组合动画为构成图形的胶囊提供动态的波纹过渡。
在 HikeView 中,将 showDetail 的默认值更改为 true,并将预览固定到画布上。这使你可以在处理另一个文件中的动画时在上下文中查看图形。
在 HikeGraph.swift 中,定义一个新的波纹动画并将其应用于每个生成的图形胶囊。
extension Animation {
static func ripple() -> Animation {
Animation.default
}
}
...
GraphCapsule(
index: index,
color: color,
height: proxy.size.height,
range: observation[keyPath: path],
overallRange: overallRange
)
.animation(.ripple())
...
将动画切换为弹簧动画,减少阻尼分数以使条形跳跃。你可以在实时预览中通过在海拔、心率和配速之间切换来查看动画效果。
extension Animation {
static func ripple() -> Animation {
Animation.spring(dampingFraction: 0.5)
}
}
稍微加快动画速度,以缩短每个条形移动到新位置所需的时间。
extension Animation {
static func ripple() -> Animation {
Animation.spring(dampingFraction: 0.5)
.speed(2)
}
}
为每个动画添加基于胶囊在图形上的位置的延迟。
extension Animation {
static func ripple(index: Int) -> Animation {
Animation.spring(dampingFraction: 0.5)
.speed(2)
.delay(0.03 * Double(index))
}
}
...
GraphCapsule(
index: index,
color: color,
height: proxy.size.height,
range: observation[keyPath: path],
overallRange: overallRange
)
.animation(.ripple(index: index))
...
观察自定义动画在图形之间过渡时如何提供波纹效果。请务必先取消固定预览,然后再继续学习下一教程。
本节专注于动画和过渡的学习,暂时就到这里。
参考链接
参考链接:🔗