You probably have seen it hundreds of times, it’s become so natural to you that you probably don’t consciously notice it any more. I’m speaking of the bouncing of icons on the dock in OS X. The method how those pesky little critters (aka “Icons”) try to win your attention. Me! Me! ME!
This animation is probably the one you see the most in your day-to-day business working on code on a Mac. Yet I have never seen anybody using it in an iOS app. Why? It’s not that this animation is the sort of Clippy that everybody hopes to forget about some day. It’s something that well established and we know what it means.
When I asked around (on Twitter) and looked around (on Google) was only found a couple of “spring loaded” formulas, but nothing concrete that would enable me to get this animation added to my app. So I researched it and now I’m happy to present to you … 3 Methods of bouncing.
I’m going to show you 3 styles of achieving a similar effect so that you can form your own opinion which you like most. I’m not a computer scientist but I would roughly name the 3 styles:
- Simulation – uses something akin to physics to get the motion
- Keyframed – use fixed points at given times to have the appearance of something physical without actually having physics
- Function – uses a formula, often involving some cosine functions and factors.
All three are using CAKeyframeAnimation as their vessel of transport for the movement over time.
Setting up the Test
To have a way to see the different animations side-by-side I set a quick view controller with some buttons and a red square which will be the view that I want to bounce.
Let’s start from the bottom (because this is the one liked least) …
Function – AHEasing
Warren More has AHEasing on his GitHub, “a library of easing functions”. To put it in simple terms, any easing function takes a start and an end point and then has a scheme to get from point A to point B. This transition could be linear, ease-in/ease-out and many others. This library is very simple to use and it also provides a bounce easing function which we’ll going to try out.
We clone the project to some place and from it we need 4 files:
The .c and .h file contains the actual functions, the category on CAKeyframeAnimation provides several convenience methods to get the animation for a specific function.
In all cases the easing has to go from one point to another. So that rules out an initial jumping motion unless we were to ease that extra somehow. We have two options, we can either animate the keyPath position.y or animate between two points.
- (void)ease { CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position" function:BounceEaseOut fromPoint:CGPointMake(_bounceView.center.x, _bounceView.center.y - 150.0) toPoint:_bounceView.center keyframeCount:90]; animation.duration = 1.5; [_bounceView.layer addAnimation:animation forKey:@"easing"]; } |
I set the duration to 1.5 seconds and then added the animation to the layer of the bounce view. This causes it to jump up by 150 points and then do the dropping and bouncing.
This method of bouncing was mentioned several times to me on Twitter as the definitive method of achieving the effect. But as I said it has two major drawbacks for my purpose: 1) I don’t get easily the initial rising launch motion and 2) you cannot animate between arbitrary values, but only points, sizes and key paths. Which means that you always have to know the position of the animated view to use it.
Keyframed – Motion Tracking
I had my friend Christian Pfandler – who is a wizard with Adobe Premiere – record several icon sizes bouncing and then do some motion tracking on it. This shows exactly the movement that the icons do in OS X.
This might be actually done with a mathematical formula, but for our intents and purposes Christian compile a set of 32 factors that give you the vertical position offset if you multiply it with the size of the bouncing icon. Apparently the larger an icon is, the higher it bounces from the dock.
Since I’m putting this into my own CAKeyframeAnimation category I am free to use the transform property of CALayer instead of the offset which makes this way more convenient. With this approach you don’t need to know where the animated view is, just that it starts and ends with the identity transform. It jumps off its current position and once it returns to rest it ends up where it started.
+ (CAKeyframeAnimation *)dockBounceAnimationWithIconHeight:(CGFloat)iconHeight { CGFloat factors[32] = {0, 32, 60, 83, 100, 114, 124, 128, 128, 124, 114, 100, 83, 60, 32, 0, 24, 42, 54, 62, 64, 62, 54, 42, 24, 0, 18, 28, 32, 28, 18, 0}; NSMutableArray *values = [NSMutableArray array]; for (int i=0; i<32; i++) { CGFloat positionOffset = factors[i]/128.0f * iconHeight; CATransform3D transform = CATransform3DMakeTranslation(0, -positionOffset, 0); [values addObject:[NSValue valueWithCATransform3D:transform]]; } CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; animation.repeatCount = 1; animation.duration = 32.0f/30.0f; animation.fillMode = kCAFillModeForwards; animation.values = values; animation.removedOnCompletion = YES; // final stage is equal to starting stage animation.autoreverses = NO; return animation; } |
Adding this to the bounce view when the matching button is pushed is easy:
- (void)bounce { CAKeyframeAnimation *animation = [CAKeyframeAnimation dockBounceAnimationWithIconHeight:150]; [_bounceView.layer addAnimation:animation forKey:@"jumping"]; } |
I called this method the keyframe method because it has 32 key frames each specifying an exact value for this point in time.
The third method is to employ some basic physical modeling to arrive at a similar result, but with the individual values being calculated based on our own physical parameters.
Simulation – Almost Physics!
I developed this method by slicing the process into tiny pieces and imagining what the physical world would do in every time slice. We would start with an initial upwards momentum and then gravity would start changing this momentum by a fixed factor per second. In the real world this factor is 9.81 m/s that an upwards launch gets decreased in speed every second. Hence the gravity constant g is 9.81 m/s squared.
Because of this downwards pull the momentum will reach an apex and then start plunging downwards, ever faster until it reaches the bottom point. There the momentum will be reflected, but slightly dampened. The more of it is preserved the longer the bouncing will last. I arrived at these values with a bit of experimentation, you can modify them to your heart’s content to find something that suits you better.
More initial momentum will have it lauch faster and possibly higher. A higher gravity constant will reduce the height and make it accelerate downwards more steeply. And different dampening constants will cause the rebounds to be longer or shorter.
+ (CAKeyframeAnimation *)jumpAnimation { // these three values are subject to experimentation CGFloat initialMomentum = 300.0f; // positive is upwards, per sec CGFloat gravityConstant = 250.0f; // downwards pull per sec CGFloat dampeningFactorPerBounce = 0.6; // percent of rebound // internal values for the calculation CGFloat momentum = initialMomentum; // momentum starts with initial value CGFloat positionOffset = 0; // we begin at the original position CGFloat slicesPerSecond = 60.0f; // how many values per second to calculate CGFloat lowerMomentumCutoff = 5.0f; // below this upward momentum animation ends CGFloat duration = 0; NSMutableArray *values = [NSMutableArray array]; do { duration += 1.0f/slicesPerSecond; positionOffset+=momentum/slicesPerSecond; if (positionOffset<0) { positionOffset=0; momentum=-momentum*dampeningFactorPerBounce; } // gravity pulls the momentum down momentum -= gravityConstant/slicesPerSecond; CATransform3D transform = CATransform3DMakeTranslation(0, -positionOffset, 0); [values addObject:[NSValue valueWithCATransform3D:transform]]; } while (!(positionOffset==0 && momentum < lowerMomentumCutoff)); CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; animation.repeatCount = 1; animation.duration = duration; animation.fillMode = kCAFillModeForwards; animation.values = values; animation.removedOnCompletion = YES; // final stage is equal to starting stage animation.autoreverses = NO; return animation; } |
The resulting animation will have a duration that is a result of the factors. CoreAnimation does not use fixed animation timestamps but instead sees the interim values as evenly distributed in the duration set. So you can accelerate the whole animation also be specifying a fixed duration instead of tinkering with the parameters. To have a similar duration as the other animations I set it to 1.5 seconds.
- (void)jump { CAKeyframeAnimation *animation = [CAKeyframeAnimation jumpAnimation]; animation.duration = 1.5; [_bounceView.layer addAnimation:animation forKey:@"jumping"]; } |
The test project with all the code can be found in my GitHub Exmples archive.
Update: I recorded how this looks with my iPhone.
Conclusion
AHEasing is an interesting work of computer science, but unfortunately not usable for us for the given reasons. Which leaves us with two viable options. If you want something more physically correct you’ll best go with the simulation approach. If you want to have it match Apple’s style then you go with the key framed approach from the motion tracker.
We could also add a real physics engine like Box2D to your app and have it simulate the movement, but I’m sure you’ll agree that this is overkill.
Either way you see that with a bit of imagination you can create CAKeyframeAnimations that package cool animations in a way that lets you reuse them in all your apps. (And when you do please be so kind as to mention Cocoanetics in the credits, under “Special F/X”)
Categories: Recipes
Hmmmmm, Exmples link refers to another Xcode project. Interesting, but another one.
oh, must have forgotten to push that to GitHub.
Great. Example link still refers to another project. Can you please update? Thanks.
I was able to find and reconstruct the demo, it’s now in the Examples repo.
Hi,
How do you implement, say while a primary animation is initiated, if gesture detects another touch then pauses the first primary animation and starts a secondary animation sequence? Once the secondary finishes then the primary is allowed to continue?
Thank you very much.
Exactly what I needed.