Swift中为MKMapView的MKOverlay添加模糊视图的实现方案
解决MKMapView非矩形区域模糊效果的问题
你遇到的核心问题是:普通的MKOverlayRenderer只是在地图图层上方绘制内容,并不会自动抓取下方的地图画面进行模糊处理——所以你之前看到的灰色区域其实是Renderer默认的填充色,或者因为没有正确处理底层内容导致的无效果。下面给你两种可行的解决方案,分别适合不同的场景:
方案一:用UIVisualEffectView实现高效实时模糊(推荐)
这个方案利用系统原生的UIVisualEffectView来做模糊,性能更好,而且是实时跟随地图内容变化的。核心思路是:创建一个独立的模糊视图,根据Overlay的地理坐标实时转换为屏幕坐标,然后裁剪成你需要的非矩形形状,放在地图视图的上层。
步骤1:定义自定义MKOverlay
先创建一个自定义Overlay来存储你的非矩形路径和地理范围:
class NonRectangularOverlay: NSObject, MKOverlay { var coordinate: CLLocationCoordinate2D var boundingMapRect: MKMapRect var path: UIBezierPath // 存储非矩形的形状路径 init(coordinate: CLLocationCoordinate2D, path: UIBezierPath, boundingMapRect: MKMapRect) { self.coordinate = coordinate self.path = path self.boundingMapRect = boundingMapRect super.init() } }
步骤2:在ViewController中配置模糊视图和地图监听
class MapViewController: UIViewController, MKMapViewDelegate { @IBOutlet weak var mapView: MKMapView! private var blurEffectView: UIVisualEffectView! private var customOverlay: NonRectangularOverlay! override func viewDidLoad() { super.viewDidLoad() mapView.delegate = self // 1. 创建你的非矩形区域示例(这里以圆形为例,半径1000米) let centerCoord = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194) let radius: CLLocationDistance = 1000 let mapRect = MKMapRect(center: MKMapPoint(centerCoord), size: MKMapSize(width: radius*2, height: radius*2)) // 把地理坐标转成屏幕坐标,创建初始路径 let centerScreenPoint = mapView.convert(centerCoord, toPointTo: view) let screenRadius = mapView.convert(MKMapPoint(x: radius, y: 0), toPointTo: view).x - centerScreenPoint.x let initialPath = UIBezierPath(arcCenter: centerScreenPoint, radius: screenRadius, startAngle: 0, endAngle: .pi*2, clockwise: true) // 2. 添加自定义Overlay到地图 customOverlay = NonRectangularOverlay(coordinate: centerCoord, path: initialPath, boundingMapRect: mapRect) mapView.addOverlay(customOverlay) // 3. 创建模糊效果视图 let blurEffect = UIBlurEffect(style: .systemMaterial) // 可以切换不同的模糊风格 blurEffectView = UIVisualEffectView(effect: blurEffect) blurEffectView.frame = initialPath.bounds // 裁剪视图为非矩形形状 let maskLayer = CAShapeLayer() maskLayer.path = initialPath.cgPath blurEffectView.layer.mask = maskLayer view.addSubview(blurEffectView) // 4. 监听地图区域变化,实时更新模糊视图的位置和形状 mapView.addObserver(self, forKeyPath: "region", options: [.new], context: nil) } // 更新模糊视图的位置和形状 private func updateBlurViewPosition() { let centerScreenPoint = mapView.convert(customOverlay.coordinate, toPointTo: view) let screenRadius = mapView.convert(MKMapPoint(x: customOverlay.boundingMapRect.size.width/2, y: 0), toPointTo: view).x - centerScreenPoint.x let updatedPath = UIBezierPath(arcCenter: centerScreenPoint, radius: screenRadius, startAngle: 0, endAngle: .pi*2, clockwise: true) blurEffectView.frame = updatedPath.bounds let maskLayer = CAShapeLayer() maskLayer.path = updatedPath.cgPath blurEffectView.layer.mask = maskLayer } // 空实现Renderer,因为我们用独立视图处理模糊 func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { return MKOverlayRenderer(overlay: overlay) } // 监听地图区域变化 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "region" { updateBlurViewPosition() } } deinit { mapView.removeObserver(self, forKeyPath: "region") } }
方案二:自定义MKOverlayRenderer结合CoreImage模糊
如果你希望完全通过MKOverlay的渲染机制来实现,可以在自定义Renderer中抓取地图对应区域的截图,用CoreImage做模糊后再裁剪成非矩形形状绘制。
步骤1:自定义BlurOverlayRenderer
class BlurOverlayRenderer: MKOverlayRenderer { private let blurRadius: CGFloat = 20 // 模糊程度,可调整 private let shapePath: UIBezierPath init(overlay: MKOverlay, shapePath: UIBezierPath) { self.shapePath = shapePath super.init(overlay: overlay) } override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { // 1. 获取当前地图的截图 guard let mapSnapshot = mapView?.snapshotView(afterScreenUpdates: false) else { return } let snapshotImage = mapSnapshot.asImage() // 2. 把地图矩形转换成屏幕坐标的CGRect let targetRect = rect(for: mapRect) // 3. 裁剪出需要模糊的区域 guard let croppedCGImage = snapshotImage.cgImage?.cropping(to: targetRect) else { return } let ciImage = CIImage(cgImage: croppedCGImage) // 4. 应用高斯模糊滤镜 let blurFilter = CIFilter(name: "CIGaussianBlur")! blurFilter.setValue(ciImage, forKey: kCIInputImageKey) blurFilter.setValue(blurRadius, forKey: kCIInputRadiusKey) guard let blurredCI = blurFilter.outputImage else { return } let ciContext = CIContext(options: nil) guard let blurredCGImage = ciContext.createCGImage(blurredCI, from: blurredCI.extent) else { return } let blurredImage = UIImage(cgImage: blurredCGImage) // 5. 绘制模糊后的图片,并裁剪成非矩形形状 context.saveGState() context.addPath(shapePath.cgPath) context.clip() blurredImage.draw(in: targetRect) context.restoreGState() } } // 扩展UIView转UIImage extension UIView { func asImage() -> UIImage { let renderer = UIGraphicsImageRenderer(bounds: bounds) return renderer.image { rendererContext in layer.render(in: rendererContext.cgContext) } } }
步骤2:在ViewController中使用该Renderer
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { guard let customOverlay = overlay as? NonRectangularOverlay else { return MKOverlayRenderer(overlay: overlay) } return BlurOverlayRenderer(overlay: overlay, shapePath: customOverlay.path) }
关键注意事项
- 坐标转换:一定要注意
MKMapView的convert(_:toPointTo:)方法,它能帮你在地理坐标、地图点、屏幕坐标之间正确转换,确保非矩形区域的位置准确。 - 性能差异:方案一的
UIVisualEffectView是系统优化的,实时模糊几乎没有性能损耗;方案二每次地图重绘都要截图和处理模糊,在地图频繁移动时可能会有卡顿,适合静态或低频率交互的场景。 - 非矩形路径扩展:如果是多边形区域,只需要把地理坐标数组转换成屏幕坐标数组,再用
UIBezierPath(points:count:)创建路径即可。
内容的提问来源于stack exchange,提问作者IHaveAQuestion
相关产品推荐
相关产品推荐

