Motion-Logo-Dark Motion-Logo-Light

Tests Docs

Motion 是一个动画引擎,用于 iOS、macOS 和 tvOS 上手势驱动的用户界面、动画和交互。它由 SIMD 提供支持,并完全用 Swift 编写。 Motion 可以轻松创建基于物理模型的、可中断的动画(例如弹簧、衰减等),这些动画与手势识别器协同工作,以实现最流畅和令人愉悦的交互。

使用方法

API 文档请访问此处

动画

在 Motion 中创建动画相对简单。 只需分配您想要的动画类型,指定符合 SIMDRepresentable 的类型,配置它,然后调用 start 启动它。 对于动画执行的每一帧,都会调用其 onValueChanged 块,并且您将有机会将新动画的值分配给某个对象。

默认情况下,开箱即用地支持许多类型,包括

调用 stop 将其冻结在适当的位置,无需查询 CALayer 上的 presentationLayer 并设置值,也无需担心 fillMode,或真正担心任何事情。

完成后,将调用 completion 块。

动画需要保存在某个地方,因为如果它们被释放,它们将停止运行。 另外,由于它们执行块的性质,请注意不要通过在动画 onValueChangedcompletion 块内不使用 weak selfunowned 动画来引入 retain cycles。

这是一些示例

弹簧动画

let springAnimation = SpringAnimation<CGRect>()
springAnimation.configure(response: 0.30, damping: 0.64)
springAnimation.toValue = CGRect(x: 0.0, y: 0.0, width: 320.0, height: 320.0)
springAnimation.velocity = CGRect(x: 0.0, y: 0.0, width: -200.0, height: -200.0)
springAnimation.onValueChanged(disableActions: true) { newValue in
    view.layer.bounds = newValue
}
springAnimation.completion = { [weak self] in
    // all done
    self?.animationDone()
}
springAnimation.start()

注意:你们中的一些人可能想知道 stiffnessdampingresponsedampingRatio setter 是私有的是否是错误,但这是故意的。 很容易混淆 dampingdampingRatio,并且使用一个而不是另一个会导致截然不同的结果。 此外,您应该只配置 stiffnessdamping responsedampingRatio,因为它们是配置弹簧常数的两种独立方式。

衰减动画

let decayAnimation = DecayAnimation<CGPoint>()
decayAnimation.velocity = CGPoint(x: 0.0, y: 2000.0)
decayAnimation.onValueChanged { newValue in
    view.bounds.origin = newValue
}
decayAnimation.completion = {
    // all done
}
decayAnimation.start()

基础动画

let basicAnimation = BasicAnimation<CGFloat>(easingFunction: .easeInOut)
basicAnimation.fromValue = 100.0
basicAnimation.toValue = 200.0
basicAnimation.duration = 0.4
basicAnimation.onValueChanged { newValue in
    view.bounds.frame.x = newValue
}
basicAnimation.completion = {
    // all done
}
basicAnimation.start()

注意:所有这些动画都必须在主线程上运行和交互。 不支持任何形式的线程处理。

SwiftUI 支持

Motion 开箱即用地支持 SwiftUI! 您可以使用任何 Animation 子类手动为 @State 更改设置动画。

查看示例项目的SwiftUI 演示以获取更多信息!

注意:计划支持 TimelineView。 敬请关注!

Motion vs. Core Animation

Motion 并非旨在成为 Core Animation 的通用替代品。 Core Animation 动画以特殊的方式在另一个进程中运行,在您的应用程序之外,并且即使在主线程被大量使用时也能保持平滑。 另一方面,Motion 都是在进程内运行的(就像游戏引擎一样),如果不考虑繁重的堆栈跟踪而随意使用它,会导致性能不佳和丢帧。 Motion 本身并不慢(事实上它真的非常快!),但如果不能小心地以 60 FPS(或更高)调用方法来更改视图/图层属性或更改布局,可能会非常耗费资源。

tl;dr:将 Motion 动画视为 UIScrollView(因为滚动动画的行为方式相同)。 如果您的 UIScrollView 中有太多事情要做,它在滚动时会滞后; 同样适用于 Motion。

一些关键提示

可中断性

Motion 的设计旨在使可中断的动画更容易。 可中断性是指您有能力中断飞行中的动画,以便您可以停止、更改或重新启动它。 通常,使用基于 UIView 块的动画或基于 Core Animation 的动画,这确实很难做到(需要取消动画,找出其在屏幕上的当前状态,应用它等)。 UIViewPropertyAnimator 在这方面表现尚可,但它严重依赖于“擦洗”动画,当使用基于物理的动画(即弹簧)时,这实际上没有多大意义,因为物理是动态生成动画的原因(而不是您可以擦洗的某些预定义的缓动曲线)。

Motion 使这些事情变得容易,因此您不必担心将动画状态与手势同步,而将更多精力放在交互本身上。

这是一个将拖动到弹簧动画,然后捕获并重定向该动画的示例

假设您在另一个视图(self)内有一个子视图 view

// Create a spring animation configured with our constants.
var springAnimation: SpringAnimation<CGPoint>()
springAnimation.configure(response: 0.30, damping: 0.64)

// When you drag on the view and let go, it'll spring away from the center and then rebound back.
// At any point, you can grab the view and do it again.
func didPan(_ gestureRecognizer: UIPanGestureRecognizer) {
    switch gestureRecognizer.state: {
        case .began:
            springAnimation.stop()
        case .changed:
            view.center = gestureRecognizer.location(in: self)
            springAnimation.updateValue(to: view.center)
        case .ended:
            springAnimation.toValue = self.center
            springAnimation.velocity = gestureRecognizer.velocity(in: self)
            springAnimation.onValueChanged { newValue in
                view.center = newValue
            }
            springAnimation.start()
    }
}

注意:您可以在示例项目(在拖动演示下)中尝试此操作。

SIMD

SIMD 为 Motion 的许多工作提供支持,并避免使用更多“昂贵”的对象(如 NSValueNSNumber)进行动画处理。 SIMD 允许将多个值打包到单个 SIMD 寄存器中,然后同时对所有这些值执行数学运算(单指令多数据)。 这意味着您可以做一些整洁的事情,例如在单个超快速操作中将 CGRect 动画化为另一个 CGRect(而不是 4 个单独的操作:xywidthheight)。 它并不总是灵丹妙药,但平均而言,它至少与朴素实现相当,并且通常比朴素实现更快。

Motion 公开了一个名为 SIMDRepresentable 的协议,该协议允许轻松地 boxing 和 unboxing 值

let point = CGPoint(x: 10.0, y: 10.0)
let simdPoint: SIMD2<CGFloat.NativeType> = point.simdRepresentation()
let pointBoxedAgain = CGPoint(simdPoint)

这些转换相对便宜,并且 Motion 经过大量优化,可避免在可以时复制或 boxing/unboxing 它们。

有关 SIMD 的更多信息,请查看文档

性能

Motion 非常快(尤其是在 Apple Silicon 上!),它利用了一些手动 Swift 优化/专门化以及 SIMD,能够在 iPhone 12 Pro 上以 ~130ms 执行 5000 个 SpringAnimation<SIMD64<Double>>(即每个弹簧 0.4 微秒的 320,000 个弹簧!!)。 对于像 CGFloat 这样的小类型,它可以在 ~1ms 内完成相同的事情。

它能达到最快的速度吗? 比一些手动优化的 C++ 或 C 实现更快吗? 可能不会。 也就是说,它绝对足够快,可以进行设备上的交互,并且很少(甚至从不)会成为瓶颈。 我也不是 SIMD 专家,所以如果有人有任何提示,我相信它可以更快!

tl;dr:SIMD go brrrrrrrrrrrrrrrrrrrrrrr

如果您想在自己的设备上对 Motion 进行基准测试,只需从 Benchmark 文件夹中运行以下命令

swift run -c release MotionBenchmarkRunner --time-unit ms

如果您想在设备上运行基准测试,只需使用 --benchmark 启动参数在 Release 模式下启动 MotionEample-iOS 应用程序。

附加功能

Motion 具有一些很棒的附加功能,可以帮助您创建一般的交互。

橡皮筋效果

橡皮筋效果是指使值看起来像在橡皮筋上(它们根据交互拉伸和滑动)。 当您拉过 contentSize 时,UIScrollView 会这样做,并且通过使用 Motion 中的橡皮筋功能,您可以为自己重新创建此交互。 有关更多信息,请参见示例应用程序中的“ScrollView Demo”。

CAKeyframeAnimationEmittable

Motion 中的所有动画都符合 CAKeyframeAnimationEmittable,这意味着对于您配置的任何动画,您可以让它自动生成一个 CAKeyframeAnimation,该动画镜像如果您使用 start() 为事物设置动画会发生的事情。 持续时间和所有其他内容都是通过运行从 value 到已解析状态的动画自动计算的。 唯一的区别是不能使用 onValueChangedcompletion,您必须指定要设置动画的关键路径。 还有一些辅助方法可以使这更容易(例如,将任何动画直接添加到 CALayer)。

例如

let springAnimation = SpringAnimation<CGRect>()
springAnimation.configure(response: 0.30, damping: 0.64)
springAnimation.toValue = CGRect(x: 0.0, y: 0.0, width: 320.0, height: 320.0)
springAnimation.velocity = CGRect(x: 0.0, y: 0.0, width: -200.0, height: -200.0)

let keyframeAnimation = springAnimation.keyframeAnimation()
keyframeAnimation.keyPath = "frame"
layer.add(keyframeAnimation, forKey: "MyAnimation")

// or

layer.add(springAnimation, forKey: "MyAnimation", keyPath: "frame")

注意:如果您删除或中断动画并且希望它保持在屏幕上的适当位置,就像所有其他 Core Animation 动画一样,您需要从图层的 presentationLayer 获取值并将其应用于图层(并担心 fillMode)。

let frame = layer.presentationLayer()?.frame ?? layer.frame
layer.removeAnimation(forKey: "MyAnimation")
CADisableActions {
    layer.frame = frame
}

禁用 Action

CATransaction 是一个非常有用的 API,但如果您忘记配对 CATransaction.begin()CATransaction.commit() 调用,则很容易破坏某些东西。

CADisableActions() 在使用 CATransaction 禁用隐式动画时,可以非常有助于减少创建的错误

CADisableActions {
    layer.opacity = 0.5
}

// This is the same as calling:

CATransaction.begin()
CATransaction.setDisableActions(true)
layer.opacity = 0.5
CATransaction.commit()

此外,您还可以在每个 onValueChanged 调用中禁用隐式动画

let springAnimation = SpringAnimation<CGFloat>(initialValue: 0.5)
springAnimation.onValueChanged(disableActions: true) { newValue in
    layer.opacity = newValue
}
springAnimation.start()

曲线图绘制

已完成一些初始工作来生成图形以可视化许多这些动画,您将在 Graphing 包中找到它。 它仍然是一项繁重的工作,但对于可视化弹簧/衰减函数来说非常简洁。

安装

要求

目前,Motion 支持 Swift Package Manager、CocoaPods、Carthage,用作 xcframework,以及手动用作 Xcode 子项目。 欢迎为其他依赖系统/构建系统提交 pull 请求!

Swift Package Manager

将以下内容添加到您的 Package.swift(或通过 Xcode 的 GUI 添加)

.package(url: "https://github.com/b3ll/Motion", from: "0.0.3")

xcframework

为每个标记的发行版提供了一个构建的 xcframework。

Xcode 子项目

仍在开发中...(Carthage 和 Cocoapods 也是如此)。 欢迎提交 Pull Request!

示例项目

有一个示例项目可用于尝试动画并查看它们的工作原理。 只需从 Example 目录中打开 MotionExample-iOS.xcodeproj

其他建议

如果您希望为 CATransform3D 设置动画或访问其特定部分以进行动画处理,而不必担心所涉及的复杂矩阵数学(即 transform.translation.x),则此库与 Decomposed 非常搭配。

许可证

Motion 在 BSD 2-clause license 下获得许可。

鸣谢

这个项目绝对受到了多年来我有幸与之共事的优秀人才以及我对精心创建和高度精细的界面的欣赏的启发。 此外,@timdonnelly 为 Advance 所做的工作对我开始这项工作产生了很大的启发。 我最终编写了这个项目,目的是进一步扩展并深入学习所有数学和技术优化,这些优化是制作高性能动画/交互库所必需的。 我在它上面迭代的越多,我就越意识到我在分享与他写 Advance 时相同的想法......大脑很奇怪。

这个项目真的让我突破了我在编程、Swift、动画、手势等方面的知识界限,我很高兴与大家分享,以便他们也可以有权使用它。

如果您有任何问题或想了解更多信息,请随时问我任何问题!

联系方式

欢迎在 Mastodon 上关注我:@b3ll