Advanced Natural Motion Animations, Simple Blocks Based Syntax
FlightAnimator
Introduction
FlightAnimator provides a very simple blocks based animation definition language that allows you to dynamically create, configure, group, sequence, cache, and reuse property animations.
Unlike CAAnimationGroups
, and UIViewAnimations
, which animate multiple properties using a single easing curve, FlightAnimator allows configuration, and synchronization, of unique easing curves per individual property animation.
Features
- [x] 46+ Parametric Curves, Decay, and Springs
- [x] Blocks Syntax for Building Complex Animations
- [x] Chain and Sequence Animations:
- [x] Apply Unique Easing per Property Animation
- [x] Advanced Multi-Curve Group Synchronization
- [x] Define, Cache, and Reuse Animations
Check out the FlightAnimator Project Demo in the video below to
experiment with all the different capabilities of the FlightAnimator.
Installation
- Requirements : XCode 7.3+, iOS 8.0+, tvOS 9.0+
- Installation Instructions
- Release Notes
Communication
- If you found a bug, or have a feature request, open an issue.
- If you need help or a general question, use Stack Overflow. (tag 'flight-animator')
- If you want to contribute, review the Contribution Guidelines, and submit a pull request.
Basic Use
FlightAnimator provides a very flexible syntax for defining animations ranging in complexity with ease. Following a blocks based builder approach you can easily define an animation group, and it's property animations in no time.
Under the hood animations built are CAAnimationGroup
(s) with multiple custom CAKeyframeAnimation
(s) defined uniquely per property. Once it's time to animate, FlightAnimator will dynamically synchronize the remaining progress for all the animations relative to the current presentationLayer's values, then continue to animate to it's final state.
Simple Animation
To really see the power of FlightAnimator, let's first start by defining an animation using CoreAnimation
, then re-define it using the framework's blocks based syntax. The animation below uses a CAAnimationGroup
to group 3 individual CABasicAnimations
for alpha, bounds, and position.
let alphaAnimation = CABasicAnimation(keyPath: "position")
alphaAnimation.toValue = 0.0
alphaAnimation.fromValue = 1.0
alphaAnimation.fillMode = kCAFillModeForwards
alphaAnimation.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)
let boundsAnimation = CABasicAnimation(keyPath: "bounds")
boundsAnimation.toValue = NSValue(CGRect : toBounds)
boundsAnimation.fromValue = NSValue(CGRect : view.layer.bounds)
boundsAnimation.fillMode = kCAFillModeForwards
boundsAnimation.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)
let positionAnimation = CABasicAnimation(keyPath: "position")
positionAnimation.toValue = NSValue(CGPoint : toPosition)
positionAnimation.fromValue = NSValue(CGPoint : view.layer.position)
positionAnimation.fillMode = kCAFillModeForwards
positionAnimation.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)
let progressAnimation = CABasicAnimation(keyPath: "animatableProgress")
progressAnimation.toValue = 1.0
progressAnimation.fromValue = 0
progressAnimation.fillMode = kCAFillModeForwards
progressAnimation.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)
let animationGroup = CAAnimationGroup()
animationGroup.duration = 0.5
animationGroup.removedOnCompletion = true
animationGroup.animations = [alphaAnimation, boundsAnimation, positionAnimation, progressAnimation]
view.layer.addAnimation(animationGroup, forKey: "PositionAnimationKey")
view.frame = toFrame
Now that we saw the example above. Let's re-define FlightAnimator's blocks based syntax
view.animate { [unowned self] (animator) in
animator.alpha(toAlpha).duration(0.5).easing(.OutCubic)
animator.bounds(toBounds).duration(0.5).easing(.OutCubic)
animator.position(toPosition).duration(0.5).easing(.OutCubic)
animator.value(toProgress, forKeyPath : "animatableProgress").duration(0.5).easing(.OutCubic)
}
Calling animate(:)
on the view begins the FAAnimationGroup
creation process. Inside the closure the animator creates, configures, then appends custom animations to the newly created parent group. Define each individual property animation by calling one of the pre-defined property setters, and/or the func value(:, forKeyPath:) -> PropertyAnimator
method for any other animatable property.
Once the property animation is initiated, recursively configure the PropertyAnimator
by chaining duration, easing, and/or primary designation, to create the final FABasicAnimation
, and add it to the parent group.
func duration(duration : CGFloat) -> PropertyAnimator
func easing(easing : FAEasing) -> PropertyAnimator
func primary(primary : Bool) -> PropertyAnimator
Once the function call exits the closure, FlightAnimator performs the following:
- Adds the newly created
FAAnimationGroup
to the calling view's layer, - Synchronizes the grouped
FABasicAnimations
relative to the calling view's presentation layer values - Triggers the animation by applying the toValue from the grouped animations to to the calling view's layer.
Chaining Animations
Chaining animations together in FlightAnimator is simple.
Trigger on Start
The animation created on the secondaryView is triggered once the the primaryView's animation begins.
primaryView.animate { [unowned self] (animator) in
....
animator.triggerOnStart(onView: self.secondaryView, animator: { (animator) in
....
})
}
Trigger on Completion
The animation created on the secondaryView is triggered once the the primaryView's animation completes.
primaryView.animate { [unowned self] (animator) in
....
animator.triggerOnCompletion(onView: self.secondaryView, animator: { (animator) in
....
})
}
Time Progress Trigger
The animation created on the secondaryView is triggered when the driving animation reaches the relative half way point in duration on the primaryView's animation.
primaryView.animate { [unowned self] (animator) in
....
animator.triggerOnProgress(0.5, onView: self.secondaryView, animator: { (animator) in
....
})
}
Value Progress Trigger
The animation created on the secondaryView is triggered when the driving animation reaches the relative half way point between the fromValue and toValue of the primaryView's animation. This is driven
primaryView.animate { [unowned self] (animator) in
....
animator.triggerOnValueProgress(0.5, onView: self.secondaryView, animator: { (animator) in
....
})
}
Nesting Animation Triggers
There is built in support for nesting triggers within triggers to sequence animations, and attach multiple types of triggers relative to the scope of the parent animation.
primaryView.animate { [unowned self] (animator) in
....
animator.triggerOnStart(onView: self.secondaryView, animator: { (animator) in
-> Relative to primaryView animation
animator.triggerOnCompletion(onView: self.tertiaryView, animator: { (animator) in
-> Relative to secondaryView animation
animator.triggerOnProgress(0.5, onView: self.quaternaryView, animator: { (animator) in
-> Relative to tertiaryView animation
})
})
animator.triggerOnValueProgress(0.5, onView: self.quinaryView, animator: { (animator) in
-> Relative to secondaryView animation
})
})
animator.triggerOnStart(onView: self.senaryView, animator: { (animator) in
-> Relative to primaryView animation
})
}
CAAnimationDelegate Callbacks
Sometimes there is a need to perform some logic on the start of an animation, or the end of the animation by responding to the CAAnimationDelegate methods
view.animate { (animator) in
....
animator.setDidStartCallback({ (animator) in
// Animation Did Start
})
animator.setDidStopCallback({ (animator, complete) in
// Animation Did Stop
})
}
These can be nested just as the animation triggers, and be applied animator on the group in scope of the animation creation closure by the animator.
Cache & Reuse Animations
FlightAnimator allows for registering animations (aka states) up front with a unique animation key. Once defined it can be manually triggered at any time in the application flow using the animation key used registration.
When the animation is applied, if the view is in mid flight, it will synchronize itself with the current presentation layer values, and animate to its final destination.
Register/Cache Animation
To register an animation, call a globally defined method, and create an animations just as defined earlier examples within the maker block. The following example shows how to register, and cache an animation for a key on a specified view.
struct AnimationKeys {
static let CenterStateFrameAnimation = "CenterStateFrameAnimation"
}
...
view.registerAnimation(forKey : AnimationKeys.CenterStateFrameAnimation) { (animator) in
animator.bounds(newBounds).duration(0.5).easing(.OutCubic)
animator.position(newPositon).duration(0.5).easing(.OutCubic)
})
This animation is only cached, and is not performed until it is manually triggered.
Apply Registered Animation
To trigger the animation call the following
view.applyAnimation(forKey: AnimationKeys.CenterStateFrameAnimation)
To apply final values without animating the view, override the default animated flag to false, and it will apply all the final values to the model layer of the associated view.
view.applyAnimation(forKey: AnimationKeys.CenterStateFrameAnimation, animated : false)
Advanced Use
Timing Adjustments
Due to the dynamic nature of the framework, it may take a few tweaks to get the animation just right.
FlightAnimator has a few options for finer control over timing synchronization:
- Timing Priority - Adjust how the time is select during synchronization of the overall animation
- Primary Drivers - Defines animations that affect timing during synchronization of the overall animation
Timing Priority
First a little background, the framework basically does some magic so synchronize the time by prioritizing the maximum time remaining based on progress if redirected in mid flight.
Lets look at the following example of setting the timingPriority on a group animation to .MaxTime, which is the default value for FlightAnimator.
func animateView(toFrame : CGRect) {
let newBounds = CGRectMake(0,0, toFrame.width, toFrame.height)
let newPosition = CGPointMake(toFrame.midX, toFrame.midY)
view.animate(.MaxTime) { (animator) in
animator.bounds(newBounds).duration(0.5).easing(.OutCubic)
animator.position(newPositon).duration(0.5).easing(.InSine)
}
}
Just like the demo app, This method gets called by different buttons, and takes on the frame value of button that triggered the method. Let's the animation has been triggered, and is in mid flight. While in mid flight another button is tapped, a new animation is applied, and ehe position changes, but the bounds stay the same.
Internally the framework will figure out the current progress in reference to the last animation, and will select the max duration value from the array of durations on the grouped property animations.
Lets assume the bounds don't change, thus animation's duration is assumed to be 0.0 after synchronization. The new animation will synchronize to the duration of the position animation based on progress, and automatically becomes the max duration based on the .MaxTime timing priority.
The timing priority can also be applied on triggerAtTimeProgress()
or triggerAtValueProgress()
. Now this leads into the next topic, and that is the primary flag.
The more property animations within a group, the more likely the need to adjust how the timing is applied. For this purpose there are 4 timing priorities to choose from:
- .MaxTime
- .MinTime
- .Median
- .Average
Primary Flag
As in the example prior, there is a mention that animations can get quite complex, and the more property animations within a group, the more likely the animation will have a hick-up in the timing, especially when synchronizing 4+ animations with different curves and durations.
For this purpose, set the primary flag on individual property animations, and designate them as primary duration drivers. By default, if no property animation is set to primary, during synchronization, FlightAnimator will use the timing priority setting to find the corresponding value from all the animations after progress synchronization.
If we need only some specific property animations to define the progress accordingly, and become the primary drivers, set the primary flag to true, which will exclude any other animation which is not marked as primary from consideration.
Let's look at an example below of a simple view that is being animated from its current position to a new frame using bounds and position.
view.animate(.MaxTime) { (animator) in
animator.bounds(newBounds).duration(0.5).easing(.OutCubic).primary(true)
animator.position(newPositon).duration(0.5).easing(.InSine).primary(true)
animator.alpha(0.0).duration(0.5).easing(.OutCubic)
animator.transform(newTransform).duration(0.5).easing(.InSine)
}
Simple as that, now when the view is redirected during an animation in mid flight, only the bounds and position animations will be considered as part of the timing synchronization.
.SpringDecay w/ Initial Velocity
When using a UIPanGestureRecognizer to move a view around on the screen by adjusting its position, and say there is a need to smoothly animate the view to the final destination right as the user lets go of the gesture. This is where the .SpringDecay easing comes into play. The .SpringDecay easing will slow the view down easily into place, all that need to be configured is the initial velocity, and it will calculate its own time relative to the velocity en route to its destination.
Below is an example of how to handle the handoff and use .SpringDecay(velocity: velocity)
easing to perform the animation.
func respondToPanRecognizer(recognizer : UIPanGestureRecognizer) {
switch recognizer.state {
........
case .Ended:
let currentVelocity = recognizer.velocityInView(view)
view.animate { (animator) in
animator.bounds(finalBounds).duration(0.5).easing(.OutCubic)
animator.position(finalPositon).duration(0.5).easing(.SpringDecay(velocity: velocity))
}
default:
break
}
}
Reference
CALayer's Supported Animatable Property
Framework Demo App
The project includes a highly configurable demo app that allows for experimentation to explore resulting effects of the unlimited configurations FlightAnimator supports.
Demo Features Included:
- Animate a view to different location on the screen
- Drag and release view to apply Decay easing to the final destination
- Adjust timing curves for bounds, position, alpha, and transform.
- Enable a secondary view, which follows the main view to it's last location
- Adjust group timing priority to test synchronization
- Adjust progress for time based/value based triggers on the secondary view
License
FlightAnimator is released under the MIT license. See License for details.