92、地图开发中的标注、覆盖层及相关功能实现

地图开发中的标注、覆盖层及相关功能实现

1. 标注与标注视图

标注是地图开发中常用的元素,用于标记特定的地点。除了使用内置的标注弹出框,还可以显示和隐藏自定义视图。标注弹出框可以包含左右辅助视图,分别对应 MKAnnotationView leftCalloutAccessoryView rightCalloutAccessoryView ,它们是 UIView 类型,高度应小于 32 像素。此外,还有一个 detailCalloutAccessoryView 可以替代副标题,例如可以提供一个多行小文本标签。可以像处理任何视图或控件一样响应这些视图的点击事件,地图视图的 tintColor 会影响辅助视图元素,如图像模板和按钮标题。

MKAnnotationView 可以设置为可拖动,只需将其 draggable 属性设置为 true 。如果使用自定义标注类,其 coordinate 属性也必须是可设置的。例如,在自定义标注类 MyBikeAnnotation 中, coordinate 属性被明确声明为读写属性( var ),而 MKAnnotation 协议中的 coordinate 属性是只读的。还可以通过实现标注视图类的 setDragState(_:animated:) 方法来自定义视图在拖动时的外观变化。

2. 覆盖层概述

覆盖层与标注的不同之处在于,它是完全根据地球表面的点来绘制的。因此,标注的大小始终保持不变,而覆盖层的大小与地图视图的缩放级别相关。

覆盖层的实现方式与标注类似。需要提供一个采用 MKOverlay 协议(该协议本身符合 MKAnnotation 协议)的对象,并将其添加到地图视图中。当地图视图的委托方法 mapView(_:rendererFor:) 被调用时,需要提供一个 MKOverlayRenderer 并将覆盖层对象传递给它,覆盖层渲染器会根据需要绘制覆盖层。与标注一样,这种架构意味着覆盖层本身是一个轻量级对象,只有当覆盖层所覆盖的地球部分实际显示在地图视图中时,才会绘制覆盖层。 MKOverlayRenderer 没有重用标识符,它不是一个视图,而是一个绘图引擎,用于在地图视图提供的图形上下文中进行绘制。

一些内置的 MKShape 子类采用了 MKOverlay 协议,如 MKCircle MKPolygon MKPolyline (及其子类 MKGeodesicPolyline )。与之对应的, MKOverlayRenderer 也有内置子类 MKCircleRenderer MKPolygonRenderer MKPolylineRenderer ,用于绘制相应的形状。因此,与标注一样,可以完全基于现有类的功能来创建覆盖层。

3. 绘制多边形覆盖层示例

以下是一个使用 MKPolygonRenderer 绘制三角形覆盖层的示例:

// 创建 MKPolygon 覆盖层
let lat = self.annloc.latitude
let metersPerPoint = MKMetersPerMapPointAtLatitude(lat)
var c = MKMapPoint(self.annloc)
c.x += 150/metersPerPoint
c.y -= 50/metersPerPoint
var p1 = MKMapPoint(x:c.x, y:c.y)
p1.y -= 100/metersPerPoint
var p2 = MKMapPoint(x:c.x, y:c.y)
p2.x += 100/metersPerPoint
var p3 = MKMapPoint(x:c.x, y:c.y)
p3.x += 300/metersPerPoint
p3.y -= 400/metersPerPoint
var points = [p1, p2, p3]
let tri = MKPolygon(points:&points, count:3)
self.map.addOverlay(tri)

// 实现委托方法提供 MKPolygonRenderer
func mapView(_ mapView: MKMapView,
    rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if let overlay = overlay as? MKPolygon {
            let r = MKPolygonRenderer(polygon:overlay)
            r.fillColor = UIColor.red.withAlphaComponent(0.1)
            r.strokeColor = UIColor.red.withAlphaComponent(0.8)
            r.lineWidth = 2
            return r
        }
        return MKOverlayRenderer()
}
4. 自定义覆盖层类

上述示例中的三角形覆盖层比较粗糙,可以使用 CGPath 绘制更好的箭头形状。内置的 MKOverlayRenderer 子类 MKOverlayPathRenderer 可以实现这一功能。为了与前面的示例结构相似,希望在将覆盖层实例添加到地图视图时提供 CGPath ,但没有内置类支持这一点,因此可以使用自定义类 MyPathOverlay 来采用 MKOverlay 协议。

一个最小的覆盖层类如下:

class MyPathOverlay : NSObject, MKOverlay {
    var coordinate : CLLocationCoordinate2D {
        get {
            let pt = MKMapPoint(
                x:self.boundingMapRect.midX,
                y:self.boundingMapRect.midY)
            return pt.coordinate
        }
    }
    var boundingMapRect : MKMapRect
    init(rect:MKMapRect) {
        self.boundingMapRect = rect
        super.init()
    }
}

实际的 MyPathOverlay 类还会有一个 path 属性,它是一个 UIBezierPath ,用于保存 CGPath 并将其提供给 MKOverlayPathRenderer 。就像标注的 coordinate 属性告诉地图视图标注在地球上的绘制位置一样,覆盖层的 boundingMapRect 属性告诉地图视图覆盖层在地球上的绘制位置。只要 boundingMapRect 的任何部分显示在地图视图的边界内,地图视图就需要处理覆盖层的绘制。对于 MKPolygon ,我们以地球坐标提供多边形的点, boundingMapRect 会自动计算。而对于自定义覆盖层类,我们必须自己提供或计算它。

虽然 boundingMapRect MKMapRect 类型,而 CGPath 是由 CGPoint 定义的,但这些单位是可以互换的。 CGPath CGPoint 会直接转换为相同比例的 MKMapPoint ,即任意两个 CGPoint 之间的距离与对应的两个 MKMapPoint 之间的距离相同。不过,它们的原点不同, CGPath 必须相对于 boundingMapRect 的左上角来描述。

为了简化操作,以 75 米为单位进行思考。以下是创建自定义箭头覆盖层的代码:

// 开始位置并确定绘制单位
let lat = self.annloc.latitude
let metersPerPoint = MKMetersPerMapPointAtLatitude(lat)
let c = MKMapPoint(self.annloc)
let unit = CGFloat(75.0/metersPerPoint)
// 确定覆盖层在地球上的大小和位置
let sz = CGSize(4*unit, 4*unit)
let mr = MKMapRect(
    x:c.x + 2*Double(unit), y:c.y - 4.5*Double(unit),
    width:Double(sz.width), height:Double(sz.height))
// 描述箭头的 CGPath
let p = CGMutablePath()
let start = CGPoint(0, unit*1.5)
let p1 = CGPoint(start.x+2*unit, start.y)
let p2 = CGPoint(p1.x, p1.y-unit)
let p3 = CGPoint(p2.x+unit*2, p2.y+unit*1.5)
let p4 = CGPoint(p2.x, p2.y+unit*3)
let p5 = CGPoint(p4.x, p4.y-unit)
let p6 = CGPoint(p5.x-2*unit, p5.y)
let points = [start, p1, p2, p3, p4, p5, p6]
// 围绕中心旋转箭头
let t1 = CGAffineTransform(translationX: unit*2, y: unit*2)
let t2 = t1.rotated(by:-.pi/3.5)
let t3 = t2.translatedBy(x: -unit*2, y: -unit*2)
p.addLines(between: points, transform: t3)
p.closeSubpath()
// 创建覆盖层并赋予路径
let over = MyPathOverlay(rect:mr)
over.path = UIBezierPath(cgPath:p)
// 将覆盖层添加到地图
self.map.addOverlay(over)

// 委托方法提供 MKOverlayPathRenderer
func mapView(_ mapView: MKMapView,
    rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if let overlay = overlay as? MyPathOverlay {
            let r = MKOverlayPathRenderer(overlay:overlay)
            r.path = overlay.path.cgPath
            r.fillColor = UIColor.red.withAlphaComponent(0.2)
            r.strokeColor = .black
            r.lineWidth = 2
            return r
        }
        return MKOverlayRenderer()
}
5. 自定义覆盖层渲染器

为了实现更通用的功能,可以定义自己的 MKOverlayRenderer 子类,该子类必须重写并实现 draw(_:zoomScale:in:) 方法。第一个参数是描述可见地图图块的 MKMapRect (不是覆盖层的大小和位置),第三个参数是要绘制的 CGContext 。该方法的实现可能会在不同的后台线程上同时被调用多次,每个图块一次,因此要确保绘制过程是线程安全的。覆盖层本身可以通过继承的 overlay 属性访问, MKOverlayRenderer 实例方法(如 rect(for:) )用于在地图的 MKMapRect 坐标和覆盖层渲染器的图形上下文坐标之间进行转换。图形上下文在传入时已经配置好,使得绘制操作会被裁剪到当前图块。

以下是将箭头绘制功能移到自定义 MKOverlayRenderer 子类 MyPathOverlayRenderer 中的示例:

var angle : CGFloat = 0
init(overlay:MKOverlay, angle:CGFloat) {
    self.angle = angle
    super.init(overlay:overlay)
}
override func draw(_ mapRect: MKMapRect,
    zoomScale: MKZoomScale, in con: CGContext) {
        con.setStrokeColor(UIColor.black.cgColor)
        con.setFillColor(UIColor.red.withAlphaComponent(0.2).cgColor)
        con.setLineWidth(1.2/zoomScale)
        let unit =
            CGFloat(self.overlay.boundingMapRect.width/4.0)
        let p = CGMutablePath()
        let start = CGPoint(0, unit*1.5)
        let p1 = CGPoint(start.x+2*unit, start.y)
        let p2 = CGPoint(p1.x, p1.y-unit)
        let p3 = CGPoint(p2.x+unit*2, p2.y+unit*1.5)
        let p4 = CGPoint(p2.x, p2.y+unit*3)
        let p5 = CGPoint(p4.x, p4.y-unit)
        let p6 = CGPoint(p5.x-2*unit, p5.y)
        let points = [start, p1, p2, p3, p4, p5, p6]
        let t1 = CGAffineTransform(translationX: unit*2, y: unit*2)
        let t2 = t1.rotated(by:self.angle)
        let t3 = t2.translatedBy(x: -unit*2, y: -unit*2)
        p.addLines(between: points, transform: t3)
        p.closeSubpath()
        con.addPath(p)
        con.drawPath(using: .fillStroke)
}

添加覆盖层到地图的代码如下:

let lat = self.annloc.latitude
let metersPerPoint = MKMetersPerMapPointAtLatitude(lat)
let c = MKMapPoint(self.annloc)
let unit = 75.0/metersPerPoint
// 确定覆盖层在地球上的大小和位置
let sz = CGSize(4*CGFloat(unit), 4*CGFloat(unit))
let mr = MKMapRect(
    x:c.x + 2*unit, y:c.y - 4.5*unit,
    width:Double(sz.width), height:Double(sz.height))
let over = MyPathOverlay(rect:mr)
self.map.addOverlay(over, level:.aboveRoads)

// 委托方法提供覆盖层渲染器
func mapView(_ mapView: MKMapView,
    rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if overlay is MyPathOverlay {
            let r = MyPathOverlayRenderer(overlay:overlay, angle: -.pi/3.5)
            return r
        }
        return MKOverlayRenderer()
}
6. 其他覆盖层特性

自定义覆盖层类 MyPathOverlay 采用 MKOverlay 协议,通过 getter 方法实现 coordinate 属性,返回 boundingMapRect 的中心。这虽然简单,但却是一个很好的最小实现。该属性的目的是指定添加描述覆盖层的标注的位置。例如:

// ... 像之前一样创建覆盖层并分配路径 ...
self.map.addOverlay(over, level:.aboveRoads)
let annot = MKPointAnnotation()
annot.coordinate = over.coordinate
annot.title = "This way!"
self.map.addAnnotation(annot)

MKOverlay 协议还允许提供 intersects(_:) 方法的实现,以细化覆盖层与自身相交的定义。默认情况下,使用 boundingMapRect 来判断,但如果覆盖层绘制的是非矩形形状,可能希望使用其实际形状作为判断相交的基础。

覆盖层由地图视图以数组形式维护,并从数组开头开始从后向前绘制。 MKMapView 提供了丰富的功能来添加和移除覆盖层,并管理它们的分层顺序。在将覆盖层添加到地图时,可以指定它在地图视图子层中的绘制位置,这就是添加和插入覆盖层的方法有 level: 参数的原因。覆盖层的层级有:
- .aboveRoads (在道路之上,标签之下)
- .aboveLabels

MKTileOverlay 类采用 MKOverlay 协议,允许叠加甚至替代地图视图的地图绘制。可以提供一组不同大小的图块以匹配不同的缩放级别,地图视图会根据当前区域和缩放程度获取并绘制所需的图块。例如,可以将自己的地形图集成到 MKMapView 的显示中。由于绘制任何大小的区域都需要大量图块,因此 MKTileOverlay 使用 URL 进行初始化,该 URL 可以是用于从互联网获取图块的远程 URL。

7. Map Kit 与当前位置

设备可能配备了可以报告当前位置的传感器,Map Kit 提供了与这些功能的简单集成。但要注意,用户可以关闭这些传感器或拒绝应用访问它们(在设置应用的隐私 -> 位置服务中),因此尝试使用这些功能可能会失败。此外,确定设备的位置可能需要一些时间。

实际的工作由 CLLocationManager 实例完成,需要创建并保留该实例,通常是通过将新的 CLLocationManager 实例分配给视图控制器的实例属性来初始化:

let locman = CLLocationManager()

还必须获得用户授权,并且 Info.plist 文件必须说明请求授权的原因:

self.locman.requestWhenInUseAuthorization()

然后,只需将 MKMapView showsUserLocation 属性设置为 true ,就可以让地图视图显示设备的当前位置,地图会自动在该位置添加一个标注。这个标注是 MKUserLocation 类型,采用 MKAnnotation 协议。地图视图的 userLocation 属性也会指向这个标注。如果地图视图委托的 mapView(_:viewFor:) 方法为该标注返回 nil ,或者没有实现该方法,将使用默认的用户位置标注视图,也可以替换为自己的标注视图。

MKUserLocation 有一个 location 属性,是 CLLocation 类型,其 coordinate CLLocationCoordinate2D 。如果地图视图的 showsUserLocation true 且地图视图已经确定了用户的位置,该坐标就描述了该位置。它还有 title subtitle 属性,当标注视图被选中时会显示在弹出框中,还可以检查它当前是否正在更新。

MKMapViewDelegate 方法会通知地图定位用户的尝试情况:
- mapViewWillStartLocatingUser(_:)
- mapViewDidStopLocatingUser(_:)
- mapView(_:didUpdate:) (提供新的 MKUserLocation
- mapView(_:didFailToLocateUserWithError:)

以下是一个使用 mapView(_:viewFor:) 方法替换用户位置标注标题的示例:

func mapView(_ mapView: MKMapView,
    viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        if let annotation = annotation as? MKUserLocation {
            annotation.title = "You are here, stupid!"
            return nil // 也可以替换为自己的 MKAnnotationView
        }
        return nil
}

可以询问地图视图用户的位置(如果已知)是否在地图的可见区域内( isUserLocationVisible )。如果不在,可以通过设置地图的 region 属性来显示用户所在的世界部分。最简单的方法是利用 MKMapView userTrackingMode 属性,该属性决定了地图显示应该如何自动跟踪用户的实际位置,选项有:
- .none :如果 showsUserLocation true ,地图会在用户位置添加一个标注,但仅此而已,地图的区域不会改变。可以在 mapView(_:didUpdate:) 方法中手动设置。
- .follow :设置此模式会将 showsUserLocation 设置为 true 。地图会自动将用户位置居中,并进行适当的缩放。当地图处于此模式时,不应设置地图的区域,否则会与跟踪模式的操作冲突。
- .followWithHeading :与 .follow 类似,但地图还会旋转,使用户面对的方向朝上。在这种情况下, userLocation 标注还有一个 heading 属性,是 CLHeading 类型。

以下代码足以开始显示用户的位置:

self.map.userTrackingMode = .follow

userTrackingMode .follow 模式之一时,如果用户可以自由缩放和滚动地图, userTrackingMode 可能会自动变回 .none (用户位置标注可能会被移除)。可能需要提供一种方式让用户重新开启跟踪,或者在三种跟踪模式之间切换。

一种方法是使用 MKUserTrackingBarButtonItem ,它是 UIBarButtonItem 的子类。用地图视图初始化 MKUserTrackingBarButtonItem 后,其行为会自动处理:当用户点击它时,会将地图视图切换到下一个跟踪模式,其图标会反映当前的跟踪模式。地图视图委托方法 mapView(_:didChange:animated:) 会通知 MKUserTrackingMode 的变化。

另一种选择是使用 MKUserTrackingButton ,与 MKScaleView MKCompassButton 类似,它的优点是可以在任何地方使用(不仅限于工具栏或导航栏)。

8. 与地图应用通信

应用可以与地图应用进行通信。例如,不在自己的应用的地图视图中显示兴趣点,而是请求地图应用显示它,用户可以对该位置进行书签或分享。应用与地图应用之间的通信渠道是 MKMapItem 类。

以下是请求地图应用显示之前示例中标注的同一点的代码:

let p = MKPlacemark(coordinate:self.annloc, addressDictionary:nil)
let mi = MKMapItem(placemark: p)
mi.name = "A Great Place to Dirt Bike" // 地图应用中显示的标签
mi.openInMaps(launchOptions:[
    MKLaunchOptionsMapTypeKey: MKMapType.standard.rawValue,
    MKLaunchOptionsMapCenterKey: self.map.region.center,
    MKLaunchOptionsMapSpanKey: self.map.region.span
])

综上所述,地图开发中的标注、覆盖层以及与当前位置和地图应用的交互提供了丰富的功能,可以满足各种地图相关应用的需求。通过合理运用这些技术,可以创建出功能强大、用户体验良好的地图应用。

地图开发中的标注、覆盖层及相关功能实现

9. 关键技术点总结

为了更清晰地理解和掌握上述地图开发中的各项技术,下面对关键技术点进行总结:

技术点 描述 代码示例
标注视图可拖动设置 MKAnnotationView draggable 属性设置为 true 实现可拖动,自定义标注类需确保 coordinate 属性可设置 swift<br>let annotationView = MKAnnotationView()<br>annotationView.draggable = true<br>
覆盖层绘制 提供采用 MKOverlay 协议的对象,实现 mapView(_:rendererFor:) 方法提供 MKOverlayRenderer swift<br>func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {<br> if let overlay = overlay as? MKPolygon {<br> let r = MKPolygonRenderer(polygon:overlay)<br> r.fillColor = UIColor.red.withAlphaComponent(0.1)<br> r.strokeColor = UIColor.red.withAlphaComponent(0.8)<br> r.lineWidth = 2<br> return r<br> }<br> return MKOverlayRenderer()<br>}<br>
自定义覆盖层类 创建自定义类采用 MKOverlay 协议,实现 coordinate boundingMapRect 属性 swift<br>class MyPathOverlay : NSObject, MKOverlay {<br> var coordinate : CLLocationCoordinate2D {<br> get {<br> let pt = MKMapPoint(<br> x:self.boundingMapRect.midX,<br> y:self.boundingMapRect.midY)<br> return pt.coordinate<br> }<br> }<br> var boundingMapRect : MKMapRect<br> init(rect:MKMapRect) {<br> self.boundingMapRect = rect<br> super.init()<br> }<br>}<br>
自定义覆盖层渲染器 定义 MKOverlayRenderer 子类,重写 draw(_:zoomScale:in:) 方法 swift<br>class MyPathOverlayRenderer: MKOverlayRenderer {<br> var angle : CGFloat = 0<br> init(overlay:MKOverlay, angle:CGFloat) {<br> self.angle = angle<br> super.init(overlay:overlay)<br> }<br> override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in con: CGContext) {<br> // 绘制代码<br> }<br>}<br>
当前位置显示 创建 CLLocationManager 实例,获取用户授权,设置 MKMapView showsUserLocation 属性为 true swift<br>let locman = CLLocationManager()<br>self.locman.requestWhenInUseAuthorization()<br>self.map.showsUserLocation = true<br>
与地图应用通信 使用 MKMapItem 类请求地图应用显示兴趣点 swift<br>let p = MKPlacemark(coordinate:self.annloc, addressDictionary:nil)<br>let mi = MKMapItem(placemark: p)<br>mi.name = "A Great Place to Dirt Bike"<br>mi.openInMaps(launchOptions:[<br> MKLaunchOptionsMapTypeKey: MKMapType.standard.rawValue,<br> MKLaunchOptionsMapCenterKey: self.map.region.center,<br> MKLaunchOptionsMapSpanKey: self.map.region.span<br>])<br>
10. 开发流程梳理

下面通过 mermaid 流程图梳理地图开发中从添加标注、覆盖层到显示当前位置以及与地图应用通信的整体流程:

graph LR
    A[开始] --> B[添加标注]
    B --> C[设置标注视图属性]
    C --> D[添加覆盖层]
    D --> E[实现覆盖层渲染器]
    E --> F[自定义覆盖层类和渲染器]
    F --> G[显示当前位置]
    G --> H[获取用户授权]
    H --> I[设置地图跟踪模式]
    I --> J[与地图应用通信]
    J --> K[结束]
11. 注意事项与优化建议

在地图开发过程中,有一些注意事项和优化建议可以提高开发效率和应用性能:
- 注意事项
- 用户授权 :在使用设备的位置传感器时,必须获得用户授权,并且在 Info.plist 中明确说明请求授权的原因,否则可能导致功能无法正常使用。
- 线程安全 :在自定义覆盖层渲染器的 draw(_:zoomScale:in:) 方法中,由于该方法可能在多个后台线程同时调用,因此要确保绘制操作是线程安全的,避免出现数据竞争和不一致的问题。
- 覆盖层层级管理 :在添加覆盖层时,合理设置 level: 参数,确保覆盖层的显示顺序符合预期,避免出现覆盖层遮挡重要信息的情况。
- 优化建议
- 缓存机制 :对于一些频繁使用的覆盖层或标注视图,可以考虑使用缓存机制,减少重复创建和绘制的开销,提高性能。
- 按需加载 :在地图缩放和滚动过程中,根据当前可见区域按需加载覆盖层和标注,避免一次性加载过多数据,提高响应速度。
- 性能测试 :在开发过程中,进行性能测试,找出性能瓶颈并进行优化,确保应用在不同设备和网络环境下都能有良好的表现。

12. 总结与展望

地图开发中的标注、覆盖层、显示当前位置以及与地图应用通信等功能为开发者提供了丰富的工具和手段,可以创建出功能强大、用户体验良好的地图应用。通过合理运用这些技术,并注意开发过程中的注意事项和进行性能优化,可以满足各种地图相关应用的需求。

未来,随着地图技术的不断发展,可能会有更多的功能和特性加入,例如更精确的位置定位、更丰富的地图样式和交互方式等。开发者可以持续关注地图开发领域的最新动态,不断学习和掌握新的技术,为用户带来更好的地图应用体验。同时,也可以结合其他技术,如人工智能、大数据等,进一步拓展地图应用的功能和应用场景,创造出更具创新性的地图应用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值