Merge branch 'release-candidate' into stable
tree: 5da95ebd85ddb03bd9ed06af5e17373ac6c6346a
  1. assets/
  2. examples/
  3. src/
  4. tests/
  5. .clang-format
  6. .codecov.yml
  7. .gitignore
  8. .jazzy.yaml
  9. .kokoro
  10. .swift-version
  11. .travis.yml
  12. AUTHORS
  13. BUILD
  14. CHANGELOG.md
  15. CONTRIBUTING.md
  16. LICENSE
  17. MotionAnimator.podspec
  18. Podfile
  19. Podfile.lock
  20. README.md
  21. WORKSPACE
README.md

Motion Animator

A Motion Animator creates performant, interruptible animations from motion specs.

Build Status codecov CocoaPods Compatible Platform Docs


This library provides APIs that turn Motion Interchange motion specifications into animations.


What is a motion specification?

A motion specification defines the delay, duration, and acceleration of animations in a simple data format that can live separate from your animation logic.

For example, let's say we wanted to describe the motion for this animation:

We might create a specification like so:

struct CalendarChipTiming {
  MDMMotionTiming chipWidth;
  MDMMotionTiming chipHeight;
  MDMMotionTiming chipY;

  MDMMotionTiming chipContentOpacity;
  MDMMotionTiming headerContentOpacity;

  MDMMotionTiming navigationBarY;
};
typedef struct CalendarChipTiming CalendarChipTiming;

struct CalendarChipMotionSpec {
  CalendarChipTiming expansion;
  CalendarChipTiming collapse;
};
typedef struct CalendarChipMotionSpec CalendarChipMotionSpec;

FOUNDATION_EXTERN struct CalendarChipMotionSpec CalendarChipSpec;

With our implementation of the spec looking like so:

struct CalendarChipMotionSpec CalendarChipSpec = {
  .expansion = {
    .chipWidth = {
      .delay = 0.000, .duration = 0.285, .curve = MDMEightyForty,
    },
    .chipHeight = {
      .delay = 0.015, .duration = 0.360, .curve = MDMEightyForty,
    },
    ...
  },
  .collapse = {
    .chipWidth = {
      .delay = 0.045, .duration = 0.330, .curve = MDMEightyForty,
    },
    .chipHeight = {
      .delay = 0.000, .duration = 0.330, .curve = MDMEightyForty,
    },
    ...
  },
};

Our spec defines two different transition states: expansion and collapse. At runtime, we determine which of these two specs we intend to use and then use the timings to animate our views with an instance of MDMMotionAnimator:

CalendarChipTiming timing = _expanded ? CalendarChipSpec.expansion : CalendarChipSpec.collapse;

MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init];
animator.shouldReverseValues = !_expanded;

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

A working implementation of this example can be seen in the included example app.

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:

static const MDMMotionTiming kMotionSpec = {
  .duration = 0.5, .curve = _MDMSpring(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:

static const MDMMotionTiming kMotionSpec = {
  .duration = 0.5, .curve = _MDMBezier(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.

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.