前言
今天要记录的是项目中的一块功能实现。
- 功能需求:项目需要类似于苹果系统的相册那样,自带地图,在地图上显示具有地理位置的照片,并随着地图的缩放,地理位置的图片合并或展开,并可以点击查看。功能难点在于图片位置点要随着地图的缩放也要进行相应的合并和展开,挠头了一个星期,找到了思路去解决,实现了这个功能,这里就做一下分享。我在网上也没有搜到类似的demo参考。
接入高德地图
功能实现首先要接入地图,这里要求是接入高德地图,百度地图和高德地图其实应该都是没差的。首先先介绍一下工程接入高德地图。虽然网上的例子多的不行,那也不差我这一个,就当给自己做一个详细的笔记。
- 第一步
首先进入高德开放平台iOS地图SDK网站,注册登陆。接下来获取Key,步骤是:依次点击 控制台 -> 应用管理 -> 创建新应用 -> 创建 -> 添加新Key填写信息提交就ok了。直接上个动图。
注意:安全码Bundle ID 填写的就是你工程中的Bundle Identifier
- 第二步
创建工程,接入高德地图SDK。这里我是用CocoaPods接入的。安
装CocoaPods就不提了。在Podfile文件中添加
pod 'AMap3DMap' #3D地图SDK
pod 'AMapSearch' #地图SDK搜索功能
pod 'AMapLocation' #定位SDK
执行pod repo update 之后执行 pod install命令
-
第三步
配置工程,显示地图。因为我们引入的高德地图SDK是OC的,在swift工程中引用OC的东西需要创建桥接文件。桥接文件的创建,上动图:
编译不报错就证明头文件创建成功。成功之后在桥接文件中引入需要的头文件。
//地图
#import <AMapFoundationKit/AMapFoundationKit.h>
#import <AMapSearchKit/AMapSearchKit.h>
#import <AMapLocationKit/AMapLocationKit.h>
#import <MAMapKit/MAMapKit.h>
工程配置完毕,开始写代码显示地图。在AppDelegate中设置高德地图的Key,这个Key值就是之前在开放平台上申请的。
//高德地图
AMapServices.shared().apiKey = ""
在ViewController中创建mapView对象,添加到controller上。
let mapView = MAMapView.init(frame: self.view.bounds)
mapView.delegate = self
mapView.setZoomLevel(10.0, animated: false)
self.view.addSubview(mapView)
OK,高德地图接入成功,下面正式开始实现功能。
功能实现
- 先上一个运行动图: 功能演示
主要思路
-
放大或缩小地图,改变的是地图的比例尺参数(mapView.zoomLevel),比例尺表示的是一屏幕像素等于的实际距离,单位是(米),在地图上的两个标注点的实际距离是不变的,缩放地图,两标注点的屏幕像素距离要发生改变,所以,我们就可以定一个常量,比如20屏幕像素距离,缩放地图时,标注点之间超过了20像素的距离就展开,小于就合并为一个。
-
这样我们就需要写一个方法,这个方法要在地图每次执行缩放的时候对坐标组中各个坐标之间的像素距离重新计算、重新分类。将小于规定像素距离的坐标点(CLLocationCoordinate2D),归为一个地图标注点(MAPointAnnotation)
主要代码
创建Model类 MapMdoel.swift 用于地图数据处理,实际项目中model是由网络请求回来的数据进行赋值。
import UIKit
//用于地图相册设计的model,包含图片网址和地理位置,开发中这些数据应该是由网络获取回来,然后用model来承接和处理这些数据。
class MapModel: NSObject {
var imageUrl: String = "" //图片网址
var longitude: String = "" //经度
var latitude: String = "" //纬度
}
创建一个工具类, MapViewTool.swift用于处理地图的数据。
import UIKit
let MAPZOOMLEVEL: CGFloat = 10.0
class MapViewTool: NSObject {
//初始化model数据数组这里的model数据提前设置好
class func initModelArr() -> Array<MapModel> {
let model1 = MapModel()
model1.imageUrl = "https://flashcdn.hyuge.cn/flash/1666231/78565/12516700_mid"
model1.latitude = "39.992520"
model1.longitude = "116.336170"
let model2 = MapModel()
model2.imageUrl = "https://flashcdn.hyuge.cn/flash/1666231/78565/12516699_mid"
model2.latitude = "39.978234"
model2.longitude = "116.352343"
let model3 = MapModel()
model3.imageUrl = "https://flashcdn.hyuge.cn/flash/1666231/78565/12516698_mid"
model3.latitude = "39.998293"
model3.longitude = "116.348904"
let model4 = MapModel()
model4.imageUrl = "https://flashcdn.hyuge.cn/flash/1666231/78565/12516697_mid"
model4.latitude = "40.004087"
model4.longitude = "116.353915"
let model5 = MapModel()
model5.imageUrl = "https://flashcdn.hyuge.cn/flash/1666231/78565/12516695_mid"
model5.latitude = "40.001442"
model5.longitude = "116.353915"
let model6 = MapModel()
model6.imageUrl = "https://flashcdn.hyuge.cn/flash/1666231/78565/12516532_mid"
model6.latitude = "39.989105"
model6.longitude = "116.360200"
let model7 = MapModel()
model7.imageUrl = "https://flashcdn.hyuge.cn/flash/1666231/78565/12516497_mid"
model7.latitude = "39.989098"
model7.longitude = "116.360201"
let model8 = MapModel()
model8.imageUrl = "https://flashcdn.hyuge.cn/flash/1666231/78565/12516496_mid"
model8.latitude = "39.998439"
model8.longitude = "116.324219"
let model9 = MapModel()
model9.imageUrl = "https://flashcdn.hyuge.cn/flash/1666231/78565/12516495_mid"
model9.latitude = "39.979590"
model9.longitude = "116.352792"
let modelArr = [model1, model2, model3, model4, model5, model6, model7, model8, model9]
return modelArr
}
//初始化地理坐标数组
class func initCoordinateArr(mapController: MapController) {
let modelArr = initModelArr()
mapController.mapModelArr = modelArr
for model in modelArr {
let coor = CLLocationCoordinate2D.init(latitude: Float64(model.latitude)!, longitude: Float64(model.longitude)!)
mapController.coordinateArr.append(coor)
}
}
//MARK: 功能实现的主要方法。
//思路:放大或缩小地图,改变的是地图的比例尺参数(mapView.zoomLevel),比例尺表示的是一屏幕像素等于的实际距离,单位是(米),在地图上的两个标注点的实际距离是不变的,缩放地图,两标注点的屏幕像素距离要发生改变,所以,我们就可以定一个常量,比如20屏幕像素距离,缩放地图时,标注点之间超过了20像素的距离就展开,小于就合并为一个。
//这样我们就需要写一个方法,这个方法要在地图每次执行缩放的时候对坐标组中各个坐标之间的像素距离重新计算、重新分类。将小于规定像素距离的坐标点(CLLocationCoordinate2D),归为一个地图标注点(MAPointAnnotation)
class func changeMapViewDataForMapScale(mapController: MapController) {
//mapController.coordinateArr 这是已经有值的坐标数组 mapController.mapModelArr 这是已经有值的model数组
let mapView: MAMapView! = mapController.mapView
let coorArr = mapController.coordinateArr
let modelArr = mapController.mapModelArr
if modelArr.count == 0 {
return
}
//创建接收更新分类好的数组
var refreshPointArr: Array<Array<MAPointAnnotation>> = []
var refreshModelArr: Array<Array<MapModel>> = []
//这里规定值为 20 像素
//获取当前比例尺下,20像素的实际距离,为最大实际距离,小于这个距离的合并
let scaleDistace = mapView.metersPerPoint(forZoomLevel: mapView.zoomLevel)
let maxDistance = scaleDistace * 20
//下面是主要的处理算法
for (idx, coor) in coorArr.enumerated() {
let currentLocal = CLLocation.init(latitude: coor.latitude, longitude: coor.longitude) //CLLocation 用于计算两坐标点的实际距离
let pointAnno = MAPointAnnotation() //用于地图的标注点
pointAnno.coordinate = coor
let model = modelArr[idx]
if idx == 0 {
var newPointArr: Array<MAPointAnnotation> = []
newPointArr.append(pointAnno)
refreshPointArr.append(newPointArr)
//处理坐标点分类的同时,将model数组也相应进行分类
var newModelArr: Array<MapModel> = []
newModelArr.append(model)
refreshModelArr.append(newModelArr)
} else {
//这里意思是每个坐标点要跟已经分好组的数组的第一项做比较,小于最大值就加入该数组,如果都大于的话,就新建一个数组,添加到分类数组(refreshPointArr)中
for (idxx, pointAnnoArr) in refreshPointArr.enumerated() {
let firstCoor: CLLocationCoordinate2D = pointAnnoArr.first!.coordinate
let arrFirstLocal = CLLocation.init(latitude: firstCoor.latitude, longitude: firstCoor.longitude)
let distance = currentLocal.distance(from: arrFirstLocal)
if distance < maxDistance {
//这里我直接添加到数组中去报错,所以我用替换的方法添加
var annoArr = pointAnnoArr
annoArr.append(pointAnno)
refreshPointArr.replaceSubrange(Range(idxx..<(idxx + 1)), with: [annoArr])
var modelArr = refreshModelArr[idxx]
modelArr.append(model)
refreshModelArr.replaceSubrange(Range(idxx..<(idxx + 1)), with: [modelArr])
break
} else {
//当遍历到最后一个,距离distance>maxDistance 创建一个新的分组数组,加到refreshPointArr中
if idxx == (refreshModelArr.count - 1) {
var newPointArr: Array<MAPointAnnotation> = []
newPointArr.append(pointAnno)
refreshPointArr.append(newPointArr)
var newModelArr: Array<MapModel> = []
newModelArr.append(model)
refreshModelArr.append(newModelArr)
}
}
}
}
}
getElementCountArr(mapController: mapController, refreshAnnoArr: refreshPointArr)
getPointAnnoArrAndElementFirstArr(refreshModelArr: refreshModelArr, refreshAnnoArr: refreshPointArr, mapController: mapController)
}
//MARK: 对mapController.elementCountArr进行赋值 (每个分组的数据数量)
class func getElementCountArr(mapController: MapController, refreshAnnoArr: Array<Array<MAPointAnnotation>>) {
var tempArr: Array<Int> = []
for arr in refreshAnnoArr {
let count = arr.count
tempArr.append(count)
}
mapController.elementCountArr = tempArr
}
//MARK: 对 mapController.pointAnnotationArr和 mapController.elementFirstDataArr
class func getPointAnnoArrAndElementFirstArr(refreshModelArr: Array<Array<MapModel>>, refreshAnnoArr: Array<Array<MAPointAnnotation>>, mapController: MapController){
var tempModelArr: Array<MapModel> = []
var tempAnnoArr: Array<MAPointAnnotation> = []
for (idx, arr) in refreshAnnoArr.enumerated() {
let anno: MAPointAnnotation = arr.first!
anno.title = String(idx)
tempAnnoArr.append(anno)
let modeArr = refreshModelArr[idx]
let model = modeArr.first!
tempModelArr.append(model)
}
mapController.pointAnnotationArr = tempAnnoArr
mapController.elementFirstDataArr = tempModelArr
}
}
地图的标注点需要自定义气泡样式,所以需要创建一个自定义的MAAnnotationView,这里需要对自定义的View进行重绘,代码里的注释很清楚。
import UIKit
//这里是自定义的气泡标识
class CustomAnnotationView: MAAnnotationView {
//设置一些尺寸
let labelBackColor = UIColor(red:0.09, green:0.45, blue:1.00, alpha:1.00)
let annoViewWidth: CGFloat = 60
let annoViewHeight: CGFloat = 70
let imageWith: CGFloat = 55
let imageHeight: CGFloat = 55
let labelWidth: CGFloat = 20
let labelHeight: CGFloat = 20
var picture = UIImageView()
var countLabel = UILabel()
override init!(annotation: MAAnnotation!, reuseIdentifier: String!) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
self.setNeedsDisplay()
self.bounds = CGRect(x: 0, y: 0, width: annoViewWidth, height: annoViewHeight)
self.backgroundColor = UIColor.clear
}
//MARK: 重绘View视图,画出一个尖角
override func draw(_ rect: CGRect) {
super.draw(rect)
drawInContext(context: UIGraphicsGetCurrentContext()!)
configView()
}
//添加子视图
func configView() {
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOpacity = 1.0
self.layer.shadowOffset = CGSize(width: 0, height: 0)
picture = UIImageView(frame: CGRect(x: 2.5, y: 2.5, width: imageWith, height: imageHeight))
picture.clipsToBounds = true
picture.contentMode = UIViewContentMode.scaleAspectFill
picture.isUserInteractionEnabled = true
countLabel.frame = CGRect(x: self.frame.width - 10, y: -10, width: labelWidth, height: labelHeight)
countLabel.layer.cornerRadius = 10
countLabel.clipsToBounds = true
countLabel.backgroundColor = labelBackColor
countLabel.textColor = UIColor.white
countLabel.textAlignment = NSTextAlignment.center
countLabel.font = UIFont.systemFont(ofSize: 10)
self.addSubview(picture)
self.addSubview(countLabel)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//重绘方法
func drawInContext(context: CGContext) {
//配置
context.setLineWidth(2.0)
context.setFillColor(UIColor.white.cgColor)
//绘制路线
getDrawPath(context: context)
context.fillPath()
}
//绘制路线方法
func getDrawPath(context: CGContext) {
let rrect = self.bounds
let radius: CGFloat = 6.0
let minx = rrect.minX
let midx = rrect.midX
let maxx = rrect.maxX
let miny = rrect.minY
let maxy = rrect.maxY - 10
context.move(to: .init(x: midx + 10, y: maxy))
context.addLine(to: .init(x: midx, y: maxy + 10))
context.addLine(to: .init(x: midx - 10, y: maxy))
context.addArc(tangent1End: .init(x: minx, y: maxy), tangent2End: .init(x: minx, y: miny), radius: radius)
context.addArc(tangent1End: .init(x: minx, y: miny), tangent2End: .init(x: maxx, y: miny), radius: radius)
context.addArc(tangent1End: .init(x: maxx, y: miny), tangent2End: .init(x: maxx, y: maxy), radius: radius)
context.addArc(tangent1End: .init(x: maxx, y: maxy), tangent2End: .init(x: minx, y: maxy), radius: radius)
context.closePath()
}
}
辅助类写好后,开始实现MapController,代码中都有详细的注释。
import UIKit
class MapController: UIViewController, MAMapViewDelegate {
var mapView: MAMapView!
var mapModelArr: Array<MapModel> = [] //地图数据数组
var coordinateArr: Array<CLLocationCoordinate2D> = [] //存储坐标地理位置的数组
var pointAnnotationArr: Array<MAPointAnnotation> = [] //地图标注点的数组
var elementCountArr: Array<Int> = [] //每个标注点的数据数量数组,用于显示数据的数量
var elementFirstDataArr: Array<MapModel> = [] //每个标注点中的首个元素数据,用于标注点的附图
var lastZoomAnnoArr: Array<MAPointAnnotation> = [] //记录上一次缩放生成的标注点数组
var mapZoomCount: CGFloat = 10.0 //记录mapView的缩放比例
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
configMapView()
getMapConfigData()
// Do any additional setup after loading the view.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//mapView 添加标注点
mapView.addAnnotations(pointAnnotationArr)
mapView.showAnnotations(pointAnnotationArr, edgePadding: UIEdgeInsetsMake(100, 100, 100, 100), animated: true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK: 配置地图
func configMapView() {
mapView = MAMapView.init(frame: self.view.bounds)
mapView.delegate = self
mapView.setZoomLevel(MAPZOOMLEVEL, animated: false)
self.view.addSubview(mapView)
}
//MARK: 处理地图显示的数据
func getMapConfigData() {
//对地理位置坐标数组(coordinateArr)和地图数据数组(MapModelArr)赋值
MapViewTool.initCoordinateArr(mapController: self)
getShowMapDate()
}
//MARK: 处理用于展示的地图数据
func getShowMapDate() {
MapViewTool.changeMapViewDataForMapScale(mapController: self)
}
//MARK: mapViewDelegate
//返回标注点的代理方法 这里的MAAnnotationView是自定义的。
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
if annotation.isKind(of: MAPointAnnotation.self) {
let pointReuseIndetifier = "pointReuseIndetifier"
var annotationView: CustomAnnotationView? = mapView.dequeueReusableAnnotationView(withIdentifier: pointReuseIndetifier) as? CustomAnnotationView
if annotationView == nil {
annotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: pointReuseIndetifier)
}
let idx = Int(annotation.title!)
let countText: String = String(self.elementCountArr[idx!])
annotationView?.countLabel.text = countText
let model = self.elementFirstDataArr[idx!]
let imageUrl = URL.init(string: model.imageUrl)
//主线程刷新UI
DispatchQueue.main.async {
annotationView?.picture.sd_setImage(with: imageUrl, placeholderImage: UIImage())
}
return annotationView
}
return nil
}
//将要缩放的代理方法
func mapView(_ mapView: MAMapView!, mapWillZoomByUser wasUserAction: Bool) {
//记录上一次缩放的标注点数组
lastZoomAnnoArr = self.pointAnnotationArr
}
//进行缩放的代理方法
func mapView(_ mapView: MAMapView!, mapDidZoomByUser wasUserAction: Bool) {
//当缩放比例不变就不执行
if self.mapZoomCount != mapView.zoomLevel {
DispatchQueue.main.async {
//先将已有的标注点数组删除
mapView.removeAnnotations(self.lastZoomAnnoArr)
//获取新的数据
self.getShowMapDate()
//记录缩放后的缩放比例
self.mapZoomCount = mapView.zoomLevel
//添加新数据
mapView.addAnnotations(self.pointAnnotationArr)
}
}
}
}
最后
搞定,最后附上代码链接https://github.com/Sufviay/GaoDeTest 。
网友评论