Add readme section on main thread animations vs Core Animation. (#107)

1 file changed
tree: 365dee49b22884f3edf770c72bb7a5fbff19bcaf
  1. assets/
  2. examples/
  3. img/
  4. resources/
  5. src/
  6. tests/
  7. .clang-format
  8. .codecov.yml
  9. .gitignore
  10. .jazzy.yaml
  11. .kokoro
  12. .swift-version
  13. .travis.yml
  14. AUTHORS
  15. BUILD
  16. CHANGELOG.md
  17. CONTRIBUTING.md
  18. LICENSE
  19. MotionAnimator.podspec
  20. Podfile
  21. Podfile.lock
  22. README.md
  23. WORKSPACE
README.md

Motion Animator Banner

An animator for iOS 8+ that combines the best aspects of modern UIView and CALayer animation APIs.

Build Status codecov CocoaPods Compatible Platform

The following properties can be implicitly animated using the MotionAnimator on iOS 8 and up:

Note: any animatable property can also be animated with MotionAnimator‘s explicit animation APIs, even if it’s not listed in the table above.

Is a property missing from this list? We welcome pull requests!

MotionAnimator: a drop-in replacement

UIView's implicit animation APIs are also available on the MotionAnimator:

// 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
})

But the MotionAnimator allows you to animate more properties — and on more iOS versions:

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 8 and up
})

MotionAnimator makes use of the MotionInterchange, a standardized format for representing animation traits. This makes it possible to tweak the traits of an animation without rewriting the code that ultimately creates the animation, useful for building tweaking tools and making motion “stylesheets”.

// 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
})

The MotionAnimator can also be used to replace explicit Core Animation code with additive explicit animations:

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)

Springs on iOS require an initial velocity that's normalized by the displacement of the animation. MotionAnimator calculates this for you so that you can directly provide gesture recognizer velocity values:

// 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 snippets

Implicit animations

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

Explicit animations

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#>)

Animating transitions

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#>)

Creating motion specifications

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#>)

Animating from the current state

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

Debugging animations

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

Stopping animations in reaction to a gesture recognizer

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

Removing all animations

animator.removeAllAnimations()

Main thread animations vs Core Animation

Animation systems on iOS can be split into two general categories: main thread-based and Core Animation.

Main thread-based animation systems include UIDynamics, Facebook‘s POP, or anything driven by a CADisplayLink. These animation systems share CPU time with your app’s main thread, meaning they're sharing resources with UIKit, text rendering, and any other main-thread bound processes. This also means the animations are subject to main thread jank, in other words: dropped frames of animation or “stuttering”.

Core Animation makes use of the render server, an operating system-wide process for animations on iOS. This independence from an app's process allows the render server to avoid main thread jank altogether.

The primary benefit of main thread animations over Core Animation is that Core Animation's list of animatable properties is small and unchangeable, while main thread animations can animate anything in your application. A good example of this is using POP to animate a “time” property, and to map that time to the hands of a clock. This type of behavior cannot be implemented in Core Animation without moving code out of the render server and in to the main thread.

The primary benefit of Core Animation over main thread animations, on the other hand, is that your animations will be much less likely to drop frames simply because your app is busy on its main thread.

When evaluating whether to use a main thread-based animation system or not, check first whether the same animations can be performed in Core Animation instead. If they can, you may be able to offload the animations from your app's main thread by using Core Animation, saving you valuable processing time for other main thread-bound operations.

MotionAnimator is a purely Core Animation-based animator. If you are looking for main thread solutions then check out the following technologies:

Example apps/unit tests

Check out a local copy of the repo to access the Catalog application by running the following commands:

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

Installation

Installation with CocoaPods

CocoaPods is a dependency manager for Objective-C and Swift libraries. CocoaPods automates the process of using third-party libraries in your projects. See the Getting Started guide for more information. You can install it with the following command:

gem install cocoapods

Add motion-animator to your Podfile:

pod 'MotionAnimator'

Then run the following command:

pod install

Usage

Import the framework:

@import MotionAnimator;

You will now have access to all of the APIs.

Guides

How to make a spec from existing animations

A motion spec is a complete representation of the motion curves that meant to be applied during an animation. Your motion spec might consist of a single MDMMotionTiming instance, or it might be a nested structure of MDMMotionTiming instances, each representing motion for a different part of a larger animation. In either case, your magic motion constants now have a place to live.

Consider a simple example of animating a view on and off-screen. Without a spec, our code might look like so:

CGPoint before = dismissing ? onscreen : offscreen;
CGPoint after = dismissing ? offscreen : onscreen;
view.center = before;
[UIView animateWithDuration:0.5 animations:^{
  view.center = after;
}];

What if we want to change this animation to use a spring curve instead of a cubic bezier? To do so we'll need to change our code to use a new API:

CGPoint before = dismissing ? onscreen : offscreen;
CGPoint after = dismissing ? offscreen : onscreen;
view.center = before;
[UIView animateWithDuration:0.5 delay:0 usingSpringWithDamping:0.7 initialSpringVelocity:0 options:0 animations:^{
  view.center = after;
} completion:nil];

Now let's say we wrote the same code with a motion spec and animator:

MDMMotionTiming motionSpec = {
  .duration = 0.5, .curve = MDMMotionCurveMakeSpring(1, 100, 1),
};

MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init];
animator.shouldReverseValues = dismissing;
view.center = offscreen;
[_animator animateWithTiming:kMotionSpec animations:^{
  view.center = onscreen;
}];

Now if we want to change our motion back to an easing curve, we only have to change the spec:

MDMMotionTiming motionSpec = {
  .duration = 0.5, .curve = MDMMotionCurveMakeBezier(0.4f, 0.0f, 0.2f, 1.0f),
};

The animator code stays the same. It's now possible to modify the motion parameters at runtime without affecting any of the animation logic.

This pattern is useful for building transitions and animations. To learn more through examples, see the following implementations:

Material Components Activity Indicator

Material Components Progress View

Material Components Masked Transition

How to animate explicit layer properties

MDMMotionAnimator provides an explicit API for adding animations to animatable CALayer key paths. This API is similar to creating a CABasicAnimation and adding it to the layer.

[animator animateWithTiming:timing.chipHeight
                    toLayer:chipView.layer
                 withValues:@[ @(chipFrame.size.height), @(headerFrame.size.height) ]
                    keyPath:MDMKeyPathHeight];

How to animate like UIView

MDMMotionAnimator provides an API that is similar to UIView's animateWithDuration:. Use this API when you want to apply the same timing to a block of animations:

chipView.frame = chipFrame;
[animator animateWithTiming:timing.chipHeight animations:^{
  chipView.frame = headerFrame;
}];
// chipView.layer's position and bounds will now be animated with timing.chipHeight's timing.

How to animate a transition

Start by creating an MDMMotionAnimator instance.

MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init];

When we describe our transition we‘ll describe it as though we’re moving forward and take advantage of the shouldReverseValues property on our animator to handle the reverse direction.

animator.shouldReverseValues = isTransitionReversed;

To animate a property on a view, we invoke the animate method. We must provide a timing, values, and a key path:

[animator animateWithTiming:timing
                    toLayer:view.layer
                 withValues:@[ @(collapsedHeight), @(expandedHeight) ]
                    keyPath:MDMKeyPathHeight];

How to animate an interruptible transition

MDMMotionAnimator is configured by default to generate interruptible animations using Core Animation‘s additive animation APIs. You can simply re-execute the animate calls when your transition’s direction changes and the animator will add new animations for the updated direction.

Helpful literature

Contributing

We welcome contributions!

Check out our upcoming milestones.

Learn more about our team, our community, and our contributor essentials.

License

Licensed under the Apache 2.0 license. See LICENSE for details.