CCHMapClusterController
CCHMapClusterController
解决在MKMapView
上显示多个标注的问题,并遵循MIT许可证。
注意:随着iOS 11的发布,Apple推出了MapKit中的地图聚合支持。您可以在iOS 11上继续使用CCHMapClusterController
,但对于新项目,我建议您检查内置功能是否满足您的需求。我仍然会接受对bug修复和小型增强的PR,但不会实现任何新功能。
查看变更日志以了解最近的更新概述。
需要和人交流吗?我在Twitter上是 @claushoefele。
入门指南
如果您已经设置了MKMapView
的项目,集成聚合将需要4行代码
#import "CCHMapClusterController.h" @interface ViewController() @property (strong, nonatomic) CCHMapClusterController *mapClusterController; @end - (void)viewDidLoad { [super viewDidLoad] NSArray annotations = ... self.mapClusterController = [[CCHMapClusterController alloc] initWithMapView:self.mapView]; [self.mapClusterController addAnnotations:annotations withCompletionHandler:NULL]; }
无需担心手动更新聚类;CCHMapClusterController
自动识别何时发生需要聚类重新组群的变化。
要尝试聚类功能,可以在此项目中的示例进行实验,或下载“柏林绊脚石”应用。
使用方法
- 安装
- 性能
- 单元格大小和边缘系数
- 自定义注释视图
- 为弹窗自定义标题和副标题
- 定位聚类注释
- 聚类分组
- 动态禁用聚类
- 动画
- 代码食谱
- 查找聚类注释
- 在不更改缩放级别的情况下使地图居中
- 在注释视图中接收点击
- 放大到聚类
- 仅对未聚类的注释显示弹窗附加视图
- 其他阅读材料
- 许可协议(MIT协议)
安装
使用 CocoaPods 可以轻松将 CCHMapClusterController
集成到项目中。iOS 的最低部署目标为 7.0,而 OS X 的最低部署目标为 10.9。
platform :ios, '7.0'
pod "CCHMapClusterController"
platform :osx, '10.9'
pod "CCHMapClusterController"
性能
聚类算法将地图的矩形区域分割成方形单元格的网格。对于每个单元格,选择并显示该单元格中的注释表示。
用于收集单元格注释的 quad tree 实现基于 TBQuadTree,运行非常快速。因此,性能更多地取决于地图上可见的聚类数量,而不是聚类注释的数量。可以通过单元格大小和边缘系数(如下所述)配置此数量。
其他因素包括聚类注释的密度(注释分布在较大区域内聚类更快)和注释视图的实现方式(如果可能,使用图像而不是 drawRect:
)。
项目中的示例包含两个用于测试的数据集:围绕柏林的小区域有5000多个注释,以及覆盖整个美国的80000多个注释。这两个数据集在 iPhone 4S 上都能良好运行。
单元格大小和边距因子
CCHMapClusterController
有一个名为cellSize
的属性,用于配置单元格的尺寸(以点为单位,1点=Retina显示器上的2像素)。这样,您可以选择足够大的单元格尺寸,以便尽可能少重叠地显示地图图标。更可能的是,您将选择单元格大小以优化聚类性能(尺寸越大,性能越好)。实际用于聚类的单元格大小将调整,使地图宽度成为单元格尺寸的倍数。这样,在跨越180度经线时避免单元格重排。
marginFactor
属性配置了围绕可视区域的额外地图区域,这些区域包含在聚类中。这避免了用户在平移地图时在可视区域边缘的突然变化。理想情况下,您会设置此值为1.0(每边100%的额外地图区域),因为这是用户通过平移手势可以实现的最大的滚动区域。然而,这会影响性能,因为这将覆盖9倍的聚类区域。默认值为0.5(每边50%的额外区域)。
要调试这些设置,将debugEnabled
属性设置为YES
。这将显示聚类网格覆盖在地图上。
自定义标注视图
聚类标注的类型为CCHMapClusterAnnotation
。除了实现MKAnnotation
协议外,该类的属性annotations
公开了一个包含在聚类中的标注数组。CCHMapClusterAnnotation
还有一个可以帮助您分类聚类标注的两个属性:isCluster
返回YES
,如果聚类标注包含多个标注;isUniqueLocation
如果聚类中的所有标注具有相同的地理位置。
可以通过标准的mapView:viewForAnnotation:
方法来定制聚类标注的外观,这是MKMapViewDelegate
的一部分。
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
MKAnnotationView *annotationView;
if ([annotation isKindOfClass:CCHMapClusterAnnotation.class]) {
...
annotationView = clusterAnnotationView;
}
return annotationView;
}
此外,当聚类标注被重新用于单元格时,会调用代理方法mapClusterController:willReuseMapClusterAnnotation:
。重新使用的聚类标注将具有与之前相同的地理位置,但将包含不同的标注。这避免了在添加更多数据时标注移动(关于如何禁用此行为,请参见下面的属性reuseExistingClusterAnnotations
)。
请确保您实现了mapView:viewForAnnotation:
和mapClusterController:willReuseMapClusterAnnotation:
,以确保标注视图始终处于一致的状态。
- (void)mapClusterController:(CCHMapClusterController *)mapClusterController willReuseMapClusterAnnotation:(CCHMapClusterAnnotation *)mapClusterAnnotation
{
ClusterAnnotationView *clusterAnnotationView = (ClusterAnnotationView *)[self.mapView viewForAnnotation:mapClusterAnnotation];
...
}
iOS示例中包含演示如何将当前聚类大小作为标注视图一部分的代码。
为调用输出自定义标题和副标题
您可以通过将 CCHMapClusterControllerDelegate
随 CCHMapClusterController
注册,并实现两个代理方法来自定义用于它们的调用输出标题和副标题。
在这些方法中,CCHMapClusterAnnotation
通过其属性 annotations
允许您通过获取聚合中的注释。此数组中的注释将始终实现 MKAnnotation
,但与其他在调用 addAnnotations:withCompletionHandler:
时添加到 CCHMapClusterController
的实例具有相同的类型。
以下是一个示例
- (NSString *)mapClusterController:(CCHMapClusterController *)mapClusterController titleForMapClusterAnnotation:(CCHMapClusterAnnotation *)mapClusterAnnotation
{
NSUInteger numAnnotations = mapClusterAnnotation.annotations.count;
NSString *unit = numAnnotations > 1 ? @"annotations" : @"annotation";
return [NSString stringWithFormat:@"%tu %@", numAnnotations, unit];
}
- (NSString *)mapClusterController:(CCHMapClusterController *)mapClusterController subtitleForMapClusterAnnotation:(CCHMapClusterAnnotation *)mapClusterAnnotation
{
NSUInteger numAnnotations = MIN(mapClusterAnnotation.annotations.count, 5);
NSArray *annotations = [mapClusterAnnotation.annotations.allObjects subarrayWithRange:NSMakeRange(0, numAnnotations)];
NSArray *titles = [annotations valueForKey:@"title"];
return [titles componentsJoinedByString:@", "];
}
自定义调用输出的注释位置
从美观的角度看,聚类的注释不会均匀排列,因为这会使得下方的网格线显而易见。此库自带两项实现来配置聚类注释的位置
CCHCenterOfMassMapClusterer
(默认):计算一个聚集中所有注释坐标的平均值CCHNearCenterMapClusterer
:使用紧邻中心的注释的位置
将这些类的实例分配给 CCHMapClusterController
的属性 clusterer
。通过实现 CCHMapClusterer
协议,您可以提供自定义定位聚类注释的策略。
此外,CCHMapClusterController
默认会为单元格重用聚类注释。这在增量地给聚类添加更多注释时很有用(例如,当下载数据批处理时),因为这可以避免在更新期间散点注释四处跳跃。如果您不想有这种行为,则请将 reuseExistingClusterAnnotations
设置为 NO
。
聚类分组
为了获得独立的集群组,可以有多于一个 CCHMapClusterController
在相同的 MKMapView
实例上工作。每个 CCHMapClusterController
都可以有自己的一组设置。
// First cluster controller
self.mapClusterControllerRed = [[CCHMapClusterController alloc] initWithMapView:self.mapView];
self.mapClusterControllerRed.cellSize = ...;
self.mapClusterControllerRed.marginFactor = ...;
// Second cluster controller
self.mapClusterControllerBlue = [[CCHMapClusterController alloc] initWithMapView:self.mapView];
self.mapClusterControllerBlue.cellSize = ...;
self.mapClusterControllerBlue.marginFactor = ...;
动态禁用聚类
有时根据当前地图数据禁用聚类会有所帮助。这允许在必要时显示未聚类的注释。有两个属性用于这个目的
maxZoomLevelForClustering
:根据地图缩放程度控制整个地图的聚合。要禁用聚合,将maxZoomLevelForClustering
属性设置为想要停止聚合的缩放级别。缩放级别为0表示整个地图适合屏幕宽度,缩放时值增加。您可以从CCHMapClusterController
的属性zoomLevel
获取当前缩放级别。默认情况下,maxZoomLevelForClustering
设置为DBL_MAX
,这意味着聚合永远不会被禁用。minUniqueLocationsForClustering
:根据单元格中的唯一位置数量控制基于单元格的聚合。如果单元格中的唯一位置数量低于此值,则禁用聚合。默认情况下,minUniqueLocationsForClustering
设置为0,这意味着聚合永远不会被禁用。
此项目中的示例包含用于实验这些属性的设置。
动画
默认情况下,集群注释的注释视图在添加时进行渐显动画,在删除时进行渐隐动画(CCHFadeInOutAnimator
)。您可以通过实现协议CCHMapAnimator
并提供自己的动画代码来更改CCHMapClusterController
的属性animator
。
代码秘籍
此列表包含解决人们遇到的一些问题的解决方案。您可以通过创建新问题
来提出更多问题。
查找聚合注释
一个常见的用例是拥有一个搜索字段,用户可以从匹配注释的列表中选择。选择注释后,然后再将地图缩放到其位置。
要使此功能生效,您必须找出哪个群组包含选定的注释。此外,在缩放时聚类会发生变化,因此需要一种增量方法来找到包含用户正在寻找的注释的群组。
CCHMapClusterController
提供了一个易于使用的接口来帮助您完成此操作。请注意,您必须使用先前已添加到聚类的注释。
id<MKAnnotation> clusteredAnnotation = ...
[self.mapClusterController addAnnotations:@[clusteredAnnotation] withCompletionHandler:NULL];
...
[self.mapClusterController selectAnnotation:clusteredAnnotation andZoomToRegionWithLatitudinalMeters:1000 longitudinalMeters:1000];
在不改变缩放级别的情况下居中地图
《MKMapView》提供了一个方法 setCenterCoordinate:animated:
,可以在不改变当前缩放级别的情况下将地图居中到一个新的坐标位置。遗憾的是,在iOS 7上,这个方法并不像它所宣传的那样工作。调用它时,地图会稍微进行缩放,从而导致簇重新组合到不同的缩放级别。
以下代码可以避免这个问题
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
MKMapPoint point = MKMapPointForCoordinate(view.annotation.coordinate);
MKMapRect rect = [mapView visibleMapRect];
rect.origin.x = point.x - rect.size.width * 0.5;
rect.origin.y = point.y - rect.size.height * 0.5;
[mapView setVisibleMapRect:rect animated:YES];
}
在注释视图中接收点击事件
mapView:didSelectAnnotationView:
的表现取决于注释视图中canShowCallout
属性的设置状态。
如果将 canShowCallout
设置为 YES
,则点击注释视图会打开一个呼出视图。只有当您的注释标题设置为非零长度的字符串时,地图视图才会调用 mapView:didSelectAnnotationView:
。因此,您必须实现 mapClusterController:titleForMapClusterAnnotation:
来返回一个簇注释的标题。
如果您不想在注释视图中显示呼出视图,则必须将 canShowCallout
设置为 NO
(默认值)。完成这一操作后,将不带标题调用 mapView:didSelectAnnotationView:
。
有一个需要注意的地方是,地图视图会记住最后一次选择。为了能够再次选择相同的注释视图,您必须先取消选择其注释。
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
[mapView deselectAnnotation:view.annotation animated:NO];
}
缩放以查看簇
在iOS 7上,您可以使用 showAnnotations:animated:
,但这会将指定的注释添加到 MKMapView
中。因此,除了簇之外,您还会在屏幕上看到所有的聚集注释。
相反,《CCHMapClusterAnnotation》提供了一个名为 mapRect
的方法,它会手动计算一个 MKMapRect
,其中包含所有聚集注释。
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
if ([view.annotation isKindOfClass:CCHMapClusterAnnotation.class]) {
CCHMapClusterAnnotation *clusterAnnotation = (CCHMapClusterAnnotation *)view.annotation;
MKMapRect mapRect = [clusterAnnotation mapRect];
UIEdgeInsets edgeInsets = UIEdgeInsetsMake(20, 20, 20, 20);
[mapView setVisibleMapRect:mapRect edgePadding:edgeInsets animated:YES];
}
}
如果您想要保证在放大时每个簇注释在地图上都有一个独特的位置,请使用《CCHMapClusterController》的 maxZoomLevelForClustering
属性。
仅为非聚集注释显示呼出视图附件
这可以通过设置附加视图并使用canShowCallout
属性来控制它们的显示状态来实现。
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
MKAnnotationView *annotationView;
if ([annotation isKindOfClass:CCHMapClusterAnnotation.class]) {
annotationView = ...
annotationView.rightCalloutAccessoryView = ...
CCHMapClusterAnnotation *clusterAnnotation = (CCHMapClusterAnnotation *)annotation;
annotationView.canShowCallout = clusterAnnotation.isUniqueLocation;
}
}
- (void)mapClusterController:(CCHMapClusterController *)mapClusterController willReuseMapClusterAnnotation:(CCHMapClusterAnnotation *)mapClusterAnnotation
{
ClusterAnnotationView *clusterAnnotationView = (ClusterAnnotationView *)[self.mapClusterController.mapView viewForAnnotation:mapClusterAnnotation];
clusterAnnotationView.canShowCallout = clusterAnnotation.isUniqueLocation;
}
阅读更多
- Theodore Calmes对如何实现集群算法的深入了解
- Claus 2013 年在 Macoun 上的演示视频(德语),解释了实现
CCHMapClusterController
使用的一些技术 - 博客文章涵盖了 Claus 在 Macoun 的演示
许可(MIT)
版权所有(C)2013 Claus Höfele
允许任何获得本软件及其相关文档(“软件”)副本的人免费使用该软件而不受限制,包括但不限于使用、复制、修改、合并、出版、分发、再许可和/或销售软件副本的权利,并允许向该软件提供副本的个人这样做,前提是遵守以下条件
上述版权声明和本许可声明应包含在软件的所有副本或实质部分中。
软件按“现状”提供,不提供任何形式的保证,无论是明示的、暗示的,还是关于销售性、特定目的适用性或非侵权的担保。在任何情况下,作者或版权所有者都不应对任何索赔、损害或其他责任(无论是基于合同、侵权或其他方式)承担责任,无论是因软件或其使用或其他方式而产生的,还是与之相关的。