Animation
- 创建基本动画
- 创建Spring Animation
- 通过绑定值来监听动画变化
- 创建显式动画
- 创建延时动画
- 在视图出现后立刻启动动画
- 将多个动画应用到视图
- 使用matchedGeometryEffect()将动画从一个视图同步到另一个视图
概述
文章主要分享SwiftUI Modifier的学习过程,将使用案例的方式进行说明。内容浅显易懂,Animation未做调试结果,不过测试代码是齐全的。如果想要运行结果,可以移步Github下载code -> github案例链接
1、创建基本动画
SwiftUI通过animation()修饰符内置了对动画的支持。要使用此修饰符,请将其放置在视图的任何其他修饰符之后,告诉他想要的动画类型,并确保将其附加到特定的值,以便仅在该特定值更改时触发动画。
struct FFBasicAnimations: View {
@State private var scale = 1.0
@State private var scale2 = 1.0
@State private var angle = 0.0
@State private var borderThickness = 1.0
var body: some View {
//例如,创建一个Button,每次点击,缩放+1
Button("Press here") {
scale += 1
}
.scaleEffect(scale)
.animation(.linear(duration: 1), value: scale)
Divider()
//动画的发生时间是1s,但如果你不想为动画指定精确的时间,可以使用.linear
//重要提示:从iOS17以及更高版本以后,SwiftUI使用spring动画,在此之前默认动画是线性动画
//例如,创建缩放效果动画,开始缓慢,然后变得快速
Button("Prese here") {
scale2 += 1
}
.scaleEffect(scale2)
.animation(.easeIn, value: scale)
Divider()
//还有其他很多动画修饰符,比如2D和3D旋转,不透明度,边框等
//创建一个按钮以及旋转动画,添加border,随着动画宽度+1
Button("Press here") {
angle += 45
borderThickness += 1
}
.padding()
.border(.red, width: borderThickness)
.rotationEffect(.degrees(angle))
.animation(.easeIn, value: angle)
}
}
除了简单的线性动画,还可以在各种内置选项中指定曲线:
- .easein开始缓慢,然后加速,直到结束
- .easeout还是很快,接近结束时减速,直到结束
- .easeinout开始很慢,在中间加速,然后在接近结束时减速
- .smooth是一个没有弹跳的spring动画(iOS17)
- .snappy是一个带有小弹跳的spring动画(iOS17)
- .bouncy是一个有中等弹跳的spring动画(iOS17)
或者,可以通过指定.timingCurve来控制point
2、创建Spring Animation
SwiftUI内置了对spring动画的支持,这些动画会移动到目标点,然后反弹回来,简言之就是个弹性动画。如果只使用.spring()本身,不带参数,则会得到一个合理的默认值。
struct FFAnimationSpring: View {
@State private var angle: Double = 0.0
@State private var angle2: Double = 0.0
@State private var scaless = 1.0
var body: some View {
//创建一个弹簧动画,每次点击将其旋转45度
Button("Press here") {
angle += 45
}
.padding()
.rotationEffect(.degrees(angle))
.animation(.spring, value: angle)
//如果你希望对spring动画进行颗粒度控制,可以添加参数:
//如果需要支持iOS16以及更早版本,需要指定物体的质量,弹簧的硬度,弹性减缓速度,以及在启动时开始移动的速度。
//如果只需要支持iOS17以及以后,可以指定想要的弹力持续的时间,还可以选择添加想要的弹力和混合效果。
Button("Press here") {
angle2 += 45
}
.padding()
.rotationEffect(.degrees(angle2))
.animation(.interpolatingSpring(mass: 1,stiffness: 1, damping: 0.5, initialVelocity: 10), value: angle2)
//注意:这是一个插值弹簧,这意味着如果出发了几次动画,弹簧效果会随着弹簧的结合而变得越来越强
//这段代码或多或少做相同的事情,使用与iOS17以及跟高版本兼容
Button("Press here") {
scaless += 1
}
.padding()
.scaleEffect(scaless)
.animation(.spring(duration: 1, bounce: 0.75), value: scaless)
}
}
3、通过绑定值来监听动画变化
SwiftUI的双向绑定可以调整程序的状态,可以通过调整视图层次来做出响应。例如,控制一些文本的隐藏或者视图的透明度等等。可以通过在绑定中添加animation(),而不是让状态立即发生变化,从而为绑定修改引起的变化过程制作动画。
struct FFAnimateBindingValues: View {
@State private var showingWelcome = false
@State private var showingWelcome1 = false
@State private var showingWelcome2 = false
var body: some View {
//例如,创建一个开关,切换状态来控制视图的显示和隐藏。
Toggle("Toggle label - normal", isOn: $showingWelcome)
.padding()
if showingWelcome {
Text("Hi, metaBBLv")
}
//如果没有动画,文本视图在点击的时候立即出现/消失,这将导致视觉跳动。如果修改这个切换,将它绑定到$showingWelcome,那么文本视图将平滑的出现。
Toggle("Toggle label - animation", isOn: $showingWelcome1.animation())
.padding()
if showingWelcome1 {
Text("Hi, metaBBLv - animation")
}
//如果想要更丰富的动画效果,可以给animation()传递参数来控制,比如,添加一个spring()动画
Toggle("Toggle label - animation(spring)", isOn: $showingWelcome2.animation(.spring))
.padding()
if showingWelcome2 {
Text("Hi, metaBBLv - animation(spring)")
}
}
}
4、创建显式动画
如果将动画附加到视图上,最终会得到一个隐式动画。改变视图中的其他地方的一些状态可能会使用动画,即使只是增加一个整数或者切换一个bool值。另外一种时显式动画,在这种情况下,不给有问题的视图附加修饰符,而是让SwiftUI对想要做的精确的更改进行动画化。为此,将更改封装在withAnimation()中。
struct FFAnimationExlicit: View {
@State private var opacity = 1.0
var body: some View {
Button("Press here") {
withAnimation {
opacity -= 0.2
}
}
.padding()
.font(.title)
.background(.green)
.opacity(opacity)
}
}
withAnimation()也可以接受参数,指定动画类型,因此也可以创建一个3s的线性动画withAnimation(.linear(duration: 3))
显式动画通常很有用,因为他们会使每个受影响的视图都产生动画,而不是仅仅是那些附加了隐式动画的视图。
5、创建延时动画
当想创建任何动画时(隐式、显示、绑定),可以给动画附加修饰符来调整。例如,如果想让动画在一定的时间后开始,那么就需要使用delay()修饰符。
struct FFAnimationDelay: View {
@State private var rotaitonDelay = 0.0
var body: some View {
//例如,创建一个红色矩形,当点击时,在1s后在2s时间内旋转360度。
Rectangle()
.fill(.red)
.frame(width: 200, height: 200)
.rotationEffect(.degrees(rotaitonDelay))
.animation(.easeInOut(duration: 3).delay(1), value: rotaitonDelay)
.onTapGesture {
rotaitonDelay += 360
}
}
}
6、在视图出现后立刻启动动画
如果想要一个SwiftUI视图,应该使用onApppear()修饰符来附加动画。
//创建自定扩展:即时动画
extension View {
func animate(using animation: Animation = .easeInOut(duration: 1), _ action: @escaping () -> Void) -> some View {
onAppear {
withAnimation(animation) {
action()
}
}
}
}
//即时循环动画
extension View {
func animateForever(using animation: Animation = .easeInOut(duration: 1), autorecerses: Bool = false, _ action: @escaping () -> Void) -> some View {
let repeated = animation.repeatForever(autoreverses: autorecerses)
return onAppear {
withAnimation(repeated) {
action()
}
}
}
}
struct FFAnimationViewAppears: View {
@State var scale = 1.0
@State var scale2 = 1.0
var body: some View {
//创建一个结合动画(放大和缩小连续),类似呼吸效果
Circle()
.frame(width: 200, height: 200)
.scaleEffect(scale)
.onAppear(perform: {
let baseAnimation = Animation.easeInOut(duration: 1)
let repeated = baseAnimation.repeatForever(autoreverses: true)
withAnimation(repeated) {
scale = 0.5
}
})
//如果打算频繁的添加初始动画,那么使用视图协议添加扩展是很好的方式。
//通过扩展创建动画
Circle()
.frame(width: 200, height: 200)
.scaleEffect(scale2)
.animateForever(autorecerses: true) {
scale2 = 0.5
}
}
}
7、将多个动画应用到视图
使用SwiftUI的animation()修饰符的顺序会影响那些修饰符会被动画化。也可以添加多个animation修饰符来设置不同的动画。
struct FFAnimationMultiple: View {
@State private var isEnabled = false
var body: some View {
Button("Press me") {
isEnabled.toggle()
}
.foregroundStyle(.white)
.frame(width: 200, height: 200)
.animation(.easeInOut(duration: 1)) { content in
content
.background(isEnabled ? .green : .red)
}
.animation(.easeInOut(duration: 2)) { content in
content
.clipShape(.rect(cornerRadius: isEnabled ? 100 : 0))
}
}
}
8、使用matchedGeometryEffect()将动画从一个视图同步到另一个视图
如果相同的视图出现在视图层次结构中的两个不同部分,并且想要在他们之间进行动画。那么从列表视图切换到缩放的细节,就要使用matchedGeometryEffect()修饰符。类似Keynote中的majic move。
若要使用修饰符,将其附加到层次结构中的不同部分的一对相同的视图上。两个视图状态之间切换时,会发现SwiftUI平滑的激活了同步视图。
struct FFAnimationMatchedGeometryEffect: View {
@State private var isFlipped = false
@Namespace private var animation
@State private var isFlipped1 = false
@Namespace private var appleMusic
@State private var isZoomed = false
var frame: Double {
isZoomed ? 300 : 44
}
var body: some View {
//一个视图状态中有一个红色圆和文本,另外一个视图状态中,圆在文本之后并改变颜色
VStack {
if isFlipped {
Circle()
.fill(.red)
.frame(width:44, height: 44)
Text("metaBBLv - Keep loving")
.font(.headline)
} else {
Text("metaBBLv - Keep loving")
.font(.headline)
Circle()
.fill(.blue)
.frame(width: 44, height: 44)
}
}
.onTapGesture {
withAnimation {
isFlipped.toggle()
}
}
//其实实现的动画效果就是文字在点击之后移动到图的下方,但是显然这种方式很愚蠢。
//有更好的方式,首先,需要使用@Namespace属性包装器为视图创建一个全局命名空间。在实践中,这只不过是视图上的一个属性,但在原理上,可以将视图链接在一起。
//所以,添加一个animation属性:@Namespace private var animation.
//接下来,添加matchedGeometryEffect动画效果。
VStack {
if isFlipped1 {
Circle()
.fill(.red)
.frame(width: 44, height: 44)
.matchedGeometryEffect(id: "Shape", in: animation)
Text("metaBBLv - Keep loving")
.matchedGeometryEffect(id: "AlbumTitle", in: animation)
.font(.headline)
} else {
Text("metaBBLv - Keep loving")
.matchedGeometryEffect(id: "AlbumTitle", in: animation)
.font(.headline)
Circle()
.fill(.red)
.frame(width: 44, height: 44)
.matchedGeometryEffect(id: "Shape", in: animation)
}
}
.onTapGesture {
withAnimation {
isFlipped1.toggle()
}
}
//在实现一个更高级的例子,关于Apple Music的专辑显示风格,点击时可以将小视图扩展到更大的视图。
VStack {
// Spacer()
VStack {
HStack {
RoundedRectangle(cornerRadius: 10)
.fill(.blue)
.frame(width: frame, height: frame)
.padding(.top, isZoomed ? 20 : 0)
if isZoomed == false {
Text("metaBBLv - Keep loving")
.matchedGeometryEffect(id: "AlbumTitle", in: appleMusic)
.font(.headline)
Spacer()
}
}
if isZoomed == true {
Text("metaBBLv - Keep loving")
.matchedGeometryEffect(id: "AlbumTitle", in: appleMusic)
.font(.headline)
.padding(.bottom, 60)
Spacer()
}
}
.onTapGesture {
withAnimation {
isZoomed.toggle()
}
}
.padding()
.frame(maxWidth: .infinity)
.frame(height: 400)
.background(Color(white: 0.9))
.foregroundStyle(.black)
}
}
}