iOS

地图开发总结

###坑爹的高德
项目中地图原来用的高德sdk,其实我不知道当时为什么就用了高德,但既然用了,那么就在此基础上开发吧。可是写着写着,问题就来了: 问题链接。虽然这个问题不是什么太大的问题,但保不准以后还会遇到什么别的坑,而且以高德的效率,短期内肯定不会解决的。
那为什么还要用那个sdk呢?好像我们用到的功能苹果自带的MapKit都能实现啊,而且高德官网上的合作伙伴(携程、美团)也是用的原生的MapKit(从地图默认加载背景就可以看出来)。后来只花了一点点时间就切换到苹果的MapKit(API是一样的),目前还没发现什么bug。

###坐标转换之中国特色
因为不用高德了,所以当前位置经纬度就用CoreLocation来获取。可是当你把获取的经纬度放到MKMapView中显示,位置竟然有偏移,而且还不小。

这是苹果的bug吗?不是,是有关部门的功劳。我们只有一个地球,所以地球上每个点的经纬度是固定的,国际上有个通用标准,简称国际标准。CoreLocation取到的经纬度符合国际标准。

可是在国内,由于国家安全,有关部门制定了一套新的标准,简称国家标准。只要是在国内发行的地图(包括电子地图),都要按国家标准来,不然就是非法的。所以苹果手机中的国内地图肯定也是改良过的。

我拿着国际标准的经纬度放到改良过的符合国家标准的地图中,显然会发生偏移。

解决办法就是将国际标准的经纬度转换成国家标准的经纬度。算法是现成的(以前用高德sdk获取当前位置的经纬度本来就是国家标准的,所以没事。)

###坑爹的高德
项目中地图原来用的高德sdk,其实我不知道当时为什么就用了高德,但既然用了,那么就在此基础上开发吧。可是写着写着,问题就来了: 问题链接。虽然这个问题不是什么太大的问题,但保不准以后还会遇到什么别的坑,而且以高德的效率,短期内肯定不会解决的。
那为什么还要用那个sdk呢?好像我们用到的功能苹果自带的MapKit都能实现啊,而且高德官网上的合作伙伴(携程、美团)也是用的原生的MapKit(从地图默认加载背景就可以看出来)。后来只花了一点点时间就切换到苹果的MapKit(API是一样的),目前还没发现什么bug。

###坐标转换之中国特色
因为不用高德了,所以当前位置经纬度就用CoreLocation来获取。可是当你把获取的经纬度放到MKMapView中显示,位置竟然有偏移,而且还不小。

这是苹果的bug吗?不是,是有关部门的功劳。我们只有一个地球,所以地球上每个点的经纬度是固定的,国际上有个通用标准,简称国际标准。CoreLocation取到的经纬度符合国际标准。

可是在国内,由于国家安全,有关部门制定了一套新的标准,简称国家标准。只要是在国内发行的地图(包括电子地图),都要按国家标准来,不然就是非法的。所以苹果手机中的国内地图肯定也是改良过的。

我拿着国际标准的经纬度放到改良过的符合国家标准的地图中,显然会发生偏移。

解决办法就是将国际标准的经纬度转换成国家标准的经纬度。算法是现成的(以前用高德sdk获取当前位置的经纬度本来就是国家标准的,所以没事。)

###怎么使用MapKit
用苹果官方的framework最幸福了,因为有标准的文档可查,只要看就是了。目前我们对地图的使用非常简单,就是在地图上加地理位置标注,所以我要看的只有其中一小段

要实现标注,必须要有以下3个对象:

  • AnnotationObject(标注对象)
  • AnnotationView(标注视图)
  • AnnotationCalloutView(标注弹出视图,可看做是标注视图的一部分)

annotation.png

结合上面的图,AnnotationObject是标注的模型对象,AnnotationView就是大头针,AnnotationCallView就是弹出来的浮层。

######AnnotationObject

AnnotationObject必须实现MKAnnotation协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@protocol MKAnnotation <NSObject>

// Center latitude and longitude of the annotation view.
// The implementation of this property must be KVO compliant.
@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;

@optional

// Title and subtitle for use by selection UI.
@property (nonatomic, readonly, copy) NSString *title;
@property (nonatomic, readonly, copy) NSString *subtitle;

// Called as a result of dragging an annotation view.
- (void)setCoordinate:(CLLocationCoordinate2D)newCoordinate NS_AVAILABLE(10_9, 4_0);

@end

框架默认提供了一个MKPointAnnotation,只有coordinate、title、subtitle,如果要求不高,这个类也够用了。但更多的情况就需要我们自定义MKAnnotation。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface HJGasStationAnnotation : NSObject<MKAnnotation>

//经纬度
@property (nonatomic, assign) CLLocationCoordinate2D coordinate;
//标题
@property (nonatomic, copy) NSString* title;
//地址
@property (nonatomic, copy) NSString* address;
//电话
@property (nonatomic, copy) NSString* tel;

- (instancetype)initWithLocation:(CLLocationCoordinate2D)coordinate;

@end

里面的title、address、tel都是我随便加的,只要有需求,加任何东西都可以,但官方文档中也说了,AnnotationObject不能太大,不然地图上标注一多,效率会有问题。

想一下,为什么要有AnnotationObject这个对象?我看到之前的代码没么有自定义什么AnnotationObject,所有的标注都用MKPointAnnotation,然后所有其他的属性都封装成另外一个对象(ObjectB,这个ObjectB也是从项目的其他地方拿来的)都放到AnnotationView去。

这样造成的后果就是AnnotationView中既有AnnotationObject,又有ObjectB(而且ObjectB不够纯粹,有很多冗余的属性),导致AnnotationView代码耦合性较大。而子类话AnnotationObject,就能消除这种耦合,把ObjectB中有用的属性封装到SubAnnotationObject中即可,这样对于AnnotationView而言,只需要知道AnnotationObject,管它什么ObjectB呢!

######AnnotationView

框架提供了默认的MKPinAnnotationView,就是一个大头针。因为不够美观,我们可能要换成其它图片或者自定义视图。

怎么自定义呢?只要继承一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface HJGasStationAnnotationView : MKAnnotationView

@property (nonatomic, strong) HJGasStationCalloutView *calloutView;

@end


@implementation HJGasStationAnnotationView

- (id)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];

if (self)
{
self.bounds = CGRectMake(0, 0, 23, 35);
self.backgroundColor = [UIColor clearColor];
self.image = [UIImage imageNamed:@"map_location.png"];
}

return self;
}

@end

上面的自定义非常简单,只是换了张图片,如果有更复杂的需求,直接在里面加子控件或者重写drewRect都可。

######AnnotationCalloutView

框架里有标准的AnnotationCalloutView,只需要设置AnnotationView的属性canShowCallout为YES,就能展示默认的CalloutView;如果需要自定义CalloutView的话就必须将canShowCallout设为NO,这样就不会弹出默认的CalloutView。

自定义的CalloutView就是一个普通的View,爱怎么写就怎么写。它展示和消失的逻辑是由AnnotationView来控制的,所以说CalloutView其实是AnnotationView的一部分。

交互的逻辑就是重写两个方法,而且必须要重写,变量名根据实际情况会变,但方法逻辑结构就不需要动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL inside = [super pointInside:point withEvent:event];

if (!inside && self.selected)
{
inside = [self.calloutView pointInside:[self convertPoint:point toView:self.calloutView] withEvent:event];
}

return inside;
}

- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];

if(selected)
{
if (self.calloutView == nil) {
self.calloutView = [[HJGasStationCalloutView alloc] initWithFrame:CGRectMake(0, 0, kCalloutWidth, kCalloutHeight)];
[self.calloutView.telButton addTarget:self action:@selector(telButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
[self.calloutView.navButton addTarget:self action:@selector(navButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
self.calloutView.center = CGPointMake(CGRectGetWidth(self.bounds) / 2.f + self.calloutOffset.x,(-CGRectGetHeight(self.calloutView.bounds) / 2.f + self.calloutOffset.y));
}
[self bindDataToUI];
[self addSubview:self.calloutView];
}
else
{
[self.calloutView removeFromSuperview];
}

}

###展现逻辑

像MapView添加标注

1
[self.mapView addAnnotation:self.annotation];

在MapView的代理方法中生成AnnotationView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[HJGasStationAnnotation class]]) {

static NSString *customReuseIndetifier = @"HJGasStationAnnotation";
HJGasStationAnnotationView *annotationView = (HJGasStationAnnotationView*)[mapView dequeueReusableAnnotationViewWithIdentifier:customReuseIndetifier];

if (annotationView == nil)
{
annotationView = [[HJGasStationAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:customReuseIndetifier];
annotationView.canShowCallout = NO;
annotationView.draggable = NO;
annotationView.calloutOffset = CGPointMake(0, -5);
annotationView.selected = YES;
}
else {
annotationView.annotation = annotation;
}

[annotationView bindDataToUI];

return annotationView;
}

return nil;
}