[SwiftUI 100 天] Animations - part1

1,450 阅读8分钟
译自 Animation: Introduction
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

动画: 介绍

在这个新项目中我们又将回归技术介绍,这回我们要接触一些又快又美,而且被低估的东西:动画 。

动画因为各种原因存在,其中一定包括让 UI 看起来更好这一条。不过,它们也有助于帮助用户理解我们的程序正在发生的事情:当一个窗口消失,另一个窗口滑入的时候,用户可以很清楚地看到消失窗口的去向,也就意味着他们能知道从哪里找回它。

在这个项目中我们将看到 SwiftUI 中的各类动画和过渡。有些很简单,实际上,你可以在瞬间就得到很赞的效果。而另一些需要多一点思考。不过所有的动画都有用处,尤其是当你想让你的 app 更有吸引力,并且辅助引导用户视线的时候。

像之前一样,通过 Xcode 工程来实践这些技术会是一个好注意,因为你可以即时地看到代码的效果。因此,请新建一个 Single View App 工程,取名叫 Animations 。


译自 Creating implicit animations

创建隐式动画

在 SwiftUI 中,最简单的动画类型是隐式动画: 我们提前告诉视图 “如果你需要执行动画的时候,你需要这样响应” 。SwiftUI 确保所有需要跟随动画一起变化的东西。实践中描述它会显得琐碎,因为不能再简单了。

让我们举例说明把。下面的代码展示了一个简单的红色按钮,有 50 个 point 的 padding 和圆形的按钮形状:

Button("Tap Me") {
    // do nothing
}
.padding(50)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())

我们希望每当按钮被点击时它会变大,这时我们可以用一个新的 modifier ,它叫 scaleEffect()。你需要传给它一个从 0 往上走的值,它会按照这个值绘制视图尺寸 —— 1.0 等价于 100% ,也就是按钮的正常大小。

因为我们希望每一次缩放动画都发生,所以需要用到一个 @State 属性,不过这里有一点要说明:由于历史原因,主要是与 Apple 的旧 API 交互相关,我们需要用到一个专门的数据类型叫CGFloat

CGFloat 本质上就是一个 Double 只不过起了一个不一样的名字,但是在更老的硬件上,它被映射到一个更小的数字类型,叫 Float。在这种选择还是必要的时代,CGFloat 让我们无需关心硬件类型,但如今几乎所有设备都采用 Double 的数字精度,所以这已经演变成了遗留代码。

之所以提到 CGFloat ,是因为如果我们书写 var animationAmount = 1 ,我们将得到一个整数,如果我们用var animationAmount = 1.0 ,将得到一个Double。也就说,没有内建的类型推断可以自动得到一个CGFloat —— 我们需要用到类型注释。

添加这个属性到你的视图:

@State private var animationAmount: CGFloat = 1

然后为按钮应用缩放效果,利用 scaleEffect modifier :

.scaleEffect(animationAmount)

最后,当按钮被点击时我们希望增加动画计数,把下面的代码设置给按钮的 action :

self.animationAmount += 1

运行代码,你会发现你将可以一直点击按钮,让按钮持续变大。由于分辨率不变,当按钮变得越来越大时,你会发现它开始变糊。不过没关系,我们稍后会解决。

人类的眼睛对运动的物体很敏感 —— 我们尤其擅长侦测物体移动或者改变外貌,这使得动画变得很重要,也令人愉悦。我们可以让 SwiftUI 为我们的变化创建一个隐式的动画,以便缩放过程平滑地发生,只需要给按钮添加一个 animation() modifier :

.animation(.default)

当你再点击按钮,你发现按钮的放大过程已经带有动画。

这种隐式动画可以作用于视图的各种属性,也就说,我们可以把更多的动画 modifier 添加到视图上,它们会一起作用。举个例子,假如我们给按钮再加一个 .blur() modifier ,它的作用是按照指定半径做高斯模糊:

.blur(radius: (animationAmount - 1) * 3)

一个(animationAmount - 1) * 3 的半径表示模糊半径会随着你点击按钮,从 0 (没有模糊) ,然后移动到 3 个点,6 个点,9 个点,然后更多。

这些模糊的半径的变化也会呈现出逐帧变化的动画效果,而我们并没有告诉 SwiftUI 在什么时候开始和结束。相反,是我们的动画本身,变成了状态的函数,就如同视图本身一样是状态的函数。


译自 Customizing animations in SwiftUI

在 SwiftUI 中自定义动画

当我们给一个视图添加 .animation(.default) modifier, SwiftUI 会使用默认的动画演示这个视图的任何变化。实践中,这是一个 “ease in, ease out” 动画,指的是 iOS 以慢速启动动画,然后加速,到达终点时又减速。

我们可以给 modifier 传入不同的值以控制动画的类型。例如,可以用 .easeOut 让动画快速启动,然后平滑减速到停止:

.animation(.easeOut)

甚至还有弹簧动画,它会让运动先超出目标点再弹回目标点,你可以控制弹簧的初始硬度(设置动画开始时弹簧的初始速度)以及动画衰减的速度 —— 越小的值会使得弹簧来回弹跳的时间越长。

举个例子,下面的代码能够让按钮快速放大,然后反弹:

.animation(.interpolatingSpring(stiffness: 50, damping: 1))

为了得到更精确的控制,我们可以为动画指定持续时间,以秒计。所以,一个持续两秒钟的 ease-in-out 动画就像下面这样:

struct ContentView: View {
    @State private var animationAmount: CGFloat = 1

    var body: some View {
        Button("Tap Me") {
            self.animationAmount += 1
        }
        .padding(50)
        .background(Color.red)
        .foregroundColor(.white)
        .clipShape(Circle())
        .scaleEffect(animationAmount)
        .animation(.easeInOut(duration: 2))
    }
}

当我们用 .easeInOut(duration: 2) 的时候,我们实际上是在创建一个 Animation结构体的实例,它有自己的一组 modifier 。所以,还可以再添加更多的 modifier ,比如延迟 1 秒开始动画。

.animation(
    Animation.easeInOut(duration: 2)
        .delay(1)
)

你可能注意这里我们显式地用了 Animation.easeInOut() ,因为不这样做的话 Swift 无法确定我们想表达什么。这样写之后,点击按钮时会等待 1 秒,然后执行 2 秒的动画。

我们还可以要求动画重复指定的次数,甚至通过设置 autoreverses 为 true 让动画正向反向来回执行。下面的代码创建一个 1 秒的动画,在稳定在最终大小前会来回变化:

.animation(
    Animation.easeInOut(duration: 1)
        .repeatCount(3, autoreverses: true)
)

如果我们把 repeatCount 设置为 2 ,那么按钮会放大再缩小,然后立刻跳到最大的状态。这是因为不管我们应用了什么动画,最终按钮还是必须匹配程序状态 —— 当动画结束时,按钮必须的大小必须跟你在animationAmount里设置的倍数相同。

如果想实现持续的动画,还有一个 repeatForever() modifier 可以使用:

.animation(
    Animation.easeInOut(duration: 1)
        .repeatForever(autoreverses: true)
)

我们可以把 repeatForever() 动画和 onAppear() 结合起来,让视图在出现时就执行动画并且持续整个生命周期。

为了演示这一点,我们需要先移除按钮之前的动画,并应用一个 overlay ,制作一个看起来有点像按钮周围的脉冲一样的效果。

首先,添加 overlay() modifier 到按钮:

.overlay(
    Circle()
        .stroke(Color.red)
        .scaleEffect(animationAmount)
        .opacity(Double(2 - animationAmount))
)

这会创建一个覆盖在按钮上的以红色描边的圆,不透明度采用 2 - animationAmount ,这样当 animationAmount 是 1 时不透明度是 1 (也就是完全不透明) ,而当 animationAmount 是 2 时不透明度是 0 (也就是完全透明)。

接下来,移除按钮的 scaleEffect() modifier ,把按钮的animationAmount += 1 的 action 部分也注释掉,因为我们不想要按钮再变化了,我们把动画 modifier 移到 overlay 的圆里面:

.overlay(
    Circle()
        .stroke(Color.red)
        .scaleEffect()
        .opacity(Double(2 - animationAmount))
        .animation(
            Animation.easeOut(duration: 1)
                .repeatForever(autoreverses: false)
        )
)

为了让动画不一样,我们把 autoreverses 设为 false 。

最后,添加一个 onAppear() modifier 给按钮,这里设置 animationAmount 为 2 :

.onAppear {
    self.animationAmount = 2
}

因为 overlay 的圆采用的是 “repeat forever” 的动画,并且不会自动逆转,所以你会看到 overlay 的圆一直放大并且逐渐淡出。

完成的代码就像下面这样:

Button("Tap Me") {
    // self.animationAmount += 1
}
.padding(40)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
    Circle()
        .stroke(Color.red)
        .scaleEffect(animationAmount)
        .opacity(Double(2 - animationAmount))
        .animation(
            Animation.easeOut(duration: 1)
                .repeatForever(autoreverses: false)
        )
)
.onAppear {
    self.animationAmount = 2
}

考虑到这么少的代码量,这样的动画效果实属诱人!


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~