Add inverted circle overlay to map view

前端 未结 4 1471
天命终不由人
天命终不由人 2020-12-14 11:31

(Using iOS 5 and Xcode 4.2.)

I\'ve followed the instructions here: http://developer.apple.com/library/ios/#documentation/UserExperience/Conceptual/LocationA

相关标签:
4条回答
  • 2020-12-14 11:58

    I had the same task and here is how I solve it:

    NOTE: this code will only work starting from iOS7

    Add an overlay to the map, somewhere in your view controller:

    MyMapOverlay *overlay = [[MyMapOverlay alloc] initWithCoordinate:coordinate];
    [self.mapView addOverlay:overlay level:MKOverlayLevelAboveLabels];
    

    In the MKMapViewDelegate methods write next:

    - (MKOverlayRenderer *)mapView:(MKMapView *)map rendererForOverlay:(id<MKOverlay>)overlay {
        /// we need to draw overlay on the map in a way when everything except the area in radius of 500 should be grayed
        /// to do that there is special renderer implemented - NearbyMapOverlay
        if ([overlay isKindOfClass:[NearbyMapOverlay class]]) {
            MyMapOverlayRenderer *renderer = [[MyMapOverlayRenderer alloc] initWithOverlay:overlay];
            renderer.fillColor = [UIColor whateverColor];/// specify color which you want to use for gray out everything out of radius
            renderer.diameterInMeters = 1000;/// choose whatever diameter you need
    
            return renderer;
        }
        return nil;
    }
    

    The MyMapOverlay itself should be something like followed:

    @interface MyMapOverlay : NSObject<MKOverlay>
    - (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate;
    @end
    
    @implementation MyMapOverlay
    
    @synthesize coordinate = _coordinate;
    
    - (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate {
        self = [super init];
        if (self) {
            _coordinate = coordinate;
        }
        return self;
    }
    
    - (MKMapRect)boundingMapRect {
        return MKMapRectWorld;
    }
    
    @end
    

    And the MyMapOverlayRenderer:

    @interface MyMapOverlayRenderer : MKOverlayRenderer
    @property (nonatomic, assign) double diameterInMeters;
    @property (nonatomic, copy) UIColor *fillColor;
    @end
    
    @implementation MyMapOverlayRenderer
    
    /// this method is called as a part of rendering the map, and it draws the overlay polygon by polygon
    /// which means that it renders overlay by square pieces
    - (void)drawMapRect:(MKMapRect)mapRect
          zoomScale:(MKZoomScale)zoomScale
          inContext:(CGContextRef)context {
    
        /// main path - whole area
        UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(mapRect.origin.x, mapRect.origin.y, mapRect.size.width, mapRect.size.height)];
    
        /// converting to the 'world' coordinates
        double radiusInMapPoints = self.diameterInMeters * MKMapPointsPerMeterAtLatitude(self.overlay.coordinate.latitude);
        MKMapSize radiusSquared = {radiusInMapPoints, radiusInMapPoints};
        MKMapPoint regionOrigin = MKMapPointForCoordinate(self.overlay.coordinate);
        MKMapRect regionRect = (MKMapRect){regionOrigin, radiusSquared}; //origin is the top-left corner
        regionRect = MKMapRectOffset(regionRect, -radiusInMapPoints/2, -radiusInMapPoints/2);
        // clamp the rect to be within the world
        regionRect = MKMapRectIntersection(regionRect, MKMapRectWorld);
    
        /// next path is used for excluding the area within the specific radius from current user location, so it will not be filled by overlay fill color
        UIBezierPath *excludePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(regionRect.origin.x, regionRect.origin.y, regionRect.size.width, regionRect.size.height) cornerRadius:regionRect.size.width / 2];
        [path appendPath:excludePath];
    
        /// setting overlay fill color
        CGContextSetFillColorWithColor(context, self.fillColor.CGColor);
        /// adding main path. NOTE that exclusionPath was appended to main path, so we should only add 'path'
        CGContextAddPath(context, path.CGPath);
        /// tells the context to fill the path but with regards to even odd rule
        CGContextEOFillPath(context);
    }
    

    As a result you will have exact same view like on the left image that was posted in the question.

    0 讨论(0)
  • 2020-12-14 11:58

    Here a Swift version. Thanks Valerii.

    https://github.com/dariopellegrini/MKInvertedCircle

    0 讨论(0)
  • 2020-12-14 12:01

    The best way to do it, would be to subclass MKMapView and override the drawRect method call super, then paint over the map with the color you want. Then each time the user moves, drawRect should respond by drawing appropriately.

    0 讨论(0)
  • 2020-12-14 12:07

    i tried to use this swift version and it didn't work, so im posting my implementation (tested on iOS 12)

    import Foundation
    import UIKit
    import MapKit
    
    class MKInvertedCircleOverlayRenderer: MKOverlayRenderer {
    
    var fillColor: UIColor = UIColor.red
    var strokeColor: UIColor = UIColor.blue
    var lineWidth: CGFloat = 3
    var circle: MKCircle
    
    init(circle: MKCircle) {
        self.circle = circle
        super.init(overlay: circle)
    }
    
    override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
        let path = UIBezierPath(rect: rect(for: MKMapRectWorld))
    
        let excludePath: UIBezierPath = UIBezierPath(roundedRect: CGRect(x: circle.coordinate.latitude,
                                                                         y: circle.coordinate.longitude,
                                                                         width: circle.boundingMapRect.size.width,
                                                                         height: circle.boundingMapRect.size.height),
                                                     cornerRadius: CGFloat(circle.boundingMapRect.size.width))
    
        context.setFillColor(fillColor.cgColor)
    
        path.append(excludePath)
        context.addPath(path.cgPath)
        context.fillPath(using: .evenOdd)
    
        context.addPath(excludePath.cgPath)
        context.setLineWidth(9 / zoomScale)
        context.setStrokeColor(strokeColor.cgColor)
        context.strokePath()
    
        //line showing circle radius
        let lineBeginPoint = CGPoint(x: excludePath.bounds.midX, y: excludePath.bounds.midY)
        let lineEndPoint = CGPoint(x: excludePath.bounds.maxX, y: excludePath.bounds.midY)
        let linePath: UIBezierPath = UIBezierPath()
        linePath.move(to: lineBeginPoint)
        linePath.addLine(to: lineEndPoint)
    
        context.addPath(linePath.cgPath)
        context.setLineWidth(6/zoomScale)
        context.setStrokeColor(UIColor.black.cgColor)
        context.setLineDash(phase: 1, lengths: [20 / zoomScale, 10 / zoomScale])
        context.strokePath()
    
        // circle at the end of the line above
        let circleSize: CGFloat = 30/zoomScale
        let circleRect = CGRect(origin: CGPoint(x: lineEndPoint.x - (circleSize/2), y: lineEndPoint.y - (circleSize/2)),
                                size: CGSize(width: circleSize, height: circleSize))
    
        let circlePath: UIBezierPath =
            UIBezierPath(roundedRect: circleRect, cornerRadius: circleSize)
    
        context.addPath(circlePath.cgPath)
        context.setFillColor(UIColor.black.cgColor)
        context.fillPath()
    }
    
    0 讨论(0)
提交回复
热议问题