美文网首页
SwiftUI动画进阶 - Part3 AnimatableMo

SwiftUI动画进阶 - Part3 AnimatableMo

作者: liaoworkinn | 来源:发表于2020-01-18 09:50 被阅读0次

    文章源地址: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 中了。

    image
    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上关注我来确保获取更多的内容。 欢迎评论。如果你想有新的文章出来的时候收到提醒,下面有链接。
    https://swiftui-lab.com/

    相关文章

      网友评论

          本文标题:SwiftUI动画进阶 - Part3 AnimatableMo

          本文链接:https://www.haomeiwen.com/subject/xukkzctx.html