02 Animating Views and Transitions - ly918/SwiftUI-Chinese-Documents GitHub Wiki

绘图和动画

视图动画和转场

使用SwiftUI时,无论效果在哪里,都可以对视图或视图状态的更改单独设置动画。SwiftUI负责处理这些组合、重叠和可中断动画的所有复杂性。

在本教程中,您将设置一个视图的动画,该视图包含一个图形,用于跟踪用户在使用Landmarks应用程序时的行为数据。使用animation(_:)修改器,您将看到设置视图动画是多么容易。

学习时间:25分钟

下载地址:AnimatingViewsAndTransitions.zip

第一节 向各个视图添加动画

在视图上使用animation(_:)修改器时,SwiftUI可以改变视图的任何animatable(可动画)属性。视图的颜色color、不透明度opacity、旋转rotation、大小size和其他属性都是可设置动画的。

步骤1

HikeView.swift中,打开实时预览并尝试显示和隐藏图形。

请确保在本教程中使用实时预览,以便您可以尝试每个步骤的结果。

步骤2

通过添加animation(.easeInOut())为按钮启用旋转动画。

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)
            }
        }
    }
}

步骤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)
            }
        }
    }
}

步骤4

将动画类型从easeInOut()更改为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)
            }
        }
    }
}

步骤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)
            }
        }
    }
}

步骤6

在转到下一节之前,请删除两个动画(:)修改器。

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)
            }
        }
    }
}

第二节 使状态的改变产生动画效果

既然您已经学习了如何将动画应用于各个视图,现在是时候在更改状态值的位置添加动画了。

在这里,您将对用户点击按钮并切换showDetail属性时发生的所有更改添加动画。

步骤1

withAnimation函数包装showDetail.toggle()的调用。

showDetail属性影响的两个视图(disclosure按钮和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

将4秒长的基本动画传递给withAnimation函数。

可以传递相同类型的动画给给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)
            }
        }
    }
}

步骤3

尝试在动画中打开和关闭图形视图。

步骤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)
            }
        }
    }
}

第三节 自定义视图转场

默认情况下,视图通过淡入淡出在屏幕内外切换。您可以使用transition(:)修饰符自定义此转场。

步骤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)
            }
        }
    }
}

步骤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

切换到使用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)
            }
        }
    }
}

步骤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)
            }
        }
    }
}

第四节 为复杂效果撰写动画

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

步骤1

showDetail的默认值更改为true,并将HikeView预览锁定到画布。

这使您在另一个文件中处理动画时依然可以在上下文中看到图表。

步骤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)
        )
    }
}

步骤3

将动画切换为弹簧动画spring,减少阻尼部分dampingFraction以使条形可以跳跃。

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

稍微加快动画的速度,以缩短每个条形移动到新位置所需的时间。

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)
        )
    }
}

步骤5

根据胶囊在图形上的位置为每个动画添加延迟delay

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)
        )
    }
}

步骤6

观察自定义动画在图形之间转换时如何提供波动效果。

⚠️ **GitHub.com Fallback** ⚠️