This is the third in a series of posts I’m writing on animating iOS interfaces using Core Animation. In the first post I created a planetary orbit demo using nested CALayer objects. The second post showed how to dress up a UI by animating an image.
This time I’ll show how you can trigger animations in response to button actions to illustrate to the user that an action is taking place.
Button Actions sample
In my next demo I attempted to show a few common animations that standard iOS applications use to show the user that some action is being performed. If you look at how the Mail application on the iPad behaves when a user deletes multiple messages, several animations are taking place at the same time.
For now I’ve only shown two button animations, though I plan to add more in the future.
The first example scales the button when it is clicked. This short animation scales up to 1.2 times its size, then scales back down to its original size. This animation is probably better suited to a button that’s based on an image, or a button that has some sort of gloss or gradient effect applied.
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"]; anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; anim.duration = 0.125; anim.repeatCount = 1; anim.autoreverses = YES; anim.removedOnCompletion = YES; anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.2, 1.2, 1.0)]; [buttonView.layer addAnimation:anim forKey:nil];
By first creating the CABasicAnimation object with a keyPath of “transform”, we’re saying that we are going to be animating some part of the object’s 3D affine transform. If you recall from the first post with the Orbiting Planets sample, we were animating the keyPath “transform.rotation”. That allowed us to pass just the rotation we wanted to perform as a number. In this sample I wanted to show that you could animate more than just the rotation. Here I’m passing in a complete CATransform3D struct, which means we could animate the scale, rotation and translation of the layer all at the same time.
Once we have our object we set a reasonable timing function, saying we want the animation to ease-in and -out smoothly, creating a more realistic feeling to it.
At this point the real meat of the animation kicks in. We specify a short duration for each iteration of the loop, but say that we want the animation to repeat once. This means that the animation will be performed two times, meaning the total duration of this animation will be twice as long as the value we specify here, a full 0.25 seconds.
Perhaps the most important line here is the one that sets the “autoreverses” property to YES. This means that every other loop through the animation, it will reverse the fromValue and toValue. This is crucial, because otherwise the animation will appear to snap back to its original state every 125ms.
Finally we specify the toValue that we want to animate. We don’t have to specify a fromValue since it defaults to whatever the current value of the object is at; this is especially handy if we don’t know what that value is when we create the animation. You’ll also notice that we create an NSValue object, rather than passing the CATransform3D struct in directly. Take a look at the constructor helpers on NSValue, since there are many different types of structs it’s able to convert for use in animations.
Finally we add the animation to our button’s layer, and get to watch the animation perform.
The second button sample I created mimics the animation the iPad Mail app performs when you delete multiple messages. This shows an icon shrinking and fading out as it follows a curve into the Delete button. In the Mail app however this is followed by the Delete button pulsing outward making it appear to gobble up the deleted messages. By combining this effect with the one I described up above you can produce something very similar to Apple’s Mail application.
Since multiple animations are being performed, I’ll describe each animation by itself first. If you are having difficulty following along, skip to the end and watch the video to see what I’m talking about.
CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:@"transform"]; scaleAnim.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity]; scaleAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)]; scaleAnim.removedOnCompletion = YES;
The first animation we’ll want to perform is to scale the icon down to a smaller size. As before we’re creating a CABasicAnimation object, working on the “transform” property. In this case we’re explicitly setting the fromValue to CATransform3DIdentity, which is a Core Animation constant describing a transform that has no rotation, no scaling, and no translation.
From there we animate to a small, but not zero, scale size, and then specify we want the animation object to be removed automatically once it’s complete.
CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:@"alpha"]; opacityAnim.fromValue = [NSNumber numberWithFloat:1.0]; opacityAnim.toValue = [NSNumber numberWithFloat:0.1]; opacityAnim.removedOnCompletion = YES;
The second animation is manipulating the opacity of the image, since we’ll want it to gently fade out as it animates. It’s important to note that we’re creating our CABasicAnimation object on the “alpha” property, and not the “opacity” property you may be used to dealing with. Since Core Animation works on layers, rather than UIView objects, the properties it manipulates are CALayer properties. Pretty much any property of a CALayer can be animated (with the exception of boolean values, since there’s really no way to interpolate between YES and NO), but there are some subtle differences between CALayer and UIView instances you’ll want to read up on.
UIBezierPath *movePath = [UIBezierPath bezierPath]; CGPoint ctlPoint = CGPointMake(buttonView.center.x, icon.center.y); [movePath moveToPoint:icon.center]; [movePath addQuadCurveToPoint:buttonView.center controlPoint:ctlPoint]; CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:@"position"]; moveAnim.path = movePath.CGPath; moveAnim.removedOnCompletion = YES;
The more complicated animation is the one that moves the icon into the Delete button. We don’t want to have the icon animate in a linear line towards the button since that doesn’t look realistic. Instead we want it to follow a curve, or a path, towards the button.
For this we create a UIBezierPath object and describe a quadratic curve to the position we want. It sounds more complicated than it is. If you’ve ever used Photoshop or any other image editing tools that let you manipulate paths, this will be familiar to you. The quadratic curve simply lets you describe a line with three points: the start point, the end point, and a “Control Point” that lets you control how the two ends of the line will curve.
Once the UIBezierPath has been created, starting from the icon’s center point and ending at the button’s center point, we create a CAKeyframeAnimation on the “position” CALayer property. The nice thing about CAKeyframeAnimation is that it lets you animate an object along a path, and Core Animation will interpolate each frame along that path.
CAAnimationGroup *animGroup = [CAAnimationGroup animation]; animGroup.animations = [NSArray arrayWithObjects:moveAnim, scaleAnim, opacityAnim, nil]; animGroup.duration = 0.5; [icon.layer addAnimation:animGroup forKey:nil];
Finally, since we want all these animations to happen at the same time, we need to create one final animation object. The CAAnimationGroup object lets you combine multiple CAAnimation objects together into a single aggregate animation. The duration supplied here will override the individual durations described on the sub-animations, allowing for a clean and smooth transition.
When we’re done, we add the animation to the icon’s CALayer and watch it run.
Stay tuned for details on my other samples. For now, feel free to download the sample project and pick through the code yourself. And don’t forget to read the previous Orbiting Planets sample or the Floating Clouds sample.