diff options
Diffstat (limited to 'platform/ios/MGLUserLocationAnnotationView.m')
-rw-r--r-- | platform/ios/MGLUserLocationAnnotationView.m | 332 |
1 files changed, 230 insertions, 102 deletions
diff --git a/platform/ios/MGLUserLocationAnnotationView.m b/platform/ios/MGLUserLocationAnnotationView.m index 67f360f68a..a6310a82c3 100644 --- a/platform/ios/MGLUserLocationAnnotationView.m +++ b/platform/ios/MGLUserLocationAnnotationView.m @@ -5,7 +5,8 @@ #import "MGLAnnotation.h" #import "MGLMapView.h" -const CGFloat MGLTrackingDotRingWidth = 24.0; +const CGFloat MGLUserLocationAnnotationDotSize = 22.0; +const CGFloat MGLUserLocationAnnotationHaloSize = 115.0; @interface MGLUserLocationAnnotationView () @@ -17,9 +18,15 @@ const CGFloat MGLTrackingDotRingWidth = 24.0; @implementation MGLUserLocationAnnotationView { + CALayer *_headingIndicatorLayer; + CAShapeLayer *_headingIndicatorMaskLayer; CALayer *_accuracyRingLayer; CALayer *_dotBorderLayer; CALayer *_dotLayer; + + double _oldHeadingAccuracy; + CLLocationAccuracy _oldHorizontalAccuracy; + double _oldZoom; } - (instancetype)initWithFrame:(CGRect)frame @@ -30,7 +37,7 @@ const CGFloat MGLTrackingDotRingWidth = 24.0; - (instancetype)initInMapView:(MGLMapView *)mapView { - if (self = [super initWithFrame:CGRectMake(0, 0, MGLTrackingDotRingWidth, MGLTrackingDotRingWidth)]) + if (self = [super initWithFrame:CGRectMake(0, 0, MGLUserLocationAnnotationDotSize, MGLUserLocationAnnotationDotSize)]) { self.annotation = [[MGLUserLocation alloc] initWithMapView:mapView]; _mapView = mapView; @@ -48,164 +55,285 @@ const CGFloat MGLTrackingDotRingWidth = 24.0; - (void)setTintColor:(UIColor *)tintColor { - UIImage *trackingDotHaloImage = [self trackingDotHaloImage]; - _haloLayer.bounds = CGRectMake(0, 0, trackingDotHaloImage.size.width, trackingDotHaloImage.size.height); - _haloLayer.contents = (__bridge id)[trackingDotHaloImage CGImage]; + if (_accuracyRingLayer) + { + _accuracyRingLayer.backgroundColor = [tintColor CGColor]; + } + + _haloLayer.backgroundColor = [tintColor CGColor]; + _dotLayer.backgroundColor = [tintColor CGColor]; - UIImage *dotImage = [self dotImage]; - _dotLayer.bounds = CGRectMake(0, 0, dotImage.size.width, dotImage.size.height); - _dotLayer.contents = (__bridge id)[dotImage CGImage]; + _headingIndicatorLayer.contents = (__bridge id)[[self headingIndicatorTintedGradientImage] CGImage]; } - (void)setupLayers { if (CLLocationCoordinate2DIsValid(self.annotation.coordinate)) { - if ( ! _accuracyRingLayer && self.annotation.location.horizontalAccuracy) + // update heading indicator + // + if (_headingIndicatorLayer) { - UIImage *accuracyRingImage = [self accuracyRingImage]; - _accuracyRingLayer = [CALayer layer]; - _haloLayer.bounds = CGRectMake(0, 0, accuracyRingImage.size.width, accuracyRingImage.size.height); - _haloLayer.contents = (__bridge id)[accuracyRingImage CGImage]; - _haloLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0); + _headingIndicatorLayer.hidden = (_mapView.userTrackingMode == MGLUserTrackingModeFollowWithHeading) ? NO : YES; - [self.layer addSublayer:_accuracyRingLayer]; + if (_oldHeadingAccuracy != self.annotation.heading.headingAccuracy) + { + // recalculate the clipping mask based on updated accuracy + _headingIndicatorMaskLayer.path = [[self headingIndicatorClippingMask] CGPath]; + + _oldHeadingAccuracy = self.annotation.heading.headingAccuracy; + } } - if ( ! _haloLayer) + // heading indicator (tinted, semi-circle) + // + if ( ! _headingIndicatorLayer && self.annotation.heading.headingAccuracy) { - UIImage *haloImage = [self trackingDotHaloImage]; - _haloLayer = [CALayer layer]; - _haloLayer.bounds = CGRectMake(0, 0, haloImage.size.width, haloImage.size.height); - _haloLayer.contents = (__bridge id)[haloImage CGImage]; - _haloLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0); + CGFloat headingIndicatorSize = MGLUserLocationAnnotationHaloSize; - [CATransaction begin]; + _headingIndicatorLayer = [CALayer layer]; + _headingIndicatorLayer.bounds = CGRectMake(0, 0, headingIndicatorSize, headingIndicatorSize); + _headingIndicatorLayer.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0); + _headingIndicatorLayer.contents = (__bridge id)[[self headingIndicatorTintedGradientImage] CGImage]; + _headingIndicatorLayer.contentsGravity = kCAGravityBottom; + _headingIndicatorLayer.contentsScale = [UIScreen mainScreen].scale; + _headingIndicatorLayer.opacity = 0.4; + _headingIndicatorLayer.shouldRasterize = YES; + _headingIndicatorLayer.rasterizationScale = [UIScreen mainScreen].scale; + _headingIndicatorLayer.drawsAsynchronously = YES; - [CATransaction setAnimationDuration:3.5]; - [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]]; - - // scale out radially - // - CABasicAnimation *boundsAnimation = [CABasicAnimation animationWithKeyPath:@"transform"]; - boundsAnimation.repeatCount = MAXFLOAT; - boundsAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)]; - boundsAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2.0, 2.0, 1.0)]; - boundsAnimation.removedOnCompletion = NO; + [self.layer insertSublayer:_headingIndicatorLayer below:_dotBorderLayer]; + } + + // heading indicator accuracy mask (fan-shaped) + // + if ( ! _headingIndicatorMaskLayer && self.annotation.heading.headingAccuracy) + { + _headingIndicatorMaskLayer = [CAShapeLayer layer]; + _headingIndicatorMaskLayer.frame = _headingIndicatorLayer.bounds; + _headingIndicatorMaskLayer.path = [[self headingIndicatorClippingMask] CGPath]; + + // apply the mask to the halo-radius-sized gradient layer + _headingIndicatorLayer.mask = _headingIndicatorMaskLayer; - [_haloLayer addAnimation:boundsAnimation forKey:@"animateScale"]; + _oldHeadingAccuracy = self.annotation.heading.headingAccuracy; + } + + // update accuracy ring + // + if (_accuracyRingLayer) + { + // FIX: This stops EVERYTHING... and that isn't necessarily necessary, now is it? + if (_oldZoom == self.mapView.zoomLevel && _oldHorizontalAccuracy == self.annotation.location.horizontalAccuracy) + { + return; + } - // go transparent as scaled out - // - CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; - opacityAnimation.repeatCount = MAXFLOAT; - opacityAnimation.fromValue = [NSNumber numberWithFloat:1.0]; - opacityAnimation.toValue = [NSNumber numberWithFloat:-1.0]; - opacityAnimation.removedOnCompletion = NO; + CGFloat accuracyRingSize = [self calculateAccuracyRingSize]; - [_haloLayer addAnimation:opacityAnimation forKey:@"animateOpacity"]; + // only show the accuracy ring if it won't be obscured by the location dot + if (accuracyRingSize > MGLUserLocationAnnotationDotSize + 15) + { + _accuracyRingLayer.hidden = NO; + _accuracyRingLayer.bounds = CGRectMake(0, 0, accuracyRingSize, accuracyRingSize); + _accuracyRingLayer.cornerRadius = accuracyRingSize / 2; + + // match the halo to the accuracy ring + _haloLayer.bounds = _accuracyRingLayer.bounds; + _haloLayer.cornerRadius = _accuracyRingLayer.cornerRadius; + _haloLayer.shouldRasterize = NO; + } + else + { + _accuracyRingLayer.hidden = YES; + + _haloLayer.bounds = CGRectMake(0, 0, MGLUserLocationAnnotationHaloSize, MGLUserLocationAnnotationHaloSize); + _haloLayer.cornerRadius = MGLUserLocationAnnotationHaloSize / 2.0; + _haloLayer.shouldRasterize = YES; + _haloLayer.rasterizationScale = [UIScreen mainScreen].scale; + } - [CATransaction commit]; + // store accuracy and zoom so we're not redrawing unchanged location updates + _oldHorizontalAccuracy = self.annotation.location.horizontalAccuracy; + _oldZoom = self.mapView.zoomLevel; + } + + // accuracy ring (circular, tinted, mostly-transparent) + // + if ( ! _accuracyRingLayer && self.annotation.location.horizontalAccuracy) + { + CGFloat accuracyRingSize = [self calculateAccuracyRingSize]; + _accuracyRingLayer = [self circleLayerWithSize:accuracyRingSize]; + _accuracyRingLayer.backgroundColor = [_mapView.tintColor CGColor]; + _accuracyRingLayer.opacity = 0.1; + _accuracyRingLayer.shouldRasterize = NO; + _accuracyRingLayer.allowsGroupOpacity = NO; - [self.layer addSublayer:_haloLayer]; + [self.layer addSublayer:_accuracyRingLayer]; } - // white dot background with shadow + // expanding sonar-like pulse (circular, tinted, fades out) // - if ( ! _dotBorderLayer) + if ( ! _haloLayer) { - CGRect rect = CGRectMake(0, 0, MGLTrackingDotRingWidth * 1.5, MGLTrackingDotRingWidth * 1.5); + _haloLayer = [self circleLayerWithSize:MGLUserLocationAnnotationHaloSize]; + _haloLayer.backgroundColor = [_mapView.tintColor CGColor]; + _haloLayer.allowsGroupOpacity = NO; - UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]); - CGContextRef context = UIGraphicsGetCurrentContext(); + // set defaults for the animations + CAAnimationGroup *animationGroup = [self loopingAnimationGroupWithDuration:3.0]; - CGContextSetShadow(context, CGSizeMake(0, 0), MGLTrackingDotRingWidth / 4.0); + // scale out radially with initial acceleration + CAKeyframeAnimation *boundsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.xy"]; + boundsAnimation.values = @[@0, @0.35, @1]; + boundsAnimation.keyTimes = @[@0, @0.2, @1]; - CGContextSetFillColorWithColor(context, [[UIColor whiteColor] CGColor]); - CGContextFillEllipseInRect(context, CGRectMake((rect.size.width - MGLTrackingDotRingWidth) / 2.0, (rect.size.height - MGLTrackingDotRingWidth) / 2.0, MGLTrackingDotRingWidth, MGLTrackingDotRingWidth)); + // go transparent as scaled out, start semi-opaque + CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"]; + opacityAnimation.values = @[@0.4, @0.4, @0]; + opacityAnimation.keyTimes = @[@0, @0.2, @1]; - UIImage *whiteBackground = UIGraphicsGetImageFromCurrentImageContext(); + animationGroup.animations = @[boundsAnimation, opacityAnimation]; - UIGraphicsEndImageContext(); + [_haloLayer addAnimation:animationGroup forKey:@"animateTransformAndOpacity"]; + + [self.layer addSublayer:_haloLayer]; + } + + // background dot (white with black shadow) + // + if ( ! _dotBorderLayer) + { + _dotBorderLayer = [self circleLayerWithSize:MGLUserLocationAnnotationDotSize]; + _dotBorderLayer.backgroundColor = [[UIColor whiteColor] CGColor]; + _dotBorderLayer.shadowColor = [[UIColor blackColor] CGColor]; + _dotBorderLayer.shadowOffset = CGSizeMake(0, 0); + _dotBorderLayer.shadowRadius = 3; + _dotBorderLayer.shadowOpacity = 0.25; - _dotBorderLayer = [CALayer layer]; - _dotBorderLayer.bounds = CGRectMake(0, 0, whiteBackground.size.width, whiteBackground.size.height); - _dotBorderLayer.contents = (__bridge id)[whiteBackground CGImage]; - _dotBorderLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0); [self.layer addSublayer:_dotBorderLayer]; } - - // pulsing, tinted dot sublayer + + // inner dot (pulsing, tinted) // if ( ! _dotLayer) { - UIImage *dotImage = [self dotImage]; - _dotLayer = [CALayer layer]; - _dotLayer.bounds = CGRectMake(0, 0, dotImage.size.width, dotImage.size.height); - _dotLayer.contents = (__bridge id)[dotImage CGImage]; - _dotLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0); - - CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"]; - animation.repeatCount = MAXFLOAT; - animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 1.0)]; - animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.8, 0.8, 1.0)]; - animation.removedOnCompletion = NO; - animation.autoreverses = YES; - animation.duration = 1.5; - animation.beginTime = CACurrentMediaTime() + 1.0; - animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; - - [_dotLayer addAnimation:animation forKey:@"animateTransform"]; + _dotLayer = [self circleLayerWithSize:MGLUserLocationAnnotationDotSize * 0.75]; + _dotLayer.backgroundColor = [_mapView.tintColor CGColor]; + _dotLayer.shouldRasterize = NO; + + // set defaults for the animations + CAAnimationGroup *animationGroup = [self loopingAnimationGroupWithDuration:1.5]; + animationGroup.autoreverses = YES; + animationGroup.fillMode = kCAFillModeBoth; + + // scale the dot up and down + CABasicAnimation *pulseAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale.xy"]; + pulseAnimation.fromValue = @0.8; + pulseAnimation.toValue = @1; + + // fade opacity in and out, subtly + CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; + opacityAnimation.fromValue = @0.8; + opacityAnimation.toValue = @1; + + animationGroup.animations = @[pulseAnimation, opacityAnimation]; + + [_dotLayer addAnimation:animationGroup forKey:@"animateTransformAndOpacity"]; [self.layer addSublayer:_dotLayer]; } } } -- (UIImage *)accuracyRingImage +- (CALayer *)circleLayerWithSize:(CGFloat)layerSize { - CGFloat latRadians = self.annotation.coordinate.latitude * M_PI / 180.0f; - CGFloat pixelRadius = self.annotation.location.horizontalAccuracy / cos(latRadians) / [self.mapView metersPerPixelAtLatitude:self.annotation.coordinate.latitude]; - UIGraphicsBeginImageContextWithOptions(CGSizeMake(pixelRadius * 2, pixelRadius * 2), NO, [[UIScreen mainScreen] scale]); + CALayer *circleLayer = [CALayer layer]; + circleLayer.bounds = CGRectMake(0, 0, layerSize, layerSize); + circleLayer.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0); + circleLayer.cornerRadius = layerSize / 2.0; + circleLayer.shouldRasterize = YES; + circleLayer.rasterizationScale = [UIScreen mainScreen].scale; + circleLayer.drawsAsynchronously = YES; - CGContextSetStrokeColorWithColor(UIGraphicsGetCurrentContext(), [[UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.7] CGColor]); - CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [[UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.15] CGColor]); - CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 2.0); - CGContextStrokeEllipseInRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, pixelRadius * 2, pixelRadius * 2)); + return circleLayer; +} + +- (CAAnimationGroup *)loopingAnimationGroupWithDuration:(CGFloat)animationDuration +{ + CAAnimationGroup *animationGroup = [CAAnimationGroup animation]; + animationGroup.duration = animationDuration; + animationGroup.repeatCount = INFINITY; + animationGroup.removedOnCompletion = NO; + animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]; - UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return finalImage; + return animationGroup; } -- (UIImage *)trackingDotHaloImage +- (CGFloat)calculateAccuracyRingSize { - UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 100), NO, [[UIScreen mainScreen] scale]); - CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [[_mapView.tintColor colorWithAlphaComponent:0.75] CGColor]); - CGContextFillEllipseInRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, 100, 100)); - UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + CGFloat latRadians = self.annotation.coordinate.latitude * M_PI / 180.0f; + CGFloat pixelRadius = self.annotation.location.horizontalAccuracy / cos(latRadians) / [self.mapView metersPerPixelAtLatitude:self.annotation.coordinate.latitude]; - return finalImage; + return pixelRadius * 2; } -- (UIImage *)dotImage +- (UIImage *)headingIndicatorTintedGradientImage { - CGFloat tintedWidth = MGLTrackingDotRingWidth * 0.7; + UIImage *image; - CGRect rect = CGRectMake(0, 0, tintedWidth, tintedWidth); + CGFloat haloRadius = MGLUserLocationAnnotationHaloSize / 2.0; - UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]); + UIGraphicsBeginImageContextWithOptions(CGSizeMake(MGLUserLocationAnnotationHaloSize, haloRadius), NO, 0); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextSetFillColorWithColor(context, [_mapView.tintColor CGColor]); - CGContextFillEllipseInRect(context, CGRectMake((rect.size.width - tintedWidth) / 2.0, (rect.size.height - tintedWidth) / 2.0, tintedWidth, tintedWidth)); + // gradient from the tint color to no-alpha tint color + CGFloat gradientLocations[] = {0.0, 1.0}; + CGGradientRef gradient = CGGradientCreateWithColors( + colorSpace, (__bridge CFArrayRef)@[(id)[_mapView.tintColor CGColor], + (id)[[_mapView.tintColor colorWithAlphaComponent:0] CGColor]], gradientLocations); - UIImage *tintedForeground = UIGraphicsGetImageFromCurrentImageContext(); + // draw the gradient from the center point to the edge (full halo radius) + CGPoint centerPoint = CGPointMake(haloRadius, haloRadius); + CGContextDrawRadialGradient(context, gradient, + centerPoint, 0.0, + centerPoint, haloRadius, + nil); + image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - return tintedForeground; + CGGradientRelease(gradient); + CGColorSpaceRelease(colorSpace); + + return image; +} + +- (UIBezierPath *)headingIndicatorClippingMask +{ + CGFloat accuracy = self.annotation.heading.headingAccuracy; + + // size the mask using exagerated accuracy, but keep within a good display range + CGFloat clippingDegrees = 90 - (accuracy * 1.5); + clippingDegrees = fmin(clippingDegrees, 55); + clippingDegrees = fmax(clippingDegrees, 10); + + CGRect ovalRect = CGRectMake(0, 0, MGLUserLocationAnnotationHaloSize, MGLUserLocationAnnotationHaloSize); + UIBezierPath *ovalPath = UIBezierPath.bezierPath; + + // clip the oval to ± incoming accuracy degrees (converted to radians), from the top + [ovalPath addArcWithCenter:CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)) + radius:CGRectGetWidth(ovalRect) / 2.0 + startAngle:(-180 + clippingDegrees) * M_PI / 180 + endAngle:-clippingDegrees * M_PI / 180 + clockwise:YES]; + + [ovalPath addLineToPoint:CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect))]; + [ovalPath closePath]; + + return ovalPath; } @end |