版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.12.11 星期三 |
前言
今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)
2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)
4. SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)
5. SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)
6. SwiftUI框架详细解析 (六) —— 基于SwiftUI的导航的实现(一)
7. SwiftUI框架详细解析 (七) —— 基于SwiftUI的导航的实现(二)
开始
首先看下主要内容
主要内容:在本教程中,您将学习如何使用SwiftUI添加精美的动画。 您将从基本
(basic)
动画过渡到复杂的自定义spring
动画。
接着看下写作环境
Swift 5, iOS 13, Xcode 11
SwiftUI
带来了大量的新功能,并且为Apple的所有平台编写UI代码提供了更简便的方法。作为额外的好处,它还具有一种新的状态转换动画方法。
如果您习惯使用UIView
动画,则可能会发现它们更易于编写和理解。如果您要使用SwiftUI
,那么恭喜您!这将使您轻松使用它。
在本教程中,您将学习SwiftUI
动画的基础知识,包括:
-
animation
修改器。 -
withAnimation
,该函数可让您对状态更改进行动画处理。 - 自定义动画。
最好使用Xcode 11.2.1
或更高版本,该版本包含SwiftUI
代码中已知动画bug
的修复程序。如果您运行的是macOS Catalina
,则时间会更短一些,因为Xcode会在Canvas
窗格中并排显示代码和实时预览。
但是,如果您仍然有Mojave
,那也可以;您可以构建并运行以查看代码更改如何影响应用程序。实际上,由于预览窗口可能有问题,因此该技术在Catalina
上仍然很方便。
打开已有项目。
应用MySolarSystem
是对太阳系中行星的简单清单(假设您正在地球上阅读)。 完成后,点按一下即可显示每个行星,并列出该行星最大的卫星。 从那里轻敲月球将为用户提供有关行星的其他信息。
ContentView
代表应用程序的主屏幕。 它是一个由Planets
数组构成表格的列表。 列表中的每个元素都是一个带有多个控件的HStack
:一个用于行星图片的Image
,一个用于行星名称的Text
,一个Spacer
和一个按钮Button
(如果该行星上有卫星)。 此按钮的目的是显示有关卫星的信息。
在本教程的后面,您将把可选的MoonList
放入VStack
。 点击月亮按钮将显示或隐藏它。 这将是您要添加并设置动画的第一件事。
1. Preview Window
如果您在macOS Catalina
上运行,则可以在编辑器窗格中访问Canvas
。 如果看不到它,请选择Editor ▸ Canvas
以启用它。 更改代码后,将显示视图和实时更新。
如果单击Live Preview
按钮(又称播放图标),它将使预览具有交互性。 添加动画和交互性后,您就可以单击按钮并观察状态变化。
注意:在撰写本文时,预览窗口并非
100%
可靠。 如果预览停止更新,请检查Canvas
顶部是否显示以下消息:Automatic preview updating paused
。 如果看到,只需单击Resume
即可重建预览。 另外,如果动画似乎无法正常运行,请通过在模拟器或设备上运行该应用进行仔细检查。 实时应用是任何UI行为的真实来源。
Basic Animations
要添加动画,您需要做的第一件事就是制作动画。 因此,首先,您将创建一个状态更改,以触发对UI的更新。
打开ContentView.swift
并将以下内容添加到makePlanetRow(planet :)
中的VStack
的末尾:
if self.toggleMoons(planet.name) {
MoonList(planet: planet)
}
这会检查toggleMoons(_ :)
,以确定是否应该切换该星球的行。 如果打开了卫星,则MoonStack
视图将出现在VStack
中。 此方法与state
属性showMoon
绑定。
接下来,通过设置showMoon
属性完成操作。 在按钮的操作回调中,添加以下代码:
self.showMoon = self.toggleMoons(planet.name) ? nil : planet.name
如果用户已经按下按钮,此代码将清除showMoon
;如果用户选择其他行,则将其设置为新的行星。
生成并运行该应用程序。 轻触带有“moon disclosure”
图标的行,将显示卡通地图以及该行星最大的一些卫星的列表。 该按钮的大小也加倍,以引起人们的注意。 这样,用户便知道在何处点击以关闭该行。
这使月亮列表视图内外闪烁,这几乎不是很棒的用户体验。 幸运的是,添加一些动画很容易。
首先为按钮尺寸创建平滑的动画。 在月球图像Image
上的scaleEffect
修改器之后,添加以下修改器:
.animation(.default)
这会将默认动画添加到缩放效果。 现在,如果您展开或折叠一行,则按钮将平滑地增大或缩小。 如果您发现月亮视图的瞬时外观和消失有一些问题,请不要担心。 您将在下面的Animating State Changes
部分中对此进行修复。
1. Animation Timing
animation(_ :)
修饰符采用Animation
参数。
您可以将多个选项用于动画。 基本的是简单的时序曲线,它们描述了动画速度在持续时间内如何变化。 这些都修改了变化的属性,在起始值和结束值之间平滑地插值。
您有以下选择:
-
.linear
:随着时间的推移,它将属性从开始值到结束值均匀过渡。 这是重复动画的良好时序曲线,但看起来不像eased function
那样自然。
-
.easeIn
:缓动动画从慢速开始,并随着时间的推移加快速度。 这对于从静止点开始并在屏幕外结束的动画很有用。
-
.easeOut
:缓动的动画开始快而结束慢。 这对于使物体达到稳态或最终位置具有动画效果是很有用的。
-
.easeInOut
:缓入和缓出曲线结合了easyIn
和easeOut
。 这对于从一个稳定点开始并在另一个平衡点处结束的动画很有用。 这是大多数应用程序的最佳选择。 这就是为什么这是.default
使用的时序曲线的原因。
-
.timingCurve
:这使您可以指定自定义时序曲线。 这很少需要,并且不在本教程的讨论范围之内。
大多数情况下,.default
足以满足您的需求。 如果您还需要其他一些功能,则基本的计时功能之一将为您提供一些额外的改进。 通过将.default
替换为其中的一种来进行尝试,以找出最适合您的选择。
上图显示了每个时序曲线随时间的速度。 但是动画将如何实际显示? 将每个曲线应用到月亮按钮的scaleEffect
时,这是随着时间变化的外观。
2. Tinkering With Timing
如果您看不到细微的差异,可以通过花费更多时间来放慢动画的播放速度。 将animation
修改器替换为:
.animation(.easeIn(duration: 5))
指定更长的持续时间将使时序曲线更明显。
在模拟器中而不是SwiftUI
预览窗口中构建和运行的一个优点是,您可以启用Debug ▸ Slow Animations
标志。 这将大大降低任何动画的速度,因此您可以更清楚地看到细微的差异。 这样,您无需添加额外的duration
参数。
除了时序曲线和持续时间外,您还可以使用其他一些控制杆来控制动画的时序。 首先,有速度。
在ContentView
的顶部创建一个新属性:
let moonAnimation = Animation.easeInOut.speed(0.1)
该常量仅存储动画,因此您以后可以在代码中更轻松地使用它。 它还为您提供了一个改变周围事物的场所。
接下来,替换Image
修改器:
.animation(moonAnimation)
现在,月亮的大小将非常缓慢地调整。 尝试将速度更改为2
,动画将变得非常活泼。
除了速度之外,您还可以添加延迟delay
。 将moonAnimation
更改为:
let moonAnimation = Animation.easeInOut.delay(1)
现在,动画具有一秒钟的延迟。 在月亮列表中点击该行闪烁,然后稍后,该按钮将更改大小。 一次为多个属性或对象设置动画时,Delay
最为有用,这将在后面介绍。
最后,您可以使用修饰符来重复动画。 将动画更改为:
let moonAnimation = Animation.easeInOut.repeatForever(autoreverses: true)
现在,该按钮将永远跳动。 您还可以使用repeatCount(autoreverses :)
重复动画有限的次数。
完成实验后,将动画设置回:
let moonAnimation = Animation.default
3. Simultaneous Animations
.animation
是一个修饰符,可像其他修饰符一样叠加到SwiftUI
视图上。 如果视图具有多个变化的属性,则单个Animation
可以应用于所有这些属性。
通过将其放置在Image
和scaleEffect
线之间,添加以下旋转效果:
.rotationEffect(.degrees(self.toggleMoons(planet.name) ? -50 : 0))
这会使moon
按钮略微旋转,因此当出现“月亮”视图时,新月形会横向排列。 由于将动画添加到最后,这些动画将一起动画。
当然,您可以为每个属性指定单独的动画。 例如,在rotationEffect
修改器之后添加以下修改器:
.animation(.easeOut(duration: 1))
这为旋转提供了一秒钟的动画,因此与缩放效果相比,您会注意到旋转结束的时间稍晚一些。 接下来,将moonAnimation
更改为:
let moonAnimation = Animation.default.delay(1)
这会将大小动画延迟一秒,因此缩放在旋转完成后开始。 试试看。
最后,可以通过为动画指定nil
来选择不为特定属性更改设置动画。 将rotationEffect
动画更改为:
.animation(nil)
并将moonAnimation
更改为:
let moonAnimation = Animation.default
现在,仅动画大小。
Animating State Changes
您花了一段时间完善了“月亮”按钮的动画效果,但是会出现在所有圆圈中的大视野呢?
好了,您可以使用简单的withAnimation
块为任何block
转换设置动画。 将按钮操作块的内容替换为:
withAnimation {
self.showMoon = self.toggleMoons(planet.name) ? nil : planet.name
}
withAnimation
明确告诉SwiftUI
要设置动画。 在这种情况下,它将对showMoon
和具有依赖于该属性的任何视图的切换进行动画处理。
如果您现在观看动画,将会看到月亮视图使用默认的出现动画逐渐消失,并且列表行滑出以腾出空间。
您还可以向显式动画块提供特定的动画。 用以下内容替换withAnimation
:
withAnimation(.easeIn(duration: 2))
现在,它使用持续时间为两秒的缓动动画,而不是default
。
如果您尝试一下,放慢的动画可能看起来不正确。 有更好的方法可以对进出层次结构的视图进行动画处理。
在继续之前,将withAnimation
更改为:
withAnimation(.easeInOut)
1. Transitions
Transitions
介绍了如何插入或删除视图。 要查看其工作原理,请将此修饰符添加到if self.toggleMoons(planet.name)
块中的MoonList
中:
.transition(.slide)
现在,视图将滑入而不是淡入。从前缘滑入(Slide)
动画,然后从尾随滑出。
Transitions
属于AnyTransition
类型。 SwiftUI
带有一些预制的过渡:
-
.slide
:您已经在使用中看到了它-将视图从侧面滑入。 -
.opacity
:此过渡使视图淡入和淡出。 -
.scale
:通过放大或缩小视图进行动画处理。 -
.move
:类似于slide
,但是可以指定边缘。 -
.offset
:沿任意方向移动视图。
继续尝试这些过渡,以了解它们的工作方式。
2. Combining Transitions
您还可以结合过渡来组成自己的自定义效果。 在ContentView.swift
的顶部,添加以下扩展名:
extension AnyTransition {
static var customTransition: AnyTransition {
let transition = AnyTransition.move(edge: .top)
.combined(with: .scale(scale: 0.2, anchor: .topTrailing))
.combined(with: .opacity)
return transition
}
}
它结合了三个过渡:从顶部边缘移动,从20%锚定到顶部的scale
以及不透明的淡入淡出效果。
要使用它,请将MoonList
实例的transition
行更改为:
.transition(.customTransition)
构建并运行您的项目,然后尝试打开月球列表。 组合的效果就像视图从“月亮”按钮突然进出一样。
3. Asynchronous Transitions
如果需要,还可以使入口过渡与出口过渡不同。
在ContentView.swift
中,将customTransition
的定义替换为:
static var customTransition: AnyTransition {
let insertion = AnyTransition.move(edge: .top)
.combined(with: .scale(scale: 0.2, anchor: .topTrailing))
.combined(with: .opacity)
let removal = AnyTransition.move(edge: .top)
return .asymmetric(insertion: insertion, removal: removal)
}
这样可以保持插入状态,但是现在,月亮视图通过移动到顶部退出屏幕。
Springs
现在,卫星列表仅显示卫星以同心圆彼此堆叠。 如果它们间隔开,甚至可以动画进去,看起来会更好。
在MoonView.swift
中,将以下修饰符添加到body
链的末尾:
.onAppear {
withAnimation {
self.angle = self.targetAngle
}
}
这将在代表月亮的橙色球上设置一个随机角度。 然后,月亮将从直线上的零度动画到新位置。
该动画效果还不错,因此您需要再添加一点点爵士乐。 Spring
动画使您可以添加一些弹跳和跳动,以使视图生动活泼。
在SolarSystem.swift
中,将此方法添加到SolarSystem
中:
func animation(index: Double) -> Animation {
return Animation.spring(dampingFraction: 0.5)
}
此辅助方法创建一个Spring
动画。
然后,在makeSystem(_ :)
的最后一个ForEach
中,将以下修饰符附加到self.moon
行:
.animation(self.animation(index: Double(index)))
这会将动画添加到每个月球圆圈。 现在不用担心index
; 您将在下一部分中使用它。
下次加载视图时,随着卫星到达其最终位置,将会有一些反弹。
1. Using SwiftUI’s Pre-Built Spring Options
像SwiftUI
中的所有其他内容一样,spring
有一些内置选项:
-
spring()
:默认的弹簧行为。 这是一个很好的起点。 -
spring(response:dampingFraction:blendDuration)
:带有更多选项以微调其行为的弹簧动画。 -
interpolatingSpring(mass:stiffness:damping:initialVelocity :)
:一种非常可定制的基于物理建模的弹簧。
Spring
动画使用弹簧的真实世界作为基础。 想象一下,如果可以的话,请在弹簧线圈的末端安装一个重块。 如果该块的质量较大,则释放弹簧会导致其下落时产生较大的位移,从而使其弹跳得越来越远。
弹簧越硬,其作用就相反-弹簧越硬,它将移动得越远。 增大阻尼就像增大摩擦:它将减少行程并缩短弹跳时间。 初始速度设置动画的速度:更大的速度将使视图移动更多,并使其弹跳时间更长。
很难将物理原理映射到动画的实际工作方式。这就是为什么每次都使用一个参数以获得漂亮的动画非常有用。
这也是为什么有spring(response:dampingFraction:blendDuration)
的原因,它是编写spring
动画的一种更容易理解的方式。在幕后,它仍然使用物理模型,但杠杆较少。
阻尼(dampingFraction)
阻尼控制视图反弹的时间。零阻尼是一个无阻尼的系统,这意味着它将永远反弹。大于1的阻尼阻尼值根本不会弹起。如果您使用弹簧,通常会选择一个介于0到1之间的值。较大的值会降低速度。
响应(response)
是完成一次振荡的时间量。这将控制动画的持续时间。这两个值协同工作以调整视图的距离和速度,并且经常会反弹视图。如果更改response
或组合多个弹簧,blendDuration
将影响动画。这是一种高级机动。
2. Refining the Animation
现在您对弹簧的工作原理有了更好的了解,请花点时间整理动画。 为一堆相对独立的视图制作动画时,一件好事是给它们一些稍有不同的时间。 这有助于使动画更生动。
在SolarSystem.swift
中,将animation(index :)
更改为以下内容:
func animation(index: Double) -> Animation {
return Animation
.spring(response: 0.55, dampingFraction: 0.45, blendDuration: 0)
.speed(2)
.delay(0.075 * index)
}
这给每个“月球”增加了滚动延迟,因此它们彼此之间在75
毫秒后开始下降,从而产生了一些奇特的效果。
Animatable
这看起来不错,但如果卫星沿轨道移动而不是从顶部弹起,那将更加现实。 您可以通过使用自定义修饰符GeometryEffect
来实现。 几何效果描述了一种修改视图位置,形状或大小的动画。
将以下结构体添加到SolarSystem.swift
:
struct OrbitEffect: GeometryEffect {
let initialAngle = CGFloat.random(in: 0 ..< 2 * .pi)
var percent: CGFloat = 0
let radius: CGFloat
var animatableData: CGFloat {
get { return percent }
set { percent = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let angle = 2 * .pi * percent + initialAngle
let pt = CGPoint(
x: cos(angle) * radius,
y: sin(angle) * radius)
return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y))
}
}
此OrbitEffect
将其视图绕一圈移动。它通过GeometryEffect
的effectValue(size :)
方法实现。此方法返回一个ProjectionTransform
,它是一个坐标变换。在这种情况下,将使用平移CGAffineTransform
创建平移,该平移对应于圆上的一个点。
GeometryEffect
的神奇之处在于它符合Animatable
。这意味着它需要一个名为animatableData
的属性。在这种情况下,它包装了一个名为percent
的CGFloat
属性。动画数据可以是符合VectorArithmetic
的任何值,这意味着可以在起始值和结束值之间进行插值。为此,您将选择0%
到100%
之间的动画。
通过计算0到2π
之间的相对值,可以将百分比应用于effectValue
。还有一个随机的initialAngle
。这样,每个“月球”将从不同的位置开始;否则,将它们全部紧紧地锁定似乎很奇怪。
要应用此效果,您需要稍微更改SolarSystem
视图。首先,向其添加一个新的state
属性:
@State private var animationFlag = false
动画是根据状态变化而发生的。 由于存在视图时轨道动画将保持不变,因此此简单的布尔值将充当动画的触发器。
接下来,添加此辅助方法:
func makeOrbitEffect(diameter: CGFloat) -> some GeometryEffect {
return OrbitEffect(
percent: self.animationFlag ? 1.0 : 0.0,
radius: diameter / 2.0)
}
这将创建一个OrbitEffect
,将其百分比从0
开始,并在切换标志后将其更改为1
。
要调用此方法,请在单个月亮ForEach
中,将.animation
修饰符替换为以下内容:
.modifier(self.makeOrbitEffect(
diameter: planetSize + radiusIncrement * CGFloat(index)
))
.animation(Animation
.linear(duration: Double.random(in: 10 ... 100))
.repeatForever(autoreverses: false)
)
通过使用辅助方法,这会将OrbitEffect
应用于每个月圆。 它还应用了随机时间重复的线性动画。 这样,每个月球将以各自独立的速度无限期绕轨道运行。 这不是行星的逼真的视图,但看起来很漂亮。
接下来,替换moon(planetSize:moonSize:radiusIncrement:index :)
的实现,如下所示:
func moon(planetSize: CGFloat,
moonSize: CGFloat,
radiusIncrement: CGFloat,
index: CGFloat) -> some View {
return Circle()
.fill(Color.orange)
.frame(width: moonSize, height: moonSize)
}
以前的辅助方法使用MoonView
,该方法用于将自身放置在正确的半径处。 现在,OrbitEffect
处理此放置,因此更改辅助函数可以消除冲突。
最后,在ZStack
的makeSystem(_ :)
末尾,应用以下修饰符:
.onAppear {
self.animationFlag.toggle()
}
这将触发该标志,并在显示视图时开始播放动画。
现在,再次构建并运行。 现在,月球将以不同的速度绕地球旋转。
1. An Alternate Look at Animatable Data
在上一步中,您通过将百分比从0更改为1来使效果动画起来。这是对任何事物进行动画处理的良好通用策略。 例如,您可以对进度条执行相同操作,并使用GeometryEffect
在整个屏幕上扩展该条的宽度。
或者,您可以直接为角度设置动画并保存一个步骤。
在OrbitEffect
中,将percentage
重命名为angle
,现在将其从0设置为2π
。 确保您也用animatableData
定义的angle
n
替换了percentage
。
接下来,将effectValue(size :)
中的计算更改为:
func effectValue(size: CGSize) -> ProjectionTransform {
let pt = CGPoint(
x: cos(angle + initialAngle) * radius,
y: sin(angle + initialAngle) * radius)
let translation = CGAffineTransform(translationX: pt.x, y: pt.y)
return ProjectionTransform(translation)
}
这将直接使用angle
属性,而不是基于百分比来计算角度。
然后,将makeOrbitEffect(diameter :)
更改为:
func makeOrbitEffect(diameter: CGFloat) -> some GeometryEffect {
return OrbitEffect(
angle: self.animationFlag ? 2 * .pi : 0,
radius: diameter / 2.0)
}
在此,当切换animationFlag
时,更改将从0更改为2π
,而不是0。
现在就构建并运行,但是您不会注意到动画上的差异。
当起点和终点是任意的时,例如将用户设置的两个位置之间的视图或其他视图设置为动画时,使用值与百分比的动画效果最佳。
Other Ways to Animate
迄今为止,当视图由于状态变化而变化时,您所看到的动画类型将隐式或显式地发生。 您还可以通过显式控制视图的属性来为视图设置动画。 例如,您可以循环更改视图的offset
,以将其从一个位置移动到另一个位置。
在月球列表中点击一个月球名称,将显示该行星的详细信息屏幕,其中包含可滚动的月球缩略图列表。 现在,这个列表很无聊。
您可以通过使用rotation3DEffect
为其提供类似于CoverFlow
的界面来为列表增添趣味。
在MoonFlow.swift
中,将以下内容添加到body
中间的VStack
的末尾:
.rotation3DEffect(
.degrees(Double(moonGeometry
.frame(in: .global).midX - geometry.size.width / 2) / 3),
axis: (x: 0, y: 1, z: 0)
)
这将沿y
轴应用旋转效果,具体取决于单个VStack
距整个视图中心的距离。 当用户滚动时,该堆栈的几何形状将不断变化,从而导致旋转效果更新。 这样可以平滑地改变旋转效果,从而制作出动画。
通过结合使用GeometryReader
和诸如projectionEffect
,transformEffect
,scaleEffect
或rotationEffect
之类的效果,您可以在视图更改屏幕上位置时更改其位置和形状。
后记
本篇主要讲述了基于SwiftUI的动画的实现,感兴趣的给个赞或者关注~~~