SwiftUI 中创建反弹动画的实现

 更新时间:2020年10月30日 11:57:46   作者:firerainky  
这篇文章主要介绍了SwiftUI 中创建反弹动画的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

SwiftUI 中的动画

在写动画之前呢先简单回顾一下 SwiftUI 中动画的几个要点:

  • 动画是 view 发生变化时的渐变效果
  • SwiftUI 动画分为隐式动画( .animation() )与显式动画( withAnimation() )两种
  • 隐式动画是给 view 加动画,view 所有的能动画的变化都能被隐式动画影响
  • 显式动画是针对某个变化进行动画,能精准控制。
  • view 的插入和移除通过过渡( transition )来做效果,可以组合多个过渡或自定义过渡
  • 要构建自定义动画,我们需要实现一个可动画的 view 修饰器(遵守 AnimatableModier 协议)或者实现一个 GeometryEffect ,并将可动画的属性通过 animatableData 暴露出来

 反弹动画

反弹动画属于“起始点和终止点相等”的动画,所以不能够通过 SwiftUI 中内建的动画来实现(因为这个 view 从结果来看没有发生变化)

我们先来构建反弹动画修饰器的框架如下:

struct Bounce: AnimatableModifier {
  var animCount: CGFloat = 0
  var amplitude: CGFloat = 10  // 振幅
  
  var animatableData: CGFloat {
    get { animCount }
    set { animCount = newValue }
  }
  
  func body(content: Content) -> some View {
    // change view to animate
  }
}

下面一步一步来

实现动画曲线

实现 body 方法,好让 times 每次增加时 view 能以反弹的方式进行动画。一次反弹就是 view 向上弹起又落下,下面是动画曲线大致的样子:

三角函数是 y = -abs(sin x) ,所以 body 方法这样实现:

struct Bounce: AnimatableModifier {
  ...
  func body(content: Content) -> some View {
    let offset: CGFloat = -abs(sin(animCount * .pi * 2) * amplitude)
    return content.offset(y: offset)
  }
}

测试代码:

struct BouncingView: View {
  @State var taps = 0
  
  var body: some View {
    Button(action: {
      withAnimation(Animation.easeInOut(duration: 1)) {
        taps += 1
      }
    }, label: {
      RoundedRectangle(cornerRadius: 15)
        .modifier(Bounce(animCount: CGFloat(taps)))
    })
    .frame(width: 100, height: 100)
  }
}

点击按钮,就能弹两次了~~

这个 ​ @State var taps

​ 其实并没有什么实际的意义,只是为了触发动画。

因为 SwiftUI 里只有 View 的状态发生变化才会触发动画,而无法通过某个事件来触发;而我们的动画是一个初始状态和结束状态相等的情况,并没有状态的变化,所以这里强行把点击的次数作为 View 状态的变化来触发动画。

我找了好久有没有更优雅的方式来解决这个问题,然而并没有找到 = =b

View 扩展

暴露给外面的 ​ animCount ​ 应该是一个 Int 才对,那么就增加一个 ​ ​animValue ​ 来代替它

struct Bounce: AnimatableModifier {
  let animCount: Int
  var animValue: CGFloat
  var amplitude: CGFloat = 10 // 振幅
  
  init(animCount: Int) {
    self.animCount = animCount
    self.animValue = CGFloat(animCount)
  }
  
  var animatableData: CGFloat {
    get { animValue }
    set { animValue = newValue }
  }
  
  func body(content: Content) -> some View {
    let offset: CGFloat = -abs(sin(animValue * .pi * 2) * amplitude)
    return content.offset(y: offset)
  }
}

因为 ​ ​animValue ​ 被隐藏起来了,所以需要初始化方法

为了方便使用,再添加一个 View 的扩展方法:

extension View {
  func bounce(animCount: Int) -> some View {
    self.modifier(Bounce(animCount: animCount))
  }
}

增加弹性

现在虽然能弹了,但是相对还比较生硬,就想在 View “落地“后再给它增加振幅逐步衰减的几次反弹;一开始尝试了简单的减半反弹,实验证明观感更差,看起来有点难受。

我们实际生活中的反弹的振幅变化应该是符合 阻尼正弦波 的,参考链接里的公式,修改如下:

struct Bounce: AnimatableModifier {
  let animCount: Int
  var animValue: CGFloat
  var amplitude: CGFloat // 振幅
  var bouncingTimes: Int
  
  init(animCount: Int, amplitude: CGFloat = 10, bouncingTimes: Int = 3) {
    self.animCount = animCount
    self.animValue = CGFloat(animCount)
    self.amplitude = amplitude
    self.bouncingTimes = bouncingTimes
  }
  
  var animatableData: CGFloat {
    get { animValue }
    set { animValue = newValue }
  }
  
  func body(content: Content) -> some View {
    let t = animValue - CGFloat(animCount)
    let offset: CGFloat = -abs(pow(CGFloat(M_E), -t) * sin(t * .pi * CGFloat(bouncingTimes)) * amplitude)
    return content.offset(y: offset)
  }
}

extension View {
  func bounce(animCount: Int,
        amplitude: CGFloat = 10,
        bouncingTimes: Int = 3) -> some View {
    self.modifier(Bounce(animCount: animCount,
               amplitude: amplitude,
               bouncingTimes: bouncingTimes))
  }
}

这里我们还增加了 ​ bouncingTimes ​ 作为弹跳次数的参数,振幅也作为参数开放给用户;

由于阻尼正弦波是逐步衰减的,为了每次点击的弹跳都一样,所以得用 ​ ​animValue - CGFloat(animCount) ​ 作为阻尼正弦波函数的参数。

测试代码修改如下:

struct BouncingView: View {
  @State var taps = 0
  
  var body: some View {
    Button(action: {
      withAnimation(Animation.linear(duration: 1)) {
        taps += 1
      }
    }, label: {
      RoundedRectangle(cornerRadius: 15)
        .bounce(animCount: taps)
    })
    .frame(width: 100, height: 100)
  }
}

实际效果如下:

到此这篇关于SwiftUI 中创建反弹动画的实现的文章就介绍到这了,更多相关SwiftUI 反弹动画内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Swift枚举的一些小用法总结

    Swift枚举的一些小用法总结

    这篇文章主要给大家介绍了关于Swift枚举的一些小用法,文中通过示例代码介绍的非常详细,对大家学习或者使用Swift具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-07-07
  • Swift 指针底层探索分析

    Swift 指针底层探索分析

    这篇文章主要为大家介绍了Swift 指针底层探索分析,主要包括指针的基本使用,以及指针的内存绑定进行详细分析,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • 用SwiftUI实现3D Scroll滚动效果的实现代码

    用SwiftUI实现3D Scroll滚动效果的实现代码

    这篇文章主要介绍了用SwiftUI实现3D Scroll效果的实现代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习
    2020-04-04
  • 解析Swift语言面相对象编程中的继承特性

    解析Swift语言面相对象编程中的继承特性

    这篇文章主要介绍了解析Swift语言面相对象编程中的继承特性,是Swift入门学习中的基础知识,需要的朋友可以参考下
    2015-11-11
  • 浅谈在Swift中关于函数指针的实现

    浅谈在Swift中关于函数指针的实现

    这篇文章主要介绍了浅谈在Swift中关于函数指针的实现,是作者根据C语言的指针特性在Swifft中做出的一个实验,需要的朋友可以参考下
    2015-07-07
  • Swift如何在应用中添加图标更换功能的方法

    Swift如何在应用中添加图标更换功能的方法

    本篇文章主要介绍了Swift如何在应用中添加图标更换功能的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02
  • Swift中用到extension的一些基本的扩展功能讲解

    Swift中用到extension的一些基本的扩展功能讲解

    这篇文章主要介绍了Swift的一些基本的扩展功能,即extension关键字的使用,需要的朋友可以参考下
    2015-11-11
  • Swift Set集合及常用方法详解总结

    Swift Set集合及常用方法详解总结

    Set集合为集类型,集是最简单的一种集合,存放于集中的对象不按特定方式排序,只是简单地把对象加入集合中,类似于向口袋里放东西,对集中存在的对象的访问和操作是通过对象的引用进行的,因此在集中不能存放重复对象
    2021-11-11
  • iOS Swift读取本地json文件报错的解决方法

    iOS Swift读取本地json文件报错的解决方法

    只要是app开发者都知道,从服务器端获得的数据要不就是json格式的数据,要么就是xml格式的数据,而这篇文章主要给大家介绍了关于iOS Swift读取本地json文件报错的解决方法,需要的朋友可以参考借鉴,下面来一起学习学习吧。
    2017-11-11
  • Swift实现快速排序算法的代码示例

    Swift实现快速排序算法的代码示例

    这篇文章主要介绍了Swift实现快速排序算法的代码示例,首先利用分治法讲解了快速排序的思路,需要的朋友可以参考下
    2016-07-07

最新评论