Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iphone - create animation like user location Blue marble drop

Do you know how to create an animation like the Blue Marble drop User-Location in MKMapView?

like image 979
Tuyen Nguyen Avatar asked Jan 19 '23 22:01

Tuyen Nguyen


1 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:

  1. Large light blue circle zooms into frame
  2. Large light blue circle oscillates between two relatively large radii as location is calculated
  3. 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

like image 125
Sam Avatar answered Jan 21 '23 10:01

Sam