Do you know how to create an animation like the Blue Marble drop User-Location in MKMapView?
-
1Are you saying you'd like to make a similar animation, or you'd like to have the standard location indicator show up in a Map View? – Glenn Howes Aug 22 '11 at 16:41
-
I like to make a similar animation, thanks. – Tuyen Nguyen Aug 23 '11 at 14:31
-
your answer found in post [enter link description here][1] [1]: http://stackoverflow.com/questions/1437568/howto-initialise-mkmapview-with-a-given-user-location – Mina Nabil Sep 24 '11 at 13:46
2 Answers
Although I am not sure on the specifics of how Apple accomplished this effect, this feels to me like a great opportunity to use CoreAnimation and custom animatable properties. This post provides some nice background on the subject. I assume by the "Blue Marble drop" animation you're referring to the following sequence:
- Large light blue circle zooms into frame
- Large light blue circle oscillates between two relatively large radii as location is calculated
- Large light blue circle zooms into small darker blue circle on the user's location
Although this may be simplifying the process slightly, I think it's a good place to start and more complex/detailed functionality can be added with relative ease (i.e. the small dark circle pulsing as larger circle converges on it.)
The first thing we need is a custom CALayer subclass with a custom property for our outer large light blue circles radius:
#import <QuartzCore/QuartzCore.h>
@interface CustomLayer : CALayer
@property (nonatomic, assign) CGFloat circleRadius;
@end
and the implementation:
#import "CustomLayer.h"
@implementation CustomLayer
@dynamic circleRadius; // Linked post tells us to let CA implement our accessors for us.
// Whether this is necessary or not is unclear to me and one
// commenter on the linked post claims success only when using
// @synthesize for the animatable property.
+ (BOOL)needsDisplayForKey:(NSString*)key {
// Let our layer know it has to redraw when circleRadius is changed
if ([key isEqualToString:@"circleRadius"]) {
return YES;
} else {
return [super needsDisplayForKey:key];
}
}
- (void)drawInContext:(CGContextRef)ctx {
// This call is probably unnecessary as super's implementation does nothing
[super drawInContext:ctx];
CGRect rect = CGContextGetClipBoundingBox(ctx);
// Fill the circle with a light blue
CGContextSetRGBFillColor(ctx, 0, 0, 255, 0.1);
// Stoke a dark blue border
CGContextSetRGBStrokeColor(ctx, 0, 0, 255, 0.5);
// Construct a CGMutablePath to draw the light blue circle
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, rect.size.width / 2,
rect.size.height / 2,
self.circleRadius, 0, 2 * M_PI, NO);
// Fill the circle
CGContextAddPath(ctx, path);
CGContextFillPath(ctx);
// Stroke the circle's border
CGContextAddPath(ctx, path);
CGContextStrokePath(ctx);
// Release the path
CGPathRelease(path);
// Set a dark blue color for the small inner circle
CGContextSetRGBFillColor(ctx, 0, 0, 255, 1.0f);
// Draw the center dot
CGContextBeginPath (ctx);
CGContextAddArc(ctx, rect.size.width / 2,
rect.size.height / 2,
5, 0, 2 * M_PI, NO);
CGContextFillPath(ctx);
CGContextStrokePath(ctx);
}
@end
With this infrastructure in place, we can now animate the radius of the outer circle with ease b/c CoreAnimation will take care of the value interpolations as well as redraw calls. All we have to do his add an animation to the layer. As a simple proof of concept, I chose a simple CAKeyframeAnimation
to go through the 3 stage animation:
// In some controller class...
- (void)addLayerAndAnimate {
CustomLayer *customLayer = [[CustomLayer alloc] init];
// Make layer big enough for the initial radius
// EDIT: You may want to shrink the layer when it reacehes it's final size
[customLayer setFrame:CGRectMake(0, 0, 205, 205)];
[self.view.layer addSublayer:customLayer];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"circleRadius"];
// Zoom in, oscillate a couple times, zoom in further
animation.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:100],
[NSNumber numberWithFloat:45],
[NSNumber numberWithFloat:50],
[NSNumber numberWithFloat:45],
[NSNumber numberWithFloat:50],
[NSNumber numberWithFloat:45],
[NSNumber numberWithFloat:20],
nil];
// We want the radii to be 20 in the end
customLayer.circleRadius = 20;
// Rather arbitrary values. I thought the cubic pacing w/ a 2.5 second pacing
// looked decent enough but you'd probably want to play with them to get a more
// accurate imitation of the Maps app. You could also define a keyTimes array for
// a more discrete control of the times per step.
animation.duration = 2.5;
animation.calculationMode = kCAAnimationCubicPaced;
[customLayer addAnimation:animation forKey:nil];
}
The above is a rather "hacky" proof of concept as I am not sure of the specific way in which you intend to use this effect. For example, if you wanted to oscillate the circle until data was ready, the above wouldn't make a lot of sense because it will always oscillate twice.
Some closing notes:
- Again, I am not sure of your intent for this effect. If, for
example, you're adding it to an
MKMapView
, the above may require some tweaking to integrate with MapKit. - The linked post suggests the above method requires the version of CoreAnimation in iOS 3.0+ and OS X 10.6+
- Speaking of the linked post (as I did often), much credit and thanks to Ole Begemann who wrote it and did a wonderful job explaining custom properties in CoreAnimation.
EDIT: Also, for performance reasons, you're probably going to want to make sure the layer is only as big as it needs to be. That is, after your done animating from the larger size, you may want to scale the size down so you're only using/drawing as much room as necessary. A nice way to do this would be just to find a way animate the bounds
(as opposed to circleRadius
) and perform this animation based the size interpolation but I've had some trouble implementing that (perhaps someone could add some insight on that subject).
Hope this helps, Sam
-
the lines and the blue circle in the middle look pixelated, jagged.. anything I can do to fix that, to make is smooth? – Zsolt Jan 03 '13 at 14:23
-
figured it out.. customLayer.contentsScale = [UIScreen mainScreen].scale; – Zsolt Jan 03 '13 at 14:37