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 反弹动画内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
代码知识SEO上一篇 : python boto和boto3操作bucket的示例
下一篇 : 利用vue3+ts实现管理后台(增删改查)
-
SEO外包最佳选择国内专业的白帽SEO机构,熟知搜索算法,各行业企业站优化策略!
SEO公司
-
可定制SEO优化套餐基于整站优化与品牌搜索展现,定制个性化营销推广方案!
SEO套餐
-
SEO入门教程多年积累SEO实战案例,从新手到专家,从入门到精通,海量的SEO学习资料!
SEO教程
-
SEO项目资源高质量SEO项目资源,稀缺性外链,优质文案代写,老域名提权,云主机相关配置折扣!
SEO资源
-
SEO快速建站快速搭建符合搜索引擎友好的企业网站,协助备案,域名选择,服务器配置等相关服务!
SEO建站
-
快速搜索引擎优化建议没有任何SEO机构,可以承诺搜索引擎排名的具体位置,如果有,那么请您多注意!专业的SEO机构,一般情况下只能确保目标关键词进入到首页或者前几页,如果您有相关问题,欢迎咨询!