Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Animating Views and Transitions

Willie edited this page Feb 10, 2020 · 4 revisions

动画视图与转场

使用 SwiftUI 时,无论用作何处,我们都可以单独为视图添加动画,或者对视图的状态添加动画。 SwiftUI 为我们处理所有动画的组合、重叠和中断的复杂性。

在本文中,我们会给包含图表的视图设置动画,跟踪用户在使用 Landmarks app 时行为。我们会看到通过使用 animation(_:) 修饰符为视图设置动画是多么简单。

下载项目文件并按照以下步骤操作,也可以打开已完成的项目自行浏览代码。

  • 预计完成时间:20 分钟
  • 项目文件:下载

1. 给单个视图添加动画

当我们在一个视图上使用 animation(_:) 修饰符时, SwiftUI 会动态的修改这个视图的可动画属性。一个视图的颜色、透明度、旋转、大小以及其他属性都是可动画的。

1.1 在 HikeView.swift 中,打开实时预览来测试显示和隐藏图表。

确保在本文中过程中都打开了实时预览,这样就可以测试到每一步的结果。

1.2 添加 animation(.basic()) 修饰符来设置按钮的旋转动画。

HikeView.swift

import SwiftUI
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 self.showDetail.toggle()
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 .padding()
 //
 .animation(.easeInOut())
 //
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 }
 }
 }
}

1.3 添加一个在图表显示时让按钮变大的动画。

animation(_:) 会作用于视图所包装的所有可动画的修改。

HikeView.swift

import SwiftUI
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 self.showDetail.toggle()
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 //
 .scaleEffect(showDetail ? 1.5 : 1)
 //
 .padding()
 .animation(.easeInOut())
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 }
 }
 }
}

1.4 把动画类型从 basic() 改成 spring()

SwiftUI 包含带有预设或自定义缓动的基本动画,以及弹性和流体动画。我们可以调整动画的速度、在动画开始之前设置延迟,或指定动画的重复。

HikeView.swift

import SwiftUI
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 self.showDetail.toggle()
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 .scaleEffect(showDetail ? 1.5 : 1)
 .padding()
 //
 .animation(.spring())
 //
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 }
 }
 }
}

1.5 试着在 scaleEffect 方法上方添加另一个动画方法来关闭旋转动画。

围绕 SwiftUI 尝试结合不同的动画效果,看看都有哪些效果。

HikeView.swift

import SwiftUI
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 self.showDetail.toggle()
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 //
 .animation(nil)
 //
 .scaleEffect(showDetail ? 1.5 : 1)
 .padding()
 .animation(.spring())
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 }
 }
 }
}

1.6 在继续下一节前,删除两个 animation(_:) 修饰符。

HikeView.swift

import SwiftUI
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 self.showDetail.toggle()
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 //
 //
 .scaleEffect(showDetail ? 1.5 : 1)
 .padding()
 //
 //
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 }
 }
 }
}

2. 将状态的改变动画化

现在我们已经学会如何给单个视图添加动画,是时候给状态的值的改变添加动画了。

这一节,我们会给用户点击按钮并切换 showDetail 状态属性时发生的所有更改添加动画。

2.1 将 showDetail.toggle() 的调用包装到 withAnimation 函数中。

showDetail 属性影响的按钮和 HikeDetail 视图现在就都有了动画过渡。

HikeView.swift

import SwiftUI
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 //
 withAnimation {
 //
 self.showDetail.toggle()
 }
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 .scaleEffect(showDetail ? 1.5 : 1)
 .padding()
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 }
 }
 }
}

减缓动画,看看 SwiftUI 动画是如何可以中断的。

2.2 给 withAnimation 方法传递一个 4 秒的基础动画。

我们可以传递相同类型的动画给 animation(_:) 修饰符的 withAnimation 函数。

HikeView.swift

import SwiftUI
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 //
 withAnimation(.easeInOut(duration: 4)) {
 //
 self.showDetail.toggle()
 }
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 .scaleEffect(showDetail ? 1.5 : 1)
 .padding()
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 }
 }
 }
}

2.3 尝试在动画期间打开和关闭图表视图。

2.4 在进入下一节前,从 withAnimation 函数中移除缓慢动画。

HikeView.swift

import SwiftUI
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 //
 withAnimation {
 //
 self.showDetail.toggle()
 }
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 .scaleEffect(showDetail ? 1.5 : 1)
 .padding()
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 }
 }
 }
}

3. 自定义视图的转场

默认情况下,视图通过淡入和淡出过渡到屏幕上和屏幕外。我们可以使用 transition(_:) 修饰符来自定义转场。

3.1 给满足条件时显示的 HikeView 添加一个 transition(_:) 修饰符。

现在图标会滑动显示和消失。

HikeView.swift

import SwiftUI
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 withAnimation {
 self.showDetail.toggle()
 }
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 .scaleEffect(showDetail ? 1.5 : 1)
 .padding()
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 //
 .transition(.slide)
 //
 }
 }
 }
}

3.2 将转场提取为 AnyTransition 的静态属性。

这可以在展开自定义转场时保持代码清晰。对于自定义转场,我们可以使用与 SwiftUI 所用相同的 . 符号。

HikeView.swift

import SwiftUI
//
extension AnyTransition {
 static var moveAndFade: AnyTransition {
 AnyTransition.slide
 }
}
//
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 withAnimation {
 self.showDetail.toggle()
 }
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 .scaleEffect(showDetail ? 1.5 : 1)
 .padding()
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 //
 .transition(.moveAndFade)
 //
 }
 }
 }
}

3.3 换成使用 move(edge:) 转场,这样图表会从同一边滑入和滑出。

HikeView.swift

import SwiftUI
extension AnyTransition {
 static var moveAndFade: AnyTransition {
 //
 AnyTransition.move(edge: .trailing)
 //
 }
}
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 withAnimation {
 self.showDetail.toggle()
 }
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 .scaleEffect(showDetail ? 1.5 : 1)
 .padding()
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 .transition(.moveAndFade)
 }
 }
 }
}

3.4 使用 asymmetric(insertion:removal:) 修饰符来给视图显示和消失时提供不同的转场。

HikeView.swift

import SwiftUI
extension AnyTransition {
 static var moveAndFade: AnyTransition {
 //
 let insertion = AnyTransition.move(edge: .trailing)
 .combined(with: .opacity)
 let removal = AnyTransition.scale
 .combined(with: .opacity)
 return .asymmetric(insertion: insertion, removal: removal)
 //
 }
}
struct HikeView: View {
 var hike: Hike
 @State private var showDetail = false
 var body: some View {
 VStack {
 HStack {
 HikeGraph(data: hike.observations, path: \.elevation)
 .frame(width: 50, height: 30)
 VStack(alignment: .leading) {
 Text(hike.name)
 .font(.headline)
 Text(hike.distanceText)
 }
 Spacer()
 Button(action: {
 withAnimation {
 self.showDetail.toggle()
 }
 }) {
 Image(systemName: "chevron.right.circle")
 .imageScale(.large)
 .rotationEffect(.degrees(showDetail ? 90 : 0))
 .scaleEffect(showDetail ? 1.5 : 1)
 .padding()
 }
 }
 if showDetail {
 HikeDetail(hike: hike)
 .transition(.moveAndFade)
 }
 }
 }
}

4. 给复杂的效果组合动画

单击条形下方的按钮时,图形会在三组不同的数据之间切换。在本节中,我们将使用组合动画为构成图形的胶囊提供动态、波动的转场。

4.1 把 showDetail 的默认值改成 true ,并把 HikeView 的预览固定在画布中。

这让我们在其他文件中制作动画时依然能在上下文中看到图表。

4.2 在 GraphCapsule.swift 中,添加一个新的计算动画属性,并将其应用于胶囊的形状上。

GraphCapsule.swift

import SwiftUI
struct GraphCapsule: View {
 var index: Int
 var height: CGFloat
 var range: Range<Double>
 var overallRange: Range<Double>
 var heightRatio: CGFloat {
 max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
 }
 var offsetRatio: CGFloat {
 CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
 }
 //
 var animation: Animation {
 Animation.default
 }
 //
 var body: some View {
 Capsule()
 .fill(Color.gray)
 .frame(height: height * heightRatio, alignment: .bottom)
 .offset(x: 0, y: height * -offsetRatio)
 //
 .animation(animation)
 //
 }
}

4.3 将动画改为弹性动画,使用初始速度让条形图跳跃。

GraphCapsule.swift

import SwiftUI
struct GraphCapsule: View {
 var index: Int
 var height: CGFloat
 var range: Range<Double>
 var overallRange: Range<Double>
 var heightRatio: CGFloat {
 max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
 }
 var offsetRatio: CGFloat {
 CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
 }
 var animation: Animation {
 //
 Animation.spring(dampingFraction: 0.5)
 //
 }
 var body: some View {
 Capsule()
 .fill(Color.gray)
 .frame(height: height * heightRatio, alignment: .bottom)
 .offset(x: 0, y: height * -offsetRatio)
 .animation(animation)
 }
}

4.4 加快动画速度,缩短每个小节移动到新位置所需的时间。

GraphCapsule.swift

import SwiftUI
struct GraphCapsule: View {
 var index: Int
 var height: CGFloat
 var range: Range<Double>
 var overallRange: Range<Double>
 var heightRatio: CGFloat {
 max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
 }
 var offsetRatio: CGFloat {
 CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
 }
 var animation: Animation {
 Animation.spring(dampingFraction: 0.5)
 //
 .speed(2)
 //
 }
 var body: some View {
 Capsule()
 .fill(Color.gray)
 .frame(height: height * heightRatio, alignment: .bottom)
 .offset(x: 0, y: height * -offsetRatio)
 .animation(animation)
 }
}

4.5 根据 胶囊在图表上的位置为每个动画添加延迟。

GraphCapsule.swift

import SwiftUI
struct GraphCapsule: View {
 var index: Int
 var height: CGFloat
 var range: Range<Double>
 var overallRange: Range<Double>
 var heightRatio: CGFloat {
 max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
 }
 var offsetRatio: CGFloat {
 CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
 }
 var animation: Animation {
 Animation.spring(dampingFraction: 0.5)
 .speed(2)
 //
 .delay(0.03 * Double(index))
 //
 }
 var body: some View {
 Capsule()
 .fill(Color.gray)
 .frame(height: height * heightRatio, alignment: .bottom)
 .offset(x: 0, y: height * -offsetRatio)
 .animation(animation)
 }
}

4.6 观察自定义动画在图表之间转场时是如何营造波动效果的。

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /