swiftui之深入解析高级动画的animatablemodifier使用(代码片段)

Serendipity·y Serendipity·y     2023-03-09     731

关键词:

一、前言

二、动画文本

  • 首先需要制作一些文字动画,如下所示,创建一个进度加载指示器:

  • 可能很多人都认为应该使用动画路径实现,但是,内部标签就无法设置动画,使用 AnimatableModifier 可以实现,关键代码如下(完整代码请参考文末的完整示例的示例 10):
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)
        
    

  • 在示例中,可以看到没有使 ArcShape animatable,因为 modifier 已经多次创建形状,具有不同的 pct 值。

三、动画渐变

  • 在实现渐变动画时,会遇到一些限制,比如,可以为起点和终点设置动画,但是不能为渐变颜色设置动画。使用 AnimatableModifier 就可以避免出现:

  • 很容易就可以实现这个功能,在这个基础上可以实现更多复杂的动画。如果需要插入中间颜色,只需要计算 RGB 值的平均值。另外需要注意,modifier 假设输入颜色数组都包含相同数量的颜色。关键代码如下(完整代码请参考文末的完整示例的示例 11):
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))
    

四、更多文本动画

  • 再次实现一个文本动画,逐步进行,一次放大一个字符,如下所示:

  • 关键代码如下(完整代码请参考文末的完整示例的示例 12):
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 

五、计数器动画

  • 如下所示,如何创建一个计数器动画:

  • 其实很简单,就是为每个数字使用 5 个 Text 视图,并通过 .spring() 动画上下移动它们,此外还需要使用 .clipshape() 修饰符,来隐藏绘制边框外的部分。
  • 为了更好地理解它是如何工作的,可以给. clipshape() 加注释并大大降低动画的速度(完整代码请参考文末的完整示例的示例 13),关键代码如下:
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() 为动画添加颜色,但是在文本类动画中使用没有效果。然而,如果需要动画文本的颜色,可以使用 AnimatableModifier 实现:

  • 关键如下所示(完整代码请参考文末的完整示例的示例 14):
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))
        

    

七、AnimatableModifier 无法实现动画问题

  • 如果是第一次使用 AnimatableModifier,可能会遇到问题,试下一个简单的动画,但是没有动画效果,如下所示,在 VStack 中就没有动画效果:
VStack 查看详情  

swift之深入解析如何结合coredata和swiftui

SwiftUI和CoreData之间相差将近十年,SwiftUI随着iOS13面世,而CoreData则是iPhoneOS3的产物;很久以前,它还没有被称为iOS,因为iPad尚未发布。尽管时间相距遥远,Apple还是投入了大量工作以确保这两种强大的技术能够完美地相互配合使... 查看详情

swift之深入解析如何使用swiftui实现3dscroll效果(代码片段)

一、SwiftUI视图创建首先,创建一个新的SwiftUI视图,为了举例说明,在这个新视图中,会展示一个有各种颜色的矩形列表,并把新视图命名为ColorList:importSwiftUIstructColorList:Viewvarbody:someViewText("Hello,World!&#... 查看详情

swiftui之深入解析如何使用组合矩形geometryreader创建条形(柱状)图(代码片段)

...数据的类别,其宽度和高度与它们表示的值成比例。SwiftUI对探索不同布局和预览实时视图结果是很友好的,很容易将部分内容提取到子视图中,以便每个部分都很小且易于维护。从包含HistogramView以及可能的其它文本... 查看详情

swiftui之深入解析如何使用组合矩形geometryreader创建条形(柱状)图(代码片段)

...数据的类别,其宽度和高度与它们表示的值成比例。SwiftUI对探索不同布局和预览实时视图结果是很友好的,很容易将部分内容提取到子视图中,以便每个部分都很小且易于维护。从包含HistogramView以及可能的其它文本... 查看详情

git之深入解析高级合并(代码片段)

...暂存区和轻量级地分支及合并的威力。如果想进一步对Git深入学习,可以学习一些Git更加强大的功能,这些功能可能并不会在日常操作中使用,但在某些时候可能还是会起到一定的关键性作用。如果还不清楚Git的基础... 查看详情

swiftui之深入解析如何处理特定的数据和如何在视图中适配数据模型对象(代码片段)

...言阅读了我的前两篇博客的朋友,应该都熟练掌握了SwiftUI如何创建一个任何相关信息的展示视图和各个视图之间的相互组合,以及动态生成一个展示相关信息的可滚动列表,用户可以点击列表项去查看其相关的详细... 查看详情

swiftui之深入解析如何创建和组合视图(代码片段)

一、创建项目并体验画布①系统要求创建SwiftUI项目工程,体验画布、预览模式和SwiftUI模板代码;要想在Xcode中预览画布中的视图,或者与画布中的视图进行交互,需要Mac系统版本号不低于macOSCatalina10.15。②步骤打... 查看详情

swift之深入解析swiftui布局如何自定义alignmentguides(代码片段)

SwiftUI提供了视图不同边缘的对齐指南(.leading、trailing、top等)以及.center和两个基线选项来帮助文本对齐。然而,当处理在不同视图之间分割的视图时,如果必须使在用户界面完全不同的两个视图部分对齐,... 查看详情

swift之深入解析swiftui属性包装器如何处理结构体(代码片段)

已经了解了SwiftUI如何通过使用@State属性包装器将变化的数据存储在结构体中,如何使用$将状态绑定到UI控件的值,以及更改@state包装的属性时是如何自动让SwiftUI重新调用结构体的body属性的。所有这些结合在一起&#x... 查看详情

swiftui之深入解析如何创建列表展示页和导航跳转详情页(代码片段)

...情。地标详情页视图的创建,请参考我的博客:SwiftUI之深入解析如何创建和组合视图。本文将分析如何创建一个可以展示任何地标信息的视图,并动态生成一个可滚动列表,用户可以点击列表项去查看地标的详细... 查看详情

swiftui之深入解析@stateobject@observedobject和@environmentobject的联系和区别(代码片段)

...f1f;状态在任何现代应用程序中都是不可避免的,但在SwiftUI中,重要的是所有的视图都是它们状态的简单函数,我们不需要直接改变视图,而是操纵状态,让状态决定结果。SwiftUI提供了几种在应用程序中存储... 查看详情

python之深入解析box为字典添加高级点符号访问特性(代码片段)

一、前言正常情况下,想访问字典中的某个值,都是通过中括号访问,比如:test_dict="test":"imdbstars":6.7,"length":104print(test_dict["test"]["imdbstars"])#104而通过B 查看详情

python之深入解析numpy的高级操作和使用(代码片段)

一、数组上的迭代NumPy包含一个迭代器对象numpy.nditer,它是一个有效的多维迭代器对象,可以用于在数组上进行迭代。数组的每个元素可使用Python的标准Iterator接口来访问,如下所示:importnumpyasnpa=np.arange(0,60,5)a... 查看详情

深入理解stream之foreach源码解析

...然是JDK1.8。所以,我们有必要聊一聊Java8的一些新特性。深入理解lambda的奥秘深入理解Stream之原理剖析深入理解Stream之foreach源码解析深入浅出NPE神器Optional谈谈接口默认方法与静态方法深入浅出重复注解与类型注解深入浅出JVM元... 查看详情

SwiftUI 视图层次结构中较高的动画覆盖嵌套动画

】SwiftUI视图层次结构中较高的动画覆盖嵌套动画【英文标题】:AnimationshigherintheSwiftUIviewhierarchyoverridingnestedanimations【发布时间】:2020-07-1811:17:50【问题描述】:我有一个SwiftUIView,它有一个重复动画onAppear。当该视图在包含另一... 查看详情

ios之深入探究动画渲染降帧

一、为什么要对动画降帧?众所周知,刷新频率越高体验越好,对于iOSapp的刷新频率应该是越接近越60fps越好,主动给动画降帧,肯定会影响动画的体验。但是另一方面,我们也知道动画渲染的过程中需要... 查看详情

36-vue之echarts高级-动画的使用(代码片段)

ECharts高级-动画的使用前言加载动画增量动画动画的配置前言本篇来学习下ECharts中动画的使用加载动画showLoading():显示加载动画,一般在获取图表数据之前显示加载动画hideLoading():隐藏加载动画,在获取图表数... 查看详情