How can I create a custom "pin-drop" animation using MKAnnotationView?
Asked Answered
V

5

24

I have an instance of MKMapView and would like to use custom annotation icons instead of the standard pin icons supplied by MKPinAnnotationView. So, I've setup a subclass of MKAnnotationView called CustomMapAnnotation and am overriding -(void)drawRect: to draw a CGImage. This works.

The trouble comes when I try to replicate the .animatesDrop functionality supplied by MKPinAnnotationView; I would love for my icons to appear gradually, dropped from above and in left-to-right order, when the annotations are added to the MKMapView instance.

Here is -(void)drawRect: for CustomMapAnnotation, which works when you just draw the UIImage (which is what the 2nd line does):

- (void)drawRect:(CGRect)rect {
 [super drawRect:rect];
 [((Incident *)self.annotation).smallIcon drawInRect:rect];
 if (newAnnotation) {
  [self animateDrop];
  newAnnotation = NO;
 }
} 

The trouble comes when you add the animateDrop method:

-(void)animateDrop {
 CGContextRef myContext = UIGraphicsGetCurrentContext();

 CGPoint finalPos = self.center;
 CGPoint startPos = CGPointMake(self.center.x, self.center.y-480.0);
 self.layer.position = startPos;

 CABasicAnimation *theAnimation;
 theAnimation=[CABasicAnimation animationWithKeyPath:@"position"];
 theAnimation.fromValue=[NSValue valueWithCGPoint:startPos];
 theAnimation.toValue=[NSValue valueWithCGPoint:finalPos];
 theAnimation.removedOnCompletion = NO;
 theAnimation.fillMode = kCAFillModeForwards;
 theAnimation.delegate = self;
 theAnimation.beginTime = 5.0 * (self.center.x/320.0);
 theAnimation.duration = 1.0;
 [self.layer addAnimation:theAnimation forKey:@""];
}

It just doesn't work, and there could be a lot of reasons why. I won't get into all of them now. The main thing I am wanting to know is if the approach is sound at all, or if I should try something entirely different.

I tried also to package up the whole thing into an animation transaction so that the beginTime parameter might actually work; this seemed to not do anything at all. I don't know if this is because I am missing some key point or whether it's because MapKit is trashing my animations somehow.

  // Does nothing
  [CATransaction begin];
  [map addAnnotations:list];
  [CATransaction commit];

If anyone has any experience with animated MKMapAnnotations like this, I'd love some hints, otherwise if you can offer CAAnimation advice on the approach, that'd be great too.

Villar answered 7/12, 2009 at 0:38 Comment(1)
This is not a drop animation, but simply a nice animation https://mcmap.net/q/130843/-ios-mkmapview-animate-map-pinsBernadette
C
58

One problem with the code by Paul Shapiro is that it doesn't deal with when you add annotations below where the user is looking at the moment. Those annotations will float in mid-air before dropping because they are moved into the user's visible map rect.

Another is that it also drops the user location blue dot. With this code below, you handle both user location and large amounts of map annotations off-screen. I've also added a nice bounce ;)

- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views {
    MKAnnotationView *aV; 

    for (aV in views) {

        // Don't pin drop if annotation is user location
        if ([aV.annotation isKindOfClass:[MKUserLocation class]]) {
            continue;
        }

        // Check if current annotation is inside visible map rect, else go to next one
        MKMapPoint point =  MKMapPointForCoordinate(aV.annotation.coordinate);
        if (!MKMapRectContainsPoint(self.mapView.visibleMapRect, point)) {
            continue;
        }

        CGRect endFrame = aV.frame;

        // Move annotation out of view
        aV.frame = CGRectMake(aV.frame.origin.x, aV.frame.origin.y - self.view.frame.size.height, aV.frame.size.width, aV.frame.size.height);

        // Animate drop
        [UIView animateWithDuration:0.5 delay:0.04*[views indexOfObject:aV] options:UIViewAnimationCurveLinear animations:^{

            aV.frame = endFrame;

        // Animate squash
        }completion:^(BOOL finished){
            if (finished) {
                [UIView animateWithDuration:0.05 animations:^{
                    aV.transform = CGAffineTransformMakeScale(1.0, 0.8);

                }completion:^(BOOL finished){
                    [UIView animateWithDuration:0.1 animations:^{
                        aV.transform = CGAffineTransformIdentity;
                    }];
                }];
            }
        }];
    }
}
Capriccio answered 12/8, 2011 at 20:24 Comment(4)
This answer is simply awesome. Do yourself a favor and consider using this piece of code, especially if you are into super cool animations like in the map app.Interdental
Awesome stuff. One improvement: change the CGAffineTransformMakeScale animate squash transform to aV.transform = CGAffineTransformMake(1.0, 0, 0, 0.8, 0, + aV.frame.size.height*0.1); this will make it so the annotation view squashes down into into its bottom edge (before bouncing back), rather than squishing uniformly into the center.Adrienneadrift
@WilliamDenniss thanks for the improvement! It's a very nice touch.Abate
Brilliant, and it provides great motivation to investigate those Affine Transforms. Thank you!Luciferin
L
52

First, you need to make your view controller implement MKMapViewDelegate if it doesn't already.

Then, implement this method:

- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views { 
   MKAnnotationView *aV; 
   for (aV in views) {
     CGRect endFrame = aV.frame;

     aV.frame = CGRectMake(aV.frame.origin.x, aV.frame.origin.y - 230.0, aV.frame.size.width, aV.frame.size.height);

     [UIView beginAnimations:nil context:NULL];
     [UIView setAnimationDuration:0.45];
     [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
         [aV setFrame:endFrame];
     [UIView commitAnimations];

   }
}

Add your annotations to the MapView and when they are added, this delegate method will be called and will animate the pins from top to bottom as they are added.

The values for timing and positioning can be changed a little bit but I've tweaked it to make it look best/closest to the traditional drop (as far as I've tested). Hope this helps!

Lazarus answered 18/1, 2010 at 15:52 Comment(3)
Can u help me in my problem.. I have to show 4 lines of data in that bubble..Like One title..then description, date, location name.. All the example showing only 2 lines of details. i tried like appending wanted data in subtitle and seperated by a \n character.. But it didnt worked.. Plz help meBelldas
Sijo, have a look at this. #1350550Lazarus
id you iterate over a float, you can adjust the start time by doing: [UIView setAnimationDelay:offset] so that you get the sequential animation.Dubonnet
A
12

Alternatively, if you're making a MKAnnotationView subclass, you can use didMoveToSuperview to trigger the animation. The following does a drop that ends in a slight 'squish' effect

  #define kDropCompressAmount 0.1

  @implementation MyAnnotationViewSubclass

  ...

  - (void)didMoveToSuperview {
      CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"];
      animation.duration = 0.4;
      animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
      animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, -400, 0)];
      animation.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];

      CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:@"transform"];
      animation2.duration = 0.10;
      animation2.beginTime = animation.duration;
      animation2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
      animation2.toValue = [NSValue valueWithCATransform3D:CATransform3DScale(CATransform3DMakeTranslation(0, self.layer.frame.size.height*kDropCompressAmount, 0), 1.0, 1.0-kDropCompressAmount, 1.0)];
      animation2.fillMode = kCAFillModeForwards;

      CABasicAnimation *animation3 = [CABasicAnimation animationWithKeyPath:@"transform"];
      animation3.duration = 0.15;
      animation3.beginTime = animation.duration+animation2.duration;
      animation3.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
      animation3.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
      animation3.fillMode = kCAFillModeForwards;

      CAAnimationGroup *group = [CAAnimationGroup animation];
      group.animations = [NSArray arrayWithObjects:animation, animation2, animation3, nil];
      group.duration = animation.duration+animation2.duration+animation3.duration;
      group.fillMode = kCAFillModeForwards;

      [self.layer addAnimation:group forKey:nil];
  }
Acquah answered 26/6, 2010 at 18:56 Comment(0)
M
5

For Michael Tyson's response (I can't comment everywhere yet), I propose an insertion of the following code in didMoveToSuperview for proper reuse of MKAnnotationView so it does the animation again and then imitate the sequencial addition of annotations

Play with the dividers and multipliers for different visual results...

- (void)didMoveToSuperview {
    //necessary so it doesn't add another animation when moved to superview = nil
    //and to remove the previous animations if they were not finished!
    if (!self.superview) {
        [self.layer removeAllAnimations];
        return;
    }


    float xOriginDivider = 20.;
    float pos = 0;

    UIView *mySuperview = self.superview;
    while (mySuperview && ![mySuperview isKindOfClass:[MKMapView class]])
        mySuperview = mySuperview.superview;
    if ([mySuperview isKindOfClass:[MKMapView class]]) 
        //given the position in the array
        // pos = [((MKMapView *) mySuperview).annotations indexOfObject:self.annotation];   
        // left to right sequence;
        pos = [((MKMapView *) mySuperview) convertCoordinate:self.annotation.coordinate toPointToView:mySuperview].x / xOriginDivider;

    float yOffsetMultiplier = 20.;
    float timeOffsetMultiplier = 0.05;


    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"];
    animation.duration = 0.4 + timeOffsetMultiplier * pos;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
    animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, -400 - yOffsetMultiplier * pos, 0)];
    animation.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];

    // rest of animation group...
}
Merilyn answered 2/11, 2010 at 10:8 Comment(1)
for my app, the first block of code here is crucial in iOS 6, but not previous versions. thanks.Hulahula
H
0

I think the better option is to set 'animatesDrop' to YES. It's a property of MKPinAnnotationView.

Hey answered 10/11, 2014 at 0:9 Comment(1)
It will dropped pin animated once on map. it won't animating randomly.Fluorescein

© 2022 - 2024 — McMap. All rights reserved.