SwiftUI动画进阶 - Part3 AnimatableModifier(动画修饰器)
2020/1/22 23:27:58
本文主要是介绍SwiftUI动画进阶 - Part3 AnimatableModifier(动画修饰器),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
文章源地址:swiftui-lab.com/swiftui-ani…
作者: Javier
翻译: Liaoworking
我们已经知道了Animatable协议是如何帮助我们来让path做动画和变换矩阵,在本系列的最后一个部分,我们将更近一步。AnimatableModifier 是这三个工具中最强大的一个。有了它你就可以为所欲为了。
从命名上来看AnimatableModifier(可动修饰器),这是一个遵循Animatable协议(第一节里讲的)视图修饰器,如果你不知道Animatable和animatableData 怎么工作的,可以回去第一节再看看。
现在可以先想想使用animatable modifier(可动修饰器)有什么作用,你可以通过它类多次修改你的视图来做动画。
The complete sample code for this article can be found at: gist.github.com/swiftui-lab…
Example8 requires images from an Asset catalog. Download it from here: swiftui-lab.com/?smd_proces…
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代码。你也可以试一试。
文字动画:
第一个例子是做一个加载指示器。
第一直觉告诉我应该使用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中就可以实现:
实现起来比较简单,我们只需要计算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)) } } 复制代码
更多的文字动画
在我们下面的例子中我们将只一次只给一个字母做动画。
平滑的逐步缩放需要一些数学运算。如果写出来就乐在其中了。代码我放在了 文章顶部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有所了解之前,下面的计数器可能有一点挑战性。
这个练习的取巧之处就每一列拿了五个数字竖向排列,并用了.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 中了。
struct 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上关注我来确保获取更多的内容。 欢迎评论。如果你想有新的文章出来的时候收到提醒,下面有链接。 swiftui-lab.com/
这篇关于SwiftUI动画进阶 - Part3 AnimatableModifier(动画修饰器)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值