Animation
- 通过transition添加和删除视图
- 组合transition
- 创建不对称transition
- 自定义transition
- Text的Animation
- 使用transactions重写动画
- 当动画完成时运行一个回调函数
- 使用相位动画器创建多步动画
概述
文章主要分享SwiftUI Modifier的学习过程,将使用案例的方式进行说明。内容浅显易懂,Animation未做调试结果,不过测试代码是齐全的。如果想要运行结果,可以移步Github下载code -> github案例链接
1、通过transition添加和删除视图
可以在设计中包含和排除一个视图,只需要使用一个常规的判断条件即可。
Swift
struct FFTransitionView: View {
@State private var showDetails = false
@State private var showDetails1 = false
var body: some View {
//例如,点击按钮添加或删除文本
VStack {
Button("Press to show details") {
withAnimation {
showDetails.toggle()
}
}
if showDetails {
Text("Details go here")
}
}
//默认情况下,SwiftUI使用淡出动画来插入或闪促视图,但是想要自定义效果,
//可以使用transition()修饰符来自定义
VStack {
Button("Press to show details") {
withAnimation {
showDetails1.toggle()
}
}
if showDetails1 {
Text("Details go here")
.transition(.move(edge: .bottom))
Text("Details go here")
.transition(.slide)
Text("Details go here")
.transition(.scale)
}
}
}
}
2、组合transition
当添加或删除视图时,SwiftUI可以使用combined(with:)
方法组合过度来制作新的动画。
AnyTransition扩展
为了使组合转换更容易使用和重用,可以在AnyTransition
上创建他们作为扩展
Swift
extension AnyTransition {
static var moveAndScale: AnyTransition {
AnyTransition.move(edge: .bottom).combined(with: .scale)
}
}
combined多组transition
Swift
struct FFTransitionsCombine: View {
@State private var showDetails = false
@State private var showDetails2 = false
var body: some View {
//例如,同时让视图移动和淡出
VStack {
Button("Press to show Details") {
withAnimation {
showDetails.toggle()
}
}
if showDetails {
Text("Details go here")
.transition(AnyTransition.opacity.combined(with: .slide))
}
}
VStack {
Button("Press to show Details") {
withAnimation {
showDetails2.toggle()
}
}
//已经使用扩展声明了函数moveAndscale
if showDetails2 {
Text("Details go here")
.transition(.moveAndScale)
}
}
}
}
3、创建不对称transition
SwiftUI可以在添加一个视图时指定transition,在删除时指定另一个transition。所有这些都是使用asymmetric()
完成的。
Swift
struct FFTransitionAsymmetric: View {
@State private var showDetails = false
var body: some View {
//创建一个使用不对称过渡的文本视图:添加时从左面出现,删除时向下移动
VStack {
Button("Press to show details") {
withAnimation {
showDetails.toggle()
}
}
if showDetails {
Text("Details go here")
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .bottom)))
}
}
}
}
4、自定义transition
虽然SwiftUI自带了一系列的转场动画,也可以自定义transition,这个过程分为三个步骤:
- 创建一个ViewModifier来表示转换的任何状态
- 创建一个AnyTransition扩展,该扩展使用活动状态和标识状态的视图修饰符
- 使用transition()修饰符将动画应用到视图上
具体实操步骤
编写一个形状和视图修饰符组合,模仿Keynote中的虹膜动画:
- 定义一个ScaledCircle形状,它在一个矩形中创建一个圆,该矩形根据一些可动画化的数据进行缩放。
- 创建一个自定义ViewModifier结构体,将任何形状(缩放后的圆)应用于另一个视图的剪辑形状。
- 在其包装在AnyTransition扩展中,以便将该修饰符包装在transition中,以便于调用。
ScaledCircle
Swift
struct ScaledCircle: Shape {
//控制绘制矩形内圆的大小。但为0时,圆不可见,当为1时,圆填充矩形
var animatableData: Double
func path(in rect: CGRect) -> Path {
let maximumCircleRadius = sqrt(rect.width * rect.width + rect.height * rect.height)
let circleRadius = maximumCircleRadius * animatableData
let x = rect.midX - circleRadius / 2
let y = rect.midY - circleRadius / 2
let circleRect = CGRect(x: x, y: y, width: circleRadius, height: circleRadius)
return Circle().path(in: circleRect)
}
}
ClipShapeModifier
通用修饰符,可以剪辑任何形状的视图。
Swift
struct ClipShapeModifier<T: Shape>: ViewModifier {
let shape: T
func body(content: Content) -> some View {
content.clipShape(shape)
}
}
AnyTransition
结合ScaledCircle和ClipShapeModifier的自定义transition
Swift
extension AnyTransition {
static var iris: AnyTransition {
.modifier(
active: ClipShapeModifier(shape: ScaledCircle(animatableData: 0)),
identity: ClipShapeModifier(shape: ScaledCircle(animatableData: 1)))
}
}
使用自定义transition
Swift
struct FFTransitionCustom: View {
@State private var isShowingRed = false
var body: some View {
ZStack {
Color.blue
.frame(width: 200, height: 200)
if isShowingRed {
Color.red
.frame(width: 200, height: 200)
.transition(.iris)
.zIndex(1)
}
}
.padding(50)
.onTapGesture {
withAnimation(.easeInOut) {
isShowingRed.toggle()
}
}
}
}
5、Text的Animation
从iOS16以后,SwiftUI可以将Text动画化。因此像这样的代码可以在两种不同的大小之间苹果的显示动画,自动的重新渲染文本。
Swift
struct FFAnimateTextSize: View {
@State private var fontSize = 32.0
var body: some View {
Text("Hi, metaBBLv")
.font(.custom("Georgia", size: fontSize))
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.5, blendDuration: 1).repeatForever()) {
fontSize = 72
}
}
}
}
6、使用transactions重写动画
SwiftUI提供了一个withTransaction()
函数,可以在运行时重写动画,例如删除隐式动画并用自定义内容替换他。
Swift
struct FFAnimationsOverride: View {
@State private var isZoomed = false
@State private var isZoomed1 = false
@State private var isZoomed2 = false
var body: some View {
VStack {
Button("Toggle zoom") {
isZoomed.toggle()
}
Spacer()
.frame(height: 50)
Text("Zoom text")
.font(.title)
.scaleEffect(isZoomed ? 3 : 1)
.animation(.easeInOut(duration: 2), value: isZoomed)
}
//transactions可以覆盖现有的动画。例如,你可能决定在一个特定情况下,文本的动画以一种款素、线性的方式发生,而不是先有的动画。
//要做到这一点,首先使用想要的动画创建一个新的Transaction实例,然后将他的disablesAnimations值设置为true,这样就可以覆盖任何的现有动画。当一切准备好时,使用withTranscation()。然后继续调整你想要要变的状态。它将使用你的Transcation被动画化。
Spacer()
VStack {
Button("Toggle zoom") {
var transaction = Transaction(animation: .linear)
transaction.disablesAnimations = true
withTransaction(transaction) {
isZoomed1.toggle()
}
}
Spacer()
.frame(height: 50)
Text("Zoom text")
.font(.title)
.scaleEffect(isZoomed1 ? 3 : 1)
.animation(.easeInOut(duration: 2), value: isZoomed1)
}
Spacer()
//对于更多的控制,可以将transaction()修饰符附加到任何视图上。从而可以覆盖该应用在该视图的任何事物。
VStack {
Button("Toggle Zoom") {
var transaction = Transaction(animation: .linear)
transaction.disablesAnimations = true
withTransaction(transaction) {
isZoomed2.toggle()
}
}
Spacer()
.frame(height: 50)
Text("Zoom Text 1")
.font(.title)
.scaleEffect(isZoomed2 ? 3 : 1)
Spacer()
.frame(height: 50)
Text("Zoom Text 2")
.font(.title)
.scaleEffect(isZoomed2 ? 3 : 1)
.transaction { t in
t.animation = .none
}
}
}
}
7、当动画完成时运行一个回调函数
可以选择为SwiftUI的withAnimation()
函数提供完成后的回调,并在动画完成时运行代码。这可能是调整某些程序状态的地方,但可以将其用作将动画连接在一起的简单方法。对一个事物进行动画处理,然后对其他事物进行动画处理。
Swift
struct FFAnimationFinishCallback: View {
@State private var scaleUp = false
@State private var fadeOut = false
@State private var scaleUp1 = false
@State private var fadeOut1 = false
var body: some View {
//点击按钮然后放大并淡出
Button("Tap Me!") {
withAnimation {
scaleUp = true
} completion: {
withAnimation {
fadeOut = true
}
}
}
.scaleEffect(scaleUp ? 3 : 1)
.opacity(fadeOut ? 0 : 1)
//这里有一个小细节可能会让你感觉到震惊,如果你使用弹簧动画,则最后可能会出现很长的运动尾部,其中你的动画正在移动用户无法察觉的。
//默认行为withAnimation()是认为动画完成的,及时荏苒发生微笑运动的肠胃,但如果希望100%完成,可以覆盖默认值。
Button("Tap Me!") {
withAnimation(.bouncy, completionCriteria: .removed) {
scaleUp1 = true
} completion: {
withAnimation {
fadeOut1 = true
}
}
}
.scaleEffect(scaleUp1 ? 3 : 1)
.opacity(fadeOut1 ? 0 : 1)
//对于更复杂的效果,请考虑使用相位动画器而不是动画完成的闭包
}
}
8、使用相位动画器创建多步动画
SwiftUI的phaseAnimator
视图和phaseAnimator修改器可以通过持续或触发时循环选择动画段来执行多步动画,创建这些多阶动画需要三个步骤:
- 定义阶段,可以是任何类型的序列,但使用枚举最简单CaseIterable。
- 读取相位动画中的一个相位,并调整视图以匹配想要该相位的外观。
- 添加一个触发器,使相位动画从头开始重复其序列
enum
Swift
//通过枚举设置
enum AnimationPhase: Double, CaseIterable {
case fadingIn = 0
case middle = 1
case zoomingOut = 3
}
//可以根据你的命令触发动画序列,而不是无休止的重复。为此,可以添加一个触发器值并且通过SwiftUI监视,例如随机数UUID或递增数,当值发生变化时,开始动画并完整播放。
enum AnimationPhase1: CaseIterable {
case start, middle, end
}
//向动画增加计算属性
enum AnimationPhase2: CaseIterable {
case fadingIn, middle, zoomingOut
var scale: Double {
switch self {
case .fadingIn: 0
case .middle: 1
case .zoomingOut: 3
}
}
var opacity: Double {
switch self {
case .fadingIn: 0
case .middle: 1
case .zoomingOut: 0
}
}
}
使用上面的enum做的多种例子
Swift
struct FFAnimationmulti_step: View {
@State private var animationStep = 0
@State private var animationStep1 = 0
var body: some View {
//例如,创建一个简单动画,使某些文本开始很小且不可见,放大到自然大小并完全不透明,然后放大到非常大且不可见。
Text("Hi, metaBBLv")
.font(.largeTitle)
.phaseAnimator([0,1,3]) { view, phase in
view
.scaleEffect(phase)
.opacity(phase == 1 ? 1 : 0)
}
//由于没有提供触发器,所以它将永远运行。
//使用包装视图PhaseAbunator来编写它,它的优点是多个视图可以在阶段之间一起移动
VStack(spacing: 50) {
PhaseAnimator([0,1,2]) { value in
Text("Hi, metaBBLv")
.font(.largeTitle)
.scaleEffect(value)
.opacity(value == 1 ? 1 : 0)
Text("Goodbye, metaBBLv")
.font(.largeTitle)
.scaleEffect(3 - value)
.opacity(value == 1 ? 1 : 0)
}
}
Text("Hi, metaBBLv")
.font(.largeTitle)
.phaseAnimator(AnimationPhase.allCases) { view, phase in
view
.scaleEffect(phase.rawValue)
.opacity(phase.rawValue == 1 ? 1 : 0)
}
//在下面的例子中,点击按钮会触发使用枚举情况的三步动画。首先,定义所需的各种动画阶段,然后每当属性发生变化时都会遍历。
Button("Tap Me!") {
animationStep += 1
}
.font(.largeTitle)
.phaseAnimator(AnimationPhase1.allCases, trigger: animationStep) { content, phase in
content
.blur(radius: phase == .start ? 0 : 10)
.scaleEffect(phase == .middle ? 3 : 1)
}
//为了获得更多的控制,可以准确指定每个阶段使用那个动画。.bouncy在快速和慢速动画之间移动.easeInOut获得更多的变化。
Button("Tap Me 1") {
animationStep1 += 1
}
.font(.largeTitle)
.phaseAnimator(AnimationPhase1.allCases, trigger: animationStep1) { content, phase in
content
.blur(radius: phase == .start ? 0 : 10)
.scaleEffect(phase == .middle ? 3 : 1)
} animation: { phase in
switch phase {
case .start, .end: .bouncy
case .middle: .easeInOut(duration: 2)
}
}
//由于通过计算属性添加了动画,所以在调用时更加的简洁
Text("Hi, metaBBLv")
.font(.largeTitle)
.phaseAnimator(AnimationPhase2.allCases) { content, phase in
content
.scaleEffect(phase.scale)
.opacity(phase.opacity)
}
}
}
关于Animation
先对对于认真对照demo练习的小伙伴,说声sorry。关于Animation这部分代码拆分的不好,尝试过将其分开,像前面的部分一样,但是也考虑到一些小伙伴所见即所得,就贴了整块的代码,CV即见结果。在另外一个角度,Animation相关代码确实相对较多,复杂度一般,但是涉及了一些extension和独立struct的运用,个人能力有限,没找啥太好的办法完美呈现。最后想说,我也是练习过程,意在熟能生巧,希望同学见谅。