SwiftUI动画进阶 - Part3 AnimatableMo
文章源地址:https://swiftui-lab.com/swiftui-animations-part3/
作者: Javier
翻译: Liaoworking
我们已经知道了Animatable协议是如何帮助我们来让path做动画和变换矩阵,在本系列的最后一个部分,我们将更近一步。AnimatableModifier 是这三个工具中最强大的一个。有了它你就可以为所欲为了。
从命名上来看AnimatableModifier(可动修饰器),这是一个遵循Animatable协议(第一节里讲的)视图修饰器,如果你不知道Animatable和animatableData 怎么工作的,可以回去第一节再看看。
现在可以先想想使用animatable modifier(可动修饰器)有什么作用,你可以通过它类多次修改你的视图来做动画。
The complete sample code for this article can be found at:
https://gist.github.com/swiftui-lab/e5901123101ffad6d39020cc7a810798
Example8 requires images from an Asset catalog. Download it from here:
https://swiftui-lab.com/?smd_process_download=1&download_id=916
AnimatableModifier为啥做不了动画了?
如果打算在生产环境使用AnimatableModifier,那你一定要阅读最后一节,和版本做斗争
如果你想要尝试一下协议,机会来了,你可能马上就要碰壁了。我之前已经尝试过了,我写了一个很简单的animatable modifier,但是视图并没有做动画,我又做了一些其他的尝试,还是不行,幸运的是
我坚持了一会,成功了。 先把这个幸运的是
加粗。
我的第一个modifier很好,但是当它在容器内部的时候就不起作用了。。。 第二次起作用是因为我的视图不在容器内部,如果我一开始就很幸运,就不会写第三篇文章了。
例如下面这个modifier就可以很好的做动画
MyView().modifier(MyAnimatableModifier(value: flag ? 1 : 0))
但是在VStack中,一样的代码就不会生效
VStack {
MyView().modifier(MyAnimatableModifier(value: flag ? 1 : 0))
}
那么如何在VStack中让animatable modifiers起作用呢?我们可以用下面这个取巧的方法:
VStack {
Color.clear.overlay(MyView().modifier(MyAnimatableModifier(value: flag ? 1 : 0))).frame(width: 100, height: 100)
}
先用一个透明的视图来占位,让后在透明的图上面使用.overlay()去添加实际的图。我们需要知道实际图的大小,来确定透明图的大小,这一点有时会会麻烦一些。
我把这个问题报告给苹果了,点击这里查询FB代码。你也可以试一试。
文字动画:
第一个例子是做一个加载指示器。
image
第一直觉告诉我应该使用animatable path,然而这个并不能让label做动画,那么用AnimatableModifier试试。
完整的代码在顶部的gist中的 Example10 可以找到。
struct PercentageIndicator: AnimatableModifier {
var pct: CGFloat = 0
var animatableData: CGFloat {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
content
.overlay(ArcShape(pct: pct).foregroundColor(.red))
.overlay(LabelView(pct: pct))
}
// 弧形
struct ArcShape: Shape {
let pct: CGFloat
func path(in rect: CGRect) -> Path {
var p = Path()
p.addArc(center: CGPoint(x: rect.width / 2.0, y:rect.height / 2.0),
radius: rect.height / 2.0 + 5.0,
startAngle: .degrees(0),
endAngle: .degrees(360.0 * Double(pct)), clockwise: false)
return p.strokedPath(.init(lineWidth: 10, dash: [6, 3], dashPhase: 10))
}
}
struct LabelView: View {
let pct: CGFloat
var body: some View {
Text("\(Int(pct * 100)) %")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
}
}
}
正如你再例子中所看到的,我们并没有让弧形动起来,这并不是必须的,因为modifier已经多次通过不同的百分比pct去创建图形了。
渐变动画
如果你想要让一个渐变层做动画。就好发现有很多限制,例如你可以从起点运动到终点,但是你不能让渐变色改变,但在AnimatableModifier中就可以实现:
image
实现起来比较简单,我们只需要计算RGB的平均值。不过要注意modifier 假定我们从头到尾每一个输入的颜色数组的count是相同的。
完整代码可以从文章顶部的gist的 Example11 中找到。
struct AnimatableGradient: AnimatableModifier {
let from: [UIColor]
let to: [UIColor]
var pct: CGFloat = 0
var animatableData: CGFloat {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
var gColors = [Color]()
for i in 0..<from.count {
gColors.append(colorMixer(c1: from[i], c2: to[i], pct: pct))
}
return RoundedRectangle(cornerRadius: 15)
.fill(LinearGradient(gradient: Gradient(colors: gColors),
startPoint: UnitPoint(x: 0, y: 0),
endPoint: UnitPoint(x: 1, y: 1)))
.frame(width: 200, height: 200)
}
// This is a very basic implementation of a color interpolation
// between two values.
func colorMixer(c1: UIColor, c2: UIColor, pct: CGFloat) -> Color {
guard let cc1 = c1.cgColor.components else { return Color(c1) }
guard let cc2 = c2.cgColor.components else { return Color(c1) }
let r = (cc1[0] + (cc2[0] - cc1[0]) * pct)
let g = (cc1[1] + (cc2[1] - cc1[1]) * pct)
let b = (cc1[2] + (cc2[2] - cc1[2]) * pct)
return Color(red: Double(r), green: Double(g), blue: Double(b))
}
}
更多的文字动画
在我们下面的例子中我们将只一次只给一个字母做动画。
image
平滑的逐步缩放需要一些数学运算。如果写出来就乐在其中了。代码我放在了 文章顶部gist里的 Example12
struct WaveTextModifier: AnimatableModifier {
let text: String
let waveWidth: Int
var pct: Double
var size: CGFloat
var animatableData: Double {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
HStack(spacing: 0) {
ForEach(Array(text.enumerated()), id: \.0) { (n, ch) in
Text(String(ch))
.font(Font.custom("Menlo", size: self.size).bold())
.scaleEffect(self.effect(self.pct, n, self.text.count, Double(self.waveWidth)))
}
}
}
func effect(_ pct: Double, _ n: Int, _ total: Int, _ waveWidth: Double) -> CGFloat {
let n = Double(n)
let total = Double(total)
return CGFloat(1 + valueInCurve(pct: pct, total: total, x: n/total, waveWidth: waveWidth))
}
func valueInCurve(pct: Double, total: Double, x: Double, waveWidth: Double) -> Double {
let chunk = waveWidth / total
let m = 1 / chunk
let offset = (chunk - (1 / total)) * pct
let lowerLimit = (pct - chunk) + offset
let upperLimit = (pct) + offset
guard x >= lowerLimit && x < upperLimit else { return 0 }
let angle = ((x - pct - offset) * m)*360-90
return (sin(angle.rad) + 1) / 2
}
}
extension Double {
var rad: Double { return self * .pi / 180 }
var deg: Double { return self * 180 / .pi }
}
来点创意
在我们对AnimatableModifier有所了解之前,下面的计数器可能有一点挑战性。
image
这个练习的取巧之处就每一列拿了五个数字竖向排列,并用了.spring()动画,我们还需要.clipShape()来隐藏边框外面的视图。可以把.clipShape() 注释掉和降低动画速度来更好的理解它的工作原理。完整代码在文章顶部gist里的 Example13 里。
struct MovingCounterModifier: AnimatableModifier {
@State private var height: CGFloat = 0
var number: Double
var animatableData: Double {
get { number }
set { number = newValue }
}
func body(content: Content) -> some View {
let n = self.number + 1
let tOffset: CGFloat = getOffsetForTensDigit(n)
let uOffset: CGFloat = getOffsetForUnitDigit(n)
let u = [n - 2, n - 1, n + 0, n + 1, n + 2].map { getUnitDigit($0) }
let x = getTensDigit(n)
var t = [abs(x - 2), abs(x - 1), abs(x + 0), abs(x + 1), abs(x + 2)]
t = t.map { getUnitDigit(Double($0)) }
let font = Font.custom("Menlo", size: 34).bold()
return HStack(alignment: .top, spacing: 0) {
VStack {
Text("\(t[0])").font(font)
Text("\(t[1])").font(font)
Text("\(t[2])").font(font)
Text("\(t[3])").font(font)
Text("\(t[4])").font(font)
}.foregroundColor(.green).modifier(ShiftEffect(pct: tOffset))
VStack {
Text("\(u[0])").font(font)
Text("\(u[1])").font(font)
Text("\(u[2])").font(font)
Text("\(u[3])").font(font)
Text("\(u[4])").font(font)
}.foregroundColor(.green).modifier(ShiftEffect(pct: uOffset))
}
.clipShape(ClipShape())
.overlay(CounterBorder(height: $height))
.background(CounterBackground(height: $height))
}
func getUnitDigit(_ number: Double) -> Int {
return abs(Int(number) - ((Int(number) / 10) * 10))
}
func getTensDigit(_ number: Double) -> Int {
return abs(Int(number) / 10)
}
func getOffsetForUnitDigit(_ number: Double) -> CGFloat {
return 1 - CGFloat(number - Double(Int(number)))
}
func getOffsetForTensDigit(_ number: Double) -> CGFloat {
if getUnitDigit(number) == 0 {
return 1 - CGFloat(number - Double(Int(number)))
} else {
return 0
}
}
}
动画文字颜色
你如果有尝试使.foregroundColor()做动画,就会发现开发者体验极好,完整代码在 Example14 中了。
imagestruct AnimatableColorText: View {
let from: UIColor
let to: UIColor
let pct: CGFloat
let text: () -> Text
var body: some View {
let textView = text()
return textView.foregroundColor(Color.clear)
.overlay(Color.clear.modifier(AnimatableColorTextModifier(from: from, to: to, pct: pct, text: textView)))
}
struct AnimatableColorTextModifier: AnimatableModifier {
let from: UIColor
let to: UIColor
var pct: CGFloat
let text: Text
var animatableData: CGFloat {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
return text.foregroundColor(colorMixer(c1: from, c2: to, pct: pct))
}
// This is a very basic implementation of a color interpolation
// between two values.
func colorMixer(c1: UIColor, c2: UIColor, pct: CGFloat) -> Color {
guard let cc1 = c1.cgColor.components else { return Color(c1) }
guard let cc2 = c2.cgColor.components else { return Color(c1) }
let r = (cc1[0] + (cc2[0] - cc1[0]) * pct)
let g = (cc1[1] + (cc2[1] - cc1[1]) * pct)
let b = (cc1[2] + (cc2[2] - cc1[2]) * pct)
return Color(red: Double(r), green: Double(g), blue: Double(b))
}
}
}
Dancing With Versions(和版本做斗争)
我们已经发现了AnimatableModifier很强大了,虽然也稍微有点bug。最大的问题是在一些具体的Xcode and iOS、macOS 版本下面应用会再启动的时候崩溃了,更严重的是在部署的时候更频繁。但是编译和在dev环境的时候就没事。以为会没啥问题,但在部署的时候去编译就会有下面的内容:
dyld: Symbol not found: _$s7SwiftUI18AnimatableModifierPAAE13_makeViewList8modifier6inputs4bodyAA01_fG7OutputsVAA11_GraphValueVyxG_AA01_fG6InputsVAiA01_L0V_ANtctFZ
Referenced from: /Applications/MyApp.app/Contents/MacOS/MyApp
Expected in: /System/Library/Frameworks/SwiftUI.framework/Versions/A/SwiftUI
例如 Xcode11.3在macOS 10.15.0上执行就取法启动 并显示”找不到符号表“的错误,但在10.15.1上相同的文件就稳得一批。
相反,如果在Xcode11.1上去部署,就在所有的macOS版本上正常(至少我试过的版本)
iOS系统也会有类似的问题, Xcode 11.2打包使用AnimatableModifier的应用无法在iOS 13.2.2上启动,但在iOS 13.2.3上可以正常运行。
所以我暂时都是求稳用的Xcode11.1。以后可能会使用较新的版本,不过会把Mac系统版本提升到10.15.1(除非把这个bug修了,不过我深表怀疑。。)
总结和接下来要讲什么
我们已经看到了Animatable协议的简单使用。发挥您的创造力,会有很多炫酷的动画。
到此"SwiftUI 高级动画" 系列就全结束了,下面我会讲一些关于自定义转场的文字。也算是对这几篇文章做一个总结了。
可以在Twitter上关注我来确保获取更多的内容。 欢迎评论。如果你想有新的文章出来的时候收到提醒,下面有链接。
https://swiftui-lab.com/