共计 3075 个字符,预计需要花费 8 分钟才能阅读完成。
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)
}
}
实际效果如下: