《SwiftUI编程思想》 onevcat 第一版笔记 下
动画
隐式动画
隐式动画是 view 树的一部分:通过为 view 添加 .animation
修饰器,任何对这个 view 的改变都会自动进行动画。
1 | struct ContentView: View { |
以这种方式所创建的动画感觉上有一点像 Apple 的 “Keynote 演讲” 程序里的神奇移动 (Magic Move):我们定义初始点和结束点,程序来决定要怎么在两者之间进行动画。
和其他 view 更新的机制一样,动画也只会在状态改变时被触发,当我们为 view 树添加一个像是 .animation(.default)
的修饰器时,SwiftUI 将在 view 更新的时候以动画的方式把旧的 view 树改变为新的 view 树。
demo
构建一个类似 iOS 内建的活动指示器 (activity indicator) 那样的,一个图片一直旋转的 view
首先,我们需要一些可以更改的状态以触发动画,这里我们使用一个布尔属性,当 view 出现时,我们将它设为 true
其次,我们需要通过在线性动画中加上 repeatForever,来让动画无限次重复
1 | struct LoadingIndicator: View { |
动画是由状态变更所驱动的
过渡
想要以动画的方式插入一个新 view,或者是移除一个已经存在的 view。SwiftUI为此准备了特定的方式,那就是过渡 (transition)
demo
下面是一个使用滑入滑出动画来将矩形移入和移出屏幕的过渡。当矩形被插入到 view 树中时,它从左侧动画滑入,当它被移出view 树时,它向右侧动画滑出:
1 | struct ContentView: View { |
注意,过渡效果 (.transition 修饰器) 本身并不进行动画,我们依然需要为它们开启动画。和之前一样,我们使用 .animation(.default)
来达成这一点。
动画是如何工作的
考察圆角矩形的动画,这次我们只对它的宽度进行动画。我们也会将动画的持续时间改成五秒,并让它按照线性方式进行动画:
1 | struct AnimatedButton: View { |
在动画期间内,SwiftUI 对 width 值的计算分为两部分。
首先,SwiftUI 使用我们指定的Animation 值 (在上面的例子中,这个值是 .linear)来计算动画的进度。确定了动画的进度,SwiftUI 就可以对被变更的属性在起始值和终止值之间进行插值了。
通常,动画的进度是间于 0 和 1 之间的一个值,0 代表动画的开始,1 代表动画的结束 (不过,有一些动画可以带有“弹性”,它们的进度值有可能会大于 1 或者小于 0)
为了对值进行动画,SwiftUI 使用了 Animatable 协议,该协议仅具有一个要求:一个类型遵守VectorArithmetic
协议的 animatableData
属性。
VectorArithmetic 类型可以进行加法和减法运算,也可以使用 Double 进行乘法。通过这些运算,SwiftUI 可以得出动画的起始值和终止值之间的差值,并将它乘以当前的进度值来得到实际需要的插值。
以矩形 width 动画从 50 到100 为例,SwiftUI 通过 50 + (100 - 50) * progress
就可以计算出当前的宽度。或者,当从绿色动画到红色时,我们可以将它写作 .green + (.red - .green) * progress
一般而言,SwiftUI 将animatableData
属性的当前值计算为 startValue + (endValue - startValue) * progress
当为一个 view 的子树设定隐式动画 (比如 .animation(.default)) 时,子树上的所有可动画属性都会被加上动画,我们也可以通过调用 .animation(nil) 来在这棵子树上禁用所有动画 (甚至包括那些显式动画)。
动画曲线
.linear(duration: 5)
这个 Animation 值提供了一条动画曲线,它在五秒钟的
过程内将进度平均地从 0 到 1 进行插值。
.easeInOut(duration: 0.35) 来使用缓入/缓出曲线 (ease-in/ease-out curve),它将一开始会缓慢移动,然后加快速度,最后再降低速度。
动画曲线可以被看作是一个函数,它接受时间作为输入,返回在这个时间点上对应的动画所达到的进度。下面是内置的动画曲线进行可视化后的样子。
横轴代表时间,纵轴代表动画进行的进度:
可以通过一些修饰器来改变 Animation 的值。比如,我们可以调用 .speed(0.1) 来让动画速度减慢到十分之一,或者我们可以调用 .delay(2) 来让动画延后两秒。另外,repeatCount 是一个很有意思的修饰器,它可以让我们重复一个动画若干次
显示动画
下面这个另一版本的加载指示器,它将一个小原点沿着圆圈进行动画:
1 | struct LoadingIndicator: View { |
当我们旋转模拟器(或者设备) 时,这个点将会在一条奇怪的路径上运动:因为我们使用了隐式动画,SwiftUI 也将会为点的 frame 改变进行这个永不停止的动画 (这个 frame 改变是由设备转向所造成的)。
只想为那些 view 树中由于 self.appeared 状态属性变更所造成的变化进行动画。要做到这一点,我们将隐式动画移除,并将状态改变的代码包装到一个 withAnimation (显式动画) 调用中:
1 | Circle() |
自定义动画
有时候,实现自定义的可动画 view 修饰器是达成某些特定动画的唯一途径
demo
我们想要实现一个可以摇晃 view 来吸引用户注意的动画。当动画开始时,view 应该从它的初始位置向右移动,然后向左移动到初始位置的左侧,最后在移动回到初始位置。
要实现这个动画,我们可以实现我们自己的自定义 view 修饰器,并使它遵守 AnimatableModifier
协议 (这个协议继承自 Animatable
和 ViewModifier
)。我们的修饰器有一个 times 属性,当它增加时,就将晃动 view。我们的目标是当这个属性被增加 1 时,晃动一次 view,当增加 2 时晃动两次,以此类推:
1 | struct Shake: AnimatableModifier { |
在body 的实现中,我们使用 sin 来为摇晃的动画设置一个平滑的曲线:如果 times 是一个整数,偏移量为 0。在整数之间的数字 (比如说 0 到 1 之间),偏移量将从 0 动画变更到 10,然后变更到 -10,最后回到 0。
如果需要暴露两个属性作为可动画属性,我们可以将它们包装在一个 AnimatablePair里。想要支持任意数量的属性,我们可以嵌套使用 AnimatablePair。
要验证 SwiftUI 在动画期间插入不同值的方式,我们可以在 animatableData 的 set-ter 或者 AnimatableModifier 的 body 中打印 log。
当我们的动画修饰器需要进行动画的值可以被表示为仿射变换 (不论 2D 还是 3D) 时,我们也可以使用 GeometryEffect 来代替 AnimatableModifier。
1 | struct ShakeEffect: GeometryEffect { |
自定义过渡
要自定义 view 的插入和移除动画,我们可以使用 AnyTransition 的 modifier(active:identity:)
方法来创建自定义过渡。
这个方法接受两个 view 修饰器:一个用于过渡发生时,另一个用于过渡结束时。
当 view 被插入时,active 修饰器在插入开始时被应用到 view 上,然后向着 identity
修饰器所定义的状态进行迁移。
在移除过程中,动画反向进行:从 identity
修饰器开始,向 active
修饰器靠拢。
demo
创建一个模糊过滤。当 view 被插入时,它会以模糊状态出现,并且 opacity 值为零 (全透明);当移除时,它会淡出并模糊。
第一步,我们创建一个自定义的 ViewModifier
。当 active 是 true 时,内容是模糊且透明,当 active 为 false 时,模糊半径被设为 0,内容非透明:
1 | struct Blur: ViewModifier { |
第二步,我们可以向 AnyTransition 添加一个静态属性,这可以帮助我们更容易访问到我们定义的新过渡:
1 | extension AnyTransition { |
最后,我们可以使用上面的例子,将 .transition(.slide)
替换成 .transition(.blur)
。现在,在插入时,view 会从模糊状态淡入;而在它被移除时,会淡出到模糊状态。