MotionAnimator 5.0.0

MotionAnimator 5.0.0

测试测试
语言语言 Obj-CObjective C
许可协议 Apache-2.0
发布最后发布2021年4月

Jeff VerkoeyenAdrian SecordIan Gordon 维护。



  • Material Motion 作者

Motion Animator Banner

为iOS 9以上版本设计的动画器,结合了现代UIView和CALayer动画API的最佳特性。

Build Status codecov CocoaPods Compatible Platform

🎉隐式和显式增量动画。
🎉使用Interchange进行参数化运动。
🎉直接从手势识别器向动画提供速度。
🎉通过更多地依赖核心动画来最大化帧速率。
🎉可动画属性是Swift枚举类型。
🎉一致的模型层值期望。

以下属性可以在iOS 9及以上版本的MotionAnimator中隐式动画化

CALayeranchorPoint
CALayerbackgroundColorUIViewbackgroundColor
CALayerboundsUIViewbounds
CALayerborderWidth
CALayerborderColor
CALayercornerRadius
CALayerheightUIViewheight
CALayeropacityUIViewalpha
CALayerpositionUIViewcenter
CALayerrotationUIViewrotation
CALayerscaleUIViewscale
CALayershadowColor
CALayershadowOffset
CALayershadowOpacity
CALayershadowRadius
CALayertransformUIViewtransform
CALayerwidthUIViewwidth
CALayerxUIViewx
CALayeryUIViewy
CALayerz
CAShapeLayerstrokeStart
CAShapeLayerstrokeEnd

注意:任何可动画属性都可以使用MotionAnimator的显式动画API进行动画,即使它不在上表列中。

列表中缺少属性吗? 我们欢迎贡献

MotionAnimator: 即插即用的替代品

MotionAnimator也提供了UIView的隐式动画API

// Animating implicitly with UIView APIs
UIView.animate(withDuration: 1.0, animations: {
  view.alpha = 0.5
})

// Equivalent MotionAnimator API
MotionAnimator.animate(withDuration: 1.0, animations: {
  view.alpha = 0.5
})

但MotionAnimator可以动画化更多属性 — 并且在更多iOS版本上

UIView.animate(withDuration: 1.0, animations: {
  view.layer.cornerRadius = 10 // Only works on iOS 11 and up
})

MotionAnimator.animate(withDuration: 1.0, animations: {
  view.layer.cornerRadius = 10 // Works on iOS 9 and up
})

MotionAnimator利用了MotionInterchange,一种标准的动画特性表示格式。这使得可以在不重新编写创建动画的代码的情况下调整动画的特性,对于构建调整工具和制作运动"样式表"非常有用。

// Want to change a trait of your animation? You'll need to use a different function altogether
// to do so:
UIView.animate(withDuration: 1.0, animations: {
  view.alpha = 0.5
})
UIView.animate(withDuration: 1.0, delay: 0.5, options: [], animations: {
  view.alpha = 0.5
}, completion: nil)

// But with the MotionInterchange, you can create and manipulate the traits of an animation
// separately from its execution.
let traits = MDMAnimationTraits(duration: 1.0)
traits.delay = 0.5

let animator = MotionAnimator()
animator.animate(with: traits, animations: {
  view.alpha = 0.5
})

MotionAnimator还可以用来用增量显式动画替换显式核心动画代码

let from = 0
let to = 10
// Animating expicitly with Core Animation APIs
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.fromValue = (from - to)
animation.toValue = 0
animation.isAdditive = true
animation.duration = 1.0
view.layer.add(animation, forKey: animation.keyPath)
view.layer.cornerRadius = to

// Equivalent implicit MotionAnimator API. cornerRadius will be animated additively by default.
view.layer.cornerRadius = 0
MotionAnimator.animate(withDuration: 1, animations: {
  view.layer.cornerRadius = 10
})

// Equivalent explicit MotionAnimator API
// Note that this API will also set the final animation value to the layer's model layer, similar
// to how implicit animations work, and unlike the explicit pure Core Animation implementation
// above.
let animator = MotionAnimator()
animator.animate(with: MDMAnimationTraits(duration: 1.0),
                 between: [0, 10],
                 layer: view.layer,
                 keyPath: .cornerRadius)

在iOS上使用Spring需要通过动画的位移来归一化的初始速度。MotionAnimator会为您计算这个值,以便您可以直接提供手势识别器的速度值。

// Common variables
let gestureYVelocity = gestureRecognizer.velocity(in: someContainerView).y
let destinationY = 75

// Animating springs implicitly with UIView APIs
let displacement = destinationY - view.position.y
UIView.animate(withDuration: 1.0,
               delay: 0,
               usingSpringWithDamping: 1.0,
               initialSpringVelocity: gestureYVelocity / displacement,
               options: [],
               animations: {
                 view.layer.position = CGPoint(x: view.position.x, y: destinationY)
               },
               completion: nil)

// Equivalent MotionAnimator API
let animator = MotionAnimator()
let traits = MDMAnimationTraits(duration: 1.0)
traits.timingCurve = MDMSpringTimingCurveGenerator(duration: traits.duration,
                                                   dampingRatio: 1.0,
                                                   initialVelocity: gestureYVelocity)
animator.animate(with: traits,
                 between: [view.layer.position.y, destinationY],
                 layer: view.layer,
                 keyPath: .y)

API代码片段

隐式动画

MotionAnimator.animate(withDuration: <#T##TimeInterval#>) {
  <#code#>
}
MotionAnimator.animate(withDuration: <#T##TimeInterval#>,
                       delay: <#T##TimeInterval#>,
                       options: <#T##UIViewAnimationOptions#>,
                       animations: {
  <#code#>
})

显式动画

let traits = MDMAnimationTraits(delay: <#T##TimeInterval#>,
                                duration: <#T##TimeInterval#>,
                                animationCurve: <#T##UIViewAnimationCurve#>)
let animator = MotionAnimator()
animator.animate(with: <#T##MDMAnimationTraits#>,
                 between: [<#T##[From (Any)]#>, <#T##[To (Any)]#>],
                 layer: <#T##CALayer#>,
                 keyPath: <#T##AnimatableKeyPath#>)

动画过渡

let animator = MotionAnimator()
animator.shouldReverseValues = transition.direction == .backwards

let traits = MDMAnimationTraits(delay: <#T##TimeInterval#>,
                                duration: <#T##TimeInterval#>,
                                animationCurve: <#T##UIViewAnimationCurve#>)
animator.animate(with: <#T##MDMAnimationTraits#>,
                 between: [<#T##[From (Any)]#>, <#T##[To (Any)]#>],
                 layer: <#T##CALayer#>,
                 keyPath: <#T##AnimatableKeyPath#>)

创建运动规范

class MotionSpec {
  static let chipWidth = MDMAnimationTraits(delay: 0.000, duration: 0.350)
  static let chipHeight = MDMAnimationTraits(delay: 0.000, duration: 0.500)
}

let animator = MotionAnimator()
animator.shouldReverseValues = transition.direction == .backwards

animator.animate(with: MotionSpec.chipWidth,
                 between: [<#T##[From (Any)]#>, <#T##[To (Any)]#>],
                 layer: <#T##CALayer#>,
                 keyPath: <#T##AnimatableKeyPath#>)
animator.animate(with: MotionSpec.chipHeight,
                 between: [<#T##[From (Any)]#>, <#T##[To (Any)]#>],
                 layer: <#T##CALayer#>,
                 keyPath: <#T##AnimatableKeyPath#>)

从当前状态动画化

// Will animate any non-additive animations from their current presentation layer value
animator.beginFromCurrentState = true

调试动画

animator.addCoreAnimationTracer { layer, animation in
  print(animation.debugDescription)
}

在手势识别器反应中停止动画

if gesture.state == .began {
  animator.stopAllAnimations()
}

移除所有动画

animator.removeAllAnimations()

主线程动画与Core Animation的比较

在iOS上的动画系统可以分为两大类:基于主线程和基于Core Animation。

基于主线程的动画系统包括UIDynamics、Facebook的POP或由CADisplayLink驱动的任何动画。这些动画系统与你的应用主线程共享CPU时间,这意味着它们与UIKit、文本渲染以及任何其他主线程绑定的进程共享资源。这也意味着动画会受到主线程卡顿的影响,换句话说:动画丢帧或“卡顿”。

Core Animation利用了渲染服务器,iOS操作系统层面上用于动画的处理进程。这种与应用进程的独立性允许渲染服务器避免主线程卡顿。

基于主线程的动画比Core Animation的一个主要优点是,Core Animation可以动画化的属性列表很小且不可变,而主线程动画可以动画化你的应用程序中的任何内容。一个好的例子是使用POP来动画化“时间”属性,并将该时间映射到时钟的指针上。这种类型的操作不能在没有将代码从渲染服务器移动到主线程的情况下在Core Animation中实现。

另一方面,Core Animation比基于主线程动画的一个主要优点是,你的动画不太可能因为这些动画所在的线程忙碌而丢帧。

在评估是否使用基于主线程的动画系统时,首先检查是否可以使用Core Animation执行相同的动画。如果可以,您可以通过使用Core Animation将动画卸载到应用程序的主线程,从而节省宝贵的处理时间分配给其他主线程受限的操作。

MotionAnimator是一个纯粹基于Core Animation的动画器。如果您正在寻找主线程解决方案,请查看以下技术

Core Animation:深入探究

推荐阅读

在iOS上使用Core Animation进行动画化的主要方式有两种

  1. 隐式地使用UIView的 animateWithDuration: API,或者通过设置独立CALayer实例(那些不是UIView后盾的)上的属性,以及
  2. 显式地使用CALayer的 addAnimation:forKey: API。

UIView和CALayer的公共API的子集可以由Core Animation动画化。在这些可动画化属性中,一些是隐式可动画的,而另一些则不是。属性是否可动画化取决于它被动画化的上下文,而动画是否是累加的则取决于使用的哪个动画API。在这个条件的矩阵中,理解为何有时很难有效地使用Core Animation是可以理解的。

以下小测验有助于说明UIKit和Core Animation API可能导致不直观的行为。猜一猜以下哪个代码片段会生成动画,如果会,这个生成的动画的持续时间是多少

想象每个代码片段都是一个独立的单元测试(因为 它们是!)。

let view = UIView()
UIView.animate(withDuration: 0.8, animations: {
  view.alpha = 0.5
})
点击查看答案生成持续时间为0.8的动画。

let view = UIView()
UIView.animate(withDuration: 0.8, animations: {
  view.layer.opacity = 0.5
})
点击查看答案生成持续时间为0.8的动画。

let view = UIView()
UIView.animate(withDuration: 0.8, animations: {
  view.layer.cornerRadius = 3
})
点击查看答案在iOS 11及以上版本,生成持续时间为0.8的动画。较旧的操作系统不会生成动画。

let view = UIView()
view.alpha = 0.5
点击查看答案不生成动画。

let view = UIView()
view.layer.opacity = 0.5
点击查看答案不生成动画。

let layer = CALayer()
layer.opacity = 0.5
点击查看答案不生成动画。

let view = UIView()
window.addSubview(view)
let layer = CALayer()
view.layer.addSublayer(layer)

// Pump the run loop once.
RunLoop.main.run(mode: .defaultRunLoopMode, before: .distantFuture)

layer.opacity = 0.5
点击查看答案生成持续时间为0.25的动画。

let view = UIView()
window.addSubview(view)
let layer = CALayer()
view.layer.addSublayer(layer)

// Pump the run loop once.
RunLoop.main.run(mode: .defaultRunLoopMode, before: .distantFuture)

UIView.animate(withDuration: 0.8, animations: {
  layer.opacity = 0.5
})
点击查看答案生成持续时间为0.25的动画。这不是打字错误:独立层在隐式动画时是从当前的CATransaction读取的,而不是从UIView的参数读取,即使在UIView动画块内部发生变化。

哪些属性可以显式动画化?

有关所有可动画的CALayer属性的完整列表,请参阅Apple文档

可以使用MotionAnimator的显式API来动画化任何Core Animation支持的动画属性。

可以隐式动画化的属性有哪些?

UIKit和Core Animation在何时以及如何隐式动画化属性方面有不同的规则。

当UIView属性在animateWithDuration:动画块内更改时,会生成隐式动画。

CALayer属性只有在以下任一条件下更改时才会生成隐式动画:

  1. 如果CALayer支持在UIView下隐式动画化的属性,并且更改发生在animateWithDuration:块中(这一点并未在文档中明确说明),或者
  2. 如果CALayer不支持在UIView下隐式动画化(未托管图层),且图层已经至少经过一次CATransaction刷新——可以通过调用CATransaction.flush()或让运行循环至少泵送一次实现——且属性被更改。

这种行为可能相当难以推理,尤其是在尝试使用UIView的animateWithDuration: API来动画化CALayer属性时尤为明显。例如,在iOS 11之前,CALayer的cornerRadius无法使用animateWithDuration:进行动画,其他许多CALayer属性也仍然无法隐式动画化。

// This doesn't work until iOS 11.
UIView.animate(withDuration: 0.8, animations: {
  view.layer.borderWidth = 10
}, completion: nil)

// This works back to iOS 9.
MotionAnimator.animate(withDuration: 0.8, animations: {
  view.layer.borderWidth = 10
}, completion: nil)

MotionAnimator提供了一套定义良好的支持属性集合,使得隐式动画API更加一致。

一般而言,何时会更改一个属性导致生成隐式动画?

以下图表说明了更改给定对象上的属性是否会导致生成隐式动画。

UIView

let view = UIView()

// inside animation block
UIView.animate(withDuration: 0.8, animations: {
  view.alpha = 0.5 // Will generate an animation with a duration of 0.8
})

// outside animation block
view.alpha = 0.5 // Will not animate

// inside MotionAnimator animation block
MotionAnimator.animate(withDuration: 0.8, animations: {
  view.alpha = 0.5 // Will generate an animation with a duration of 0.8
})
UIVIew键路径 在动画块内 在动画块外 在MotionAnimator动画块内
alpha
backgroundColor
bounds
bounds.size.height
bounds.size.width
center
center.x
center.y
transform
transform.rotation.z
transform.scale

Backing CALayer

每个UIView都有一个幕后CALayer。

let view = UIView()

// inside animation block
UIView.animate(withDuration: 0.8, animations: {
  view.layer.opacity = 0.5 // Will generate an animation with a duration of 0.8
})

// outside animation block
view.layer.opacity = 0.5 // Will not animate

// inside MotionAnimator animation block
MotionAnimator.animate(withDuration: 0.8, animations: {
  view.layer.opacity = 0.5 // Will generate an animation with a duration of 0.8
})
CALayer键路径 在动画块内 在动画块外 在MotionAnimator动画块内
anchorPoint ✓ (从iOS 11开始)
backgroundColor
bounds
borderWidth
borderColor
cornerRadius ✓ (从iOS 11开始)
bounds.size.height
opacity
position
transform.rotation.z
transform.scale
shadowColor
shadowOffset
shadowOpacity
shadowRadius
strokeStart
strokeEnd
transform
bounds.size.width
位置.x
位置.y
z位置

未刷新,未托管CALayer

CALayers在下次调用CATransaction.flush()之前不会被刷新,这个调用可能直接进行,也可能在当前运行循环结束时进行。

let layer = CALayer()

// inside animation block
UIView.animate(withDuration: 0.8, animations: {
  layer.opacity = 0.5 // Will not animate
})

// outside animation block
layer.opacity = 0.5 // Will not animate

// inside MotionAnimator animation block
MotionAnimator.animate(withDuration: 0.8, animations: {
  layer.opacity = 0.5 // Will generate an animation with a duration of 0.8
})
CALayer键路径 在动画块内 在动画块外 在MotionAnimator动画块内
anchorPoint
backgroundColor
bounds
borderWidth
borderColor
cornerRadius
bounds.size.height
opacity
position
transform.rotation.z
transform.scale
shadowColor
shadowOffset
shadowOpacity
shadowRadius
strokeStart
strokeEnd
transform
bounds.size.width
位置.x
位置.y
z位置

已刷新,未托管CALayer

let layer = CALayer()

// It's usually unnecessary to flush the transaction, unless you want to be able to implicitly
// animate it without using a MotionAnimator.
CATransaction.flush()

// inside animation block
UIView.animate(withDuration: 0.8, animations: {
  // Will generate an animation with a duration of 0.25 because it uses the CATransaction duration
  // rather than the UIKit duration.
  layer.opacity = 0.5
})

// outside animation block
// Will generate an animation with a duration of 0.25
layer.opacity = 0.5

// inside MotionAnimator animation block
MotionAnimator.animate(withDuration: 0.8, animations: {
  layer.opacity = 0.5 // Will generate an animation with a duration of 0.8
})
CALayer键路径 在动画块内 在动画块外 在MotionAnimator动画块内
anchorPoint
backgroundColor
bounds
borderWidth
borderColor
cornerRadius
bounds.size.height
opacity
position
transform.rotation.z
transform.scale
shadowColor
shadowOffset
shadowOpacity
shadowRadius
strokeStart
strokeEnd
transform
bounds.size.width
位置.x
位置.y
z位置

示例应用程序/单元测试

要访问目录应用程序,请在本地副本上运行以下命令

git clone https://github.com/material-motion/motion-animator-objc.git
cd motion-animator-objc
pod install
open MotionAnimator.xcworkspace

安装

使用CocoaPods安装

CocoaPods是Objective-C和Swift库的依赖项管理器。CocoaPods自动处理在项目中使用第三方库的过程。有关更多信息,请参阅入门指南。您可以使用以下命令安装它

gem install cocoapods

motion-animator添加到您的Podfile

pod 'MotionAnimator'

然后运行以下命令

pod install

使用方法

导入框架

@import MotionAnimator;

您现在将能够访问所有的API。

贡献

我们欢迎贡献!

查看我们的即将到来的里程碑

了解更多关于我们的团队社区和我们的贡献指南

许可协议

遵循Apache 2.0许可协议。详情见LICENSE文件。