译自 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
当我们给一个视图添加 .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及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~