Our rock star designer Jonathan Saragousi has a fetish for beautiful easing transitions in our animations. The correct easing method can make the difference between a plain animation and a jaw-dropping one.
In Cal for instance, most of the animations use EaseOutQuart transitions. (You can find a nice demonstration of the different transitions here)
We implement the custom animations using a very cool project called NSBKeyframeAnimation.
A while ago we moved Cal to using Auto layouts, and of course – auto layout does not behave well at all with custom animations.
In auto layouts you don’t change the frame directly, but rather the constant of the constraints, and you animate by calling layoutIfNeeded inside an animation block. For example if you want to animate the height of a view with constraints you use this code:
NSLayoutConstraint *heightConstraint = self.heightConstraint; // Get the height constraint either using IBOutlet or by iterating the superview constraints [UIView animateWithDuration:duration animations:^{ heightConstraint.constant = newValue; [view layoutIfNeeded]; }];
The problem here is that we are forced to use [UIView animateWithDuration] instead of CoreAnimation so we can’t use NSBKeyframeAnimation.
To solve it we will use a timer to set the constraints constant to the correct value at every step.
The solution consists of two parts:
- Extending NSBKeyframeAnimation to calculate the positions at each step
- Using a timer to set the constraint’s constants to the correct position
Extending NSBKeyframeAnimation
NSBKeyframeAnimation doesn’t support getting the positions array for a custom transition type, but luckily, this functionality can be easily extended. Create a category on NSBKeyframeAnimation and implement the following method:
+(NSArray *) calculatePositionValuesForFunction:(NSBAnimationType)animationType startValue:(CGFloat)startValue endValue:(CGFloat)endValue withDuration:(CGFloat)duration { NSBKeyframeAnimationFunction function = [NSBKeyframeAnimation animationFunctionForType:animationType]; NSUInteger steps = (NSUInteger)ceil(kFPS * duration) + 2; NSMutableArray *valueArray = [NSMutableArray arrayWithCapacity:steps]; const double increment = 1.0 / (double)(steps - 1); double progress = 0.0, v = 0.0, value = 0.0; NSUInteger i; for (i = 0; i < steps; i++) { v = function(duration * progress * 1000, 0, 1, duration * 1000); value = startValue + v * (endValue - startValue); [valueArray addObject:[NSNumber numberWithDouble:value]]; progress += increment; } return [NSArray arrayWithArray:valueArray]; }
Building the AutoLayoutAnimator
To avoid confusion and allow starting and stopping animations easily, I assign constraints to a AutoLayoutAnimator at initialization.
For Cal I needed a convenient method to scale a view, so this is what I implemented, but of course – you can extend this class to support every type of animation.
To support scaling I needed both the width and height constraint, so the initialization of the animator is:
- (id)initWithView:(UIImageView *)view heightConstraint:(NSLayoutConstraint *)heightConstraint widthConstraint:(NSLayoutConstraint *)widthConstraint { self = [super init]; if (self) { self.view = view; self.heightConstraint = heightConstraint; self.widthConstraint = widthConstraint; } return self; }
We start the scaling animation by calculating the correct positions for both height and width constraint, calculating the number of steps and step duration – and fire up the timer
- (void)scaleViewToScale:(float)scale withAnimationFunction:(NSBAnimationType)function andDuration:(float)duration { self.heightValues = [NSBKeyframeAnimation calculatePositionValuesForFunction:function startValue:self.heightConstraint.constant endValue:self.heightConstraint.constant*scale withDuration:duration]; self.widthValues = [NSBKeyframeAnimation calculatePositionValuesForFunction:function startValue:self.widthConstraint.constant endValue:self.widthConstraint.constant*scale withDuration:duration]; self.numberOfSteps = [NSBKeyframeAnimation numberOfStepsForDuration:1.0]; self.currentStep = 0; float stepDuration = (duration / self.numberOfSteps); // Change the timer interval for speed regulation. self.timer = [NSTimer scheduledTimerWithTimeInterval:stepDuration target:self selector:@selector(animateView:) userInfo:nil repeats:YES]; }
The timer method simply updates the constraints’ constants, and stop when we finished
-(void) animateView:(NSTimer *)timer { if (self.currentStep == self.numberOfSteps) { self.heightConstraint.constant = [[self.heightValues lastObject] floatValue]; self.widthConstraint.constant = [[self.widthValues lastObject] floatValue]; [self.view layoutIfNeeded]; [timer invalidate]; self.timer = nil; } else { self.heightConstraint.constant = [self.heightValues[self.currentStep] floatValue]; self.widthConstraint.constant = [self.widthValues[self.currentStep] floatValue]; self.currentStep++; [self.view layoutIfNeeded]; } }
That’s it. Custom transitions are a powerful new tool for your UI arsenal – now go and make your apps shine 🙂
As always, you can find the code on Github.
NOTE: iOS7 introduced a very cool API called [UIView animateKeyframesWithDuration] which can be used to generate animation blocks for key frame animations easily. Since there are still a lot of people using iOS6 (around 21% currently) I decided to create the AutoLayoutAnimator without these new API, but I might change it in the future.