主页 SwiftUI 滑动来解锁(slide to unlock)动画解析
Post
Cancel

SwiftUI 滑动来解锁(slide to unlock)动画解析

之前在UIKit中实现了这样的动画。 Github源码

通常可以在层上使用CoreAnimation,我想知道这在SwiftUI中会如何实现。 动画是SwiftUI中对我来说不容易实现的功能之一!

动画修饰符 (AnimatableModifier)

我选择将效果实现为 AnimatableModifier。该协议既继承自Animatable又继承于ViewModifier,因此我们同时实现了 animatableDatafunc body(content:Content)-> some View

我的想法是,在 ViewModifierbody 函数中获取任何给定的视图,将其覆盖渐变,然后使用mask()将其约束到内容中:

1
LinearGradient(/* ... */).mask(content)

然后通过 animatableData完成动画处理:它公开了应该进行动画处理的数据。由于我们的动画是无尽的,所以我选择了一个简单的CGFloat,它会从0到1不断地增长。该浮点数表示从左向右移动的渐变的基于水平百分比的位置。

正确布局

让我们从实现 body 方法开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import SwiftUI

struct Shimmer: AnimatableModifier {

    private let gradient: Gradient

    init(sideColor: Color = Color(white: 0.25), 
        middleColor: Color = .white) {
        gradient = Gradient(colors: [sideColor, middleColor, sideColor])
    }

    func body(content: Content) -> some View {
        content
            .overlay(LinearGradient(
                        gradient: gradient,
                        startPoint: .leading,
                        endPoint: .trailing))
            .mask(content)
    }
}

内容上有一个叠加层,该叠加层又被内容掩盖了。这样,我们就不需要了,GeometryReader因为覆盖层将完全匹配的框架content。我们LinearGradient目前使用的是一个横跨整个可用空间的。然后,此梯度再次被内容掩盖,从而标记出内容的Alpha通道。这可以很好地与文本配合使用:

内容上有一个叠加层,该叠加层又被内容掩盖了。这样,我们不需要GeometryReader,因为覆盖层将完全匹配的框架content。我们使用了一个LinearGradient,它目前跨越了整个可用空间。然后,此梯度再次被内容掩盖,从而标记出内容的Alpha通道。这可以很好地与文本配合使用:

移动渐变

目前,渐变从.leading.trailing。让我们用一个position以后可以设置动画的变量来修复它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Shimmer: AnimatableModifier {

    // ...

    @State private var position: CGFloat = 0.25

    func body(content: Content) -> some View {
        content
            .overlay(LinearGradient(
                        gradient: gradient,
                        startPoint: .init(x: position - 0.2, y: 0.5),
                        endPoint: .init(x: position + 0.2, y: 0.5)))
	// ...
    }
}

现在将以动画的25%显示动画:

挺好的!渐变现在覆盖了区域的40%(position 左侧为20%,右侧为20%)。 不幸的是,在0%和100%处存在一些伪像,其中仍显示了一半的渐变:

我们可以通过删除动画边缘的 ±0.2来解决此问题:

  • 在 0% 处,我们要隐藏渐变的右侧
  • 在 100% 处,我们要隐藏渐变的右侧

这是一个简单的线性程序:

1
2
startPoint: .init(x: position - 0.2 * (1 - position), y: 0.5),
endPoint: .init(x: position + 0.2 * position, y: 0.5)))

动画位置

为了符合AnimatableModifierAnimatable部分,我们需要将position公开为animatableData

1
2
3
4
5
6
7
8
9
10
11
12
struct Shimmer: AnimatableModifier {

    // ...

    @State private var position: CGFloat = 0
    var animatableData: CGFloat {
        get { position }
        set { position = newValue }
    }

	// ...
}

如果我们从0到1进行动画处理,则每帧SwiftUI将被调用,其值范围从0到1。就像数字流一样:在动画制作过程中,它将采用值0.1、0.2、0.3,…

实际的动画需要在.onAppear闭包中触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
func body(content: Content) -> some View {
    content
        .overlay(/* ... */)
        .mask(content)
        .onAppear {
            withAnimation(Animation
                            .linear(duration: 2)
                            .delay(1)
                            .repeatForever(autoreverses: false)) {
                position = 1
            }
        }
}

让我们对此进行剖析:

  • .onAppear中,我们将position设置为 1
  • 这包装在.withAnimation
  • 我们有2秒的线性动画,延迟了1秒
  • 重复播放这3秒的动画

在此处使用.withAnimation很重要,因为这只会使闭包内部发生的更改动画。如果我们在视图上选择了.animation修饰符,则所有其他更改(例如设备旋转时的帧更改)也会被设置为动画。

使用

1
2
3
4
5
  // 文字
Text("slide to unlock")
	.fontWeight(.regular)
	.font(.system(size: 20))
	.modifier(Shimmer())                  

结果如下所示:

总结

在SwiftUI中为事物设置动画非常好。预览有很大帮助:在开发此预览时,我进行了多个预览,分别显示了动画在各个阶段的进度。

  • 但是我喜欢它的主要原因是SwiftUI不会混用隐喻:在UIKit中,当未暴露某些东西(例如遮罩)时,您总是可以使用层API。没有什么可以阻止您从UIView,UIViewController或另一个UIViewController进行此操作。 SwiftUI严格限制了这些交互:从正确包含一致的API开始,这是一个崭新的开始。
该博客文章由作者通过 CC BY 4.0 进行授权。

文章目录

Swift 使用属性包装器实现应用本地化

iOS 中赏心悦目的动画