Giter VIP home page Giter VIP logo

motion-animator-objc's Introduction

Motion Animator Banner

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

Build Status codecov CocoaPods Compatible Platform

๐ŸŽ‰Implicit and explicit additive animations.
๐ŸŽ‰Parameterized motion with the Interchange.
๐ŸŽ‰Provide velocity to animations directly from gesture recognizers.
๐ŸŽ‰Maximize frame rates by relying more on Core Animation.
๐ŸŽ‰Animatable properties are Swift enum types.
๐ŸŽ‰Consistent model layer value expectations.

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

CALayer anchorPoint
CALayer backgroundColorUIView backgroundColor
CALayer boundsUIView bounds
CALayer borderWidth
CALayer borderColor
CALayer cornerRadius
CALayer heightUIView height
CALayer opacityUIView alpha
CALayer positionUIView center
CALayer rotationUIView rotation
CALayer scaleUIView scale
CALayer shadowColor
CALayer shadowOffset
CALayer shadowOpacity
CALayer shadowRadius
CALayer transformUIView transform
CALayer widthUIView width
CALayer xUIView x
CALayer yUIView y
CALayer z
CAShapeLayer strokeStart
CAShapeLayer strokeEnd

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 9 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:

Core Animation: a deep dive

Recommended reading:

There are two primary ways to animate with Core Animation on iOS:

  1. implicitly, with the UIView animateWithDuration: APIs, or by setting properties on standalone CALayer instances (those that are not backing a UIView), and
  2. explicitly, with the CALayer addAnimation:forKey: APIs.

A subset of UIView's and CALayer's public APIs is animatable by Core Animation. Of these animatable properties, some are implicitly animatable while some are not. Whether a property is animatable or not depends on the context within which it's being animated, and whether an animation is additive or not depends on which animation API is being used. With this matrix of conditions it's understandable that it can sometimes be difficult to know how to effectively make use of Core Animation.

The following quiz helps illustrate that the UIKit and Core Animation APIs can often lead to unintuitive behavior. Try to guess which of the following snippets will generate an animation and, if they do, what the generated animation's duration will be:

Imagine that each code snippet is a standalone unit test (because they are!).

let view = UIView()
UIView.animate(withDuration: 0.8, animations: {
  view.alpha = 0.5
})
Click to see the answer Generates an animation with duration of 0.8.

let view = UIView()
UIView.animate(withDuration: 0.8, animations: {
  view.layer.opacity = 0.5
})
Click to see the answer Generates an animation with duration of 0.8.

let view = UIView()
UIView.animate(withDuration: 0.8, animations: {
  view.layer.cornerRadius = 3
})
Click to see the answer On iOS 11 and up, generates an animation with duration of 0.8. Older operating systems will not generate an animation.

let view = UIView()
view.alpha = 0.5
Click to see the answer Does not generate an animation.

let view = UIView()
view.layer.opacity = 0.5
Click to see the answer Does not generate an animation.

let layer = CALayer()
layer.opacity = 0.5
Click to see the answer Does not generate an animation.

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
Click to see the answer Generates an animation with duration of 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
})
Click to see the answer Generates an animation with duration of 0.25. This isn't a typo: standalone layers read from the current CATransaction rather than UIView's parameters when implicitly animating, even when the change happens within a UIView animation block.

What properties can be explicitly animated?

For a full list of animatable CALayer properties, see the Apple documentation.

MotionAnimator's explicit APIs can be used to animate any property that is animatable by Core Animation.

What properties can be implicitly animated?

UIKit and Core Animation have different rules about when and how a property can be implicitly animated.

UIView properties generate implicit animations only when they are changed within an animateWithDuration: animation block.

CALayer properties generate implicit animations only when they are changed under either of the following conditions:

  1. if the CALayer is backing a UIView, the CALayer property is a supported implicitly animatable property (this is not documented anywhere), and the property is changed within an animateWithDuration: block, or
  2. if: the CALayer is not backing a UIView (an "unhosted layer"), the layer has been around for at least one CATransaction flush โ€” either by invoking CATransaction.flush() or by letting the run loop pump at least once โ€” and the property is changed at all.

This behavior can be somewhat difficult to reason through, most notably when trying to animate CALayer properties using the UIView animateWithDuration: APIs. For example, CALayer's cornerRadius was not animatable using animateWithDuration: up until iOS 11, and many other CALayer properties are still not implicitly animatable.

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

The MotionAnimator provides a more consistent implicit animation API with a well-defined set of supported properties.

In general, when will changing a property cause an implicit animation?

The following charts describe when changing a property on a given object will cause an implicit animation to be generated.

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 key path inside animation block outside animation block inside MotionAnimator animation block
alpha โœ“ โœ“
backgroundColor โœ“ โœ“
bounds โœ“ โœ“
bounds.size.height โœ“ โœ“
bounds.size.width โœ“ โœ“
center โœ“ โœ“
center.x โœ“ โœ“
center.y โœ“ โœ“
transform โœ“ โœ“
transform.rotation.z โœ“ โœ“
transform.scale โœ“ โœ“

Backing CALayer

Every UIView has a backing 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 key path inside animation block outside animation block inside MotionAnimator animation block
anchorPoint โœ“ (starting in iOS 11) โœ“
backgroundColor โœ“
bounds โœ“ โœ“
borderWidth โœ“
borderColor โœ“
cornerRadius โœ“ (starting in iOS 11) โœ“
bounds.size.height โœ“ โœ“
opacity โœ“ โœ“
position โœ“ โœ“
transform.rotation.z โœ“ โœ“
transform.scale โœ“ โœ“
shadowColor โœ“
shadowOffset โœ“
shadowOpacity โœ“
shadowRadius โœ“
strokeStart โœ“
strokeEnd โœ“
transform โœ“ โœ“
bounds.size.width โœ“ โœ“
position.x โœ“ โœ“
position.y โœ“ โœ“
zPosition โœ“

Unflushed, unhosted CALayer

CALayers are unflushed until the next CATransaction.flush() invocation, which can happen either directly or at the end of the current run loop.

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 key path inside animation block outside animation block inside MotionAnimator animation block
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 โœ“
position.x โœ“
position.y โœ“
zPosition โœ“

Flushed, unhosted 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 key path inside animation block outside animation block inside MotionAnimator animation block
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 โœ“ โœ“ โœ“
position.x โœ“ โœ“ โœ“
position.y โœ“ โœ“ โœ“
zPosition โœ“ โœ“ โœ“

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.

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.

motion-animator-objc's People

Contributors

arcank avatar jverkoey avatar sdefresne avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

motion-animator-objc's Issues

Add didComplete parameter to the completion blocks

Adding support for explicit animations is trivial because we can do a one-to-one mapping of animations to completion handlers.

Adding support for implicit animations, however, is non-trivial because multiple animations map to the same completion handler. We'll have to implement the animation delegate on each added animation and coalesce the overall completion state into a single invocation of the completion handler.

This would bring our API closer in alignment to UIView's animation APIs.

Add support for a static animator animate API

This would be similar to UIView's animate APIs:

MotionAnimator.animate(withTiming: timing) {

}

The implementation would simply create a throw-away motion animator instance and call through to the instance APIs.

Explore routing implicit animations through CAAction rather than our own internal record keeping

We may find it beneficial to to implement CAAction so that our animations get routed through the expected action architecture. The challenge is that we need to be able to provide the timing struct to the action implementation and that action implementation needs to be able to create an animation object using the animator's configurations. This may require splitting the animator's animation creation logic out to some standalone method.

Some references:

Bazel improvements

  • WORKSPACE should use http_archive instead of git_repository
  • Bump build_bazel_rules_apple to 0.1.0.

Add support for relative values

Values passed to the animator are currently absolute. It would be nice to also be able to provide relative values.

// Move a view 50 points down.
    [animator animateWithTiming:timing
                        toLayer:view.layer
             withRelativeValues:@[ @0, @50 ]
                        keyPath:MDMKeyPathY];

This feature should take care when implementing support for shouldReverseValues.

Returning `NSNull` for swapped animation implementations causes crash

In the method, ActionForKey(CALayer *layer, SEL _cmd, NSString *event), the returned NSNull value was causing crashes because it does not respond to selectors (like runAction:forKey:). This is easily reproduced when running the unit test suite on iOS 8.2 or lower simulators. The contract for actionForKey: should return nil if no action was found, which is what we want in this case.

Crash when completion blocks are `nil` for "legacy" API

Calling either of these two methods with a nil completion block will crash.

 [animator animateWithTiming:timing
                   animations:animations
                   completion:nil];

[animator animateWithTiming:timing
                       toLayer:aLayer
                    withValues:values
                      keyPath:keyPath
                   completion:nil];

Add support for relative timing

Consider the following spec:

struct MotionSpec spec = {
  .expand = {
    .verticalMovement = {
      .delay = 0.000, .duration = 0.350, .curve = EaseInEaseOut,
    },
    .scaling = {
      .delay = 0.000, .duration = 0.350, .curve = EaseInEaseOut,
    },
  },
  .collapse = {
    .verticalMovement = {
      .delay = 0.000, .duration = 0.350, .curve = EaseInEaseOut,
    },
    .scaling = {
      .delay = 0.000, .duration = 0.350, .curve = EaseInEaseOut,
    },
  },
};

All of the animations are described in relation to a relative timeline. If the timing could correspondingly be described in a relative fashion, our struct might look like this:

struct MotionSpec spec = {
  .expand = {
    .verticalMovement = {
      .delay = 0.0, .duration = 1.0, .curve = EaseInEaseOut,
    },
    .scaling = {
      .delay = 0.0, .duration = 1.0, .curve = EaseInEaseOut,
    },
  },
  .collapse = {
    .verticalMovement = {
      .delay = 0.0, .duration = 1.0, .curve = EaseInEaseOut,
    },
    .scaling = {
      .delay = 0.0, .duration = 1.0, .curve = EaseInEaseOut,
    },
  },
};

animator.relativeTimingDuration = 0.350;

All animation timing would correspondingly be multiplied by the relativeTimingDuration.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.