大头针显隐跟随楼层功能探索

背景

mapbox 提供的大头针默认没有楼层相关属性,无法实现切换楼层时,只显示对应楼层的大头针效果。客户端同事无法解决此问题,希望我在 SDK 端解决此问题,故进行相关探索(🤷‍♀️)。由于有段时间没有做地图 SDK 开发了,故进行了如下各种踩坑尝试。

尝试思路

在 mapbox 提供的原有类和方法基础上实现;
尽可能不影响客户端已使用的 mapbox 原有大头针 api 相关代码。

思路一

思路来源:面向协议编程!

如果能够新增一个协议,使 mapbox 原大头针相关类遵守此协议,然后实现楼层属性,在使用时对楼层属性赋值,在 SDK 内部进行逻辑判定,就实现功能就好了!

想到这,不禁感慨,不愧是我!😆

于是进行了如下尝试:

新增带楼层属性(floorID4Annotation )的协议:

//MARK:protocol
@protocol HTMIndoorMapAnnotationViewAutoHide <NSObject>

/// 大头针所在楼层id
@property (nonatomic, assign) int floorID4Annotation;

@end

让需要显隐的大头针的类遵守协议,实现楼层属性(@synthesize floorID4Annotation = _floorID4Annotation;)。eg:

@interface HTMCustomPointAnnotation : MGLPointAnnotation<HTMIndoorMapAnnotationViewAutoHide>
@end

@implementation HTMCustomPointAnnotation
@synthesize floorID4Annotation = _floorID4Annotation;
@end

使用时,对楼层属性赋值。然后在切换楼层的相关方法里遍历地图对象大头针数组,判定大头针对象是否响应 floorID4Annotation 方法,对于响应的对象,对比它的楼层属性和当前显示楼层是否一致,不一致则隐藏,一致则显示。相关代码:

- (void)pmy_updateAnnotationsWithFloorId:(int)floorID {
    [self.mapView.annotations enumerateObjectsUsingBlock:^(
                                                           id
                                                           //                                                           <MGLAnnotation>//必须注释,否则obj无法获取其他协议中属性
                                                           _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj respondsToSelector:@selector(floorID4Annotation)]) {
            int lFoorID =  [obj floorID4Annotation];
            MGLPointAnnotation *lP = (MGLPointAnnotation *)obj;
          
            //MGLPointAnnotation类没有`hidden`属性!!!
            lP.hidden = !(lFoorID == floorID);
        }else{
            //未遵守 HTMIndoorMapAnnotationViewAutoHide 协议,不管
        }
    }];
}

但是,遗憾的发现,编译器报错:Property 'hidden' not found on object of type 'MGLPointAnnotation *',oh my god,瞬间懵逼!😳

改进思路:先移除,再添加与显示楼层相同的 或 未遵守HTMIndoorMapAnnotationAutoHide协议的 大头针(使客户端可以保留不受楼层切换影响的大头针显示效果)。

//更新 大头针 显隐;先移除,再添加与显示楼层相同的 或 未遵守HTMIndoorMapAnnotationAutoHide协议的 大头针
- (void)pmy_updateAnnotationsWithFloorId:(int)floorID {
    NSArray *lArr = self.mapView.annotations;
    NSMutableArray *lArrM = @[].mutableCopy;
    
    [self.mapView.annotations enumerateObjectsUsingBlock:^(
                                                           id
                                                           //                                                           <MGLAnnotation>//必须注释,否则obj无法获取其他协议中属性
                                                           _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj respondsToSelector:@selector(floorID4Annotation)]) {
            int lFoorID =  [obj floorID4Annotation];
            if (floorID == lFoorID) {
                [lArrM addObject:obj];
            }
        }else{
            //未遵守 HTMIndoorMapAnnotationViewAutoHide 协议
            [lArrM addObject:obj];
        }
    }];
    
    [self.mapView removeAnnotations:lArr];
    [self.mapView addAnnotations:lArrM];
}

但是,运行后发现,切换楼层 1 次后,正常;再次切换楼层,大头针都没有了!于是发现此逻辑是行不通的!每次切楼层都会使大头针数量减少。

再想,如果对 self.mapView.annotations 做缓存呢?还是不行,因为当客户端新增或删除大头针时,无法监听到 self.mapView.annotation 的变化(让客户端每次增删都发通知的话,用起来就会太麻烦)。缓存无法更新,导致大头针显示数量只增不减!🙃

后来发现,有设置 shape annotation 透明度的方法:

/**
 Returns the alpha value to use when rendering a shape annotation.

 A value of `0.0` results in a completely transparent shape. A value of `1.0`,
 the default, results in a completely opaque shape.

 This method sets the opacity of an entire shape, inclusive of its stroke and
 fill. To independently set the values for stroke or fill, specify an alpha
 component in the color returned by `-mapView:strokeColorForShapeAnnotation:` or
 `-mapView:fillColorForPolygonAnnotation:`.

 @param mapView The map view rendering the shape annotation.
 @param annotation The annotation being rendered.
 @return An alpha value between `0` and `1.0`.
 */
- (CGFloat)mapView:(MGLMapView *)mapView alphaForShapeAnnotation:(MGLShape *)annotation;

但是实测发现通过 addAnnotation 方法添加的大头针不会触发上面的回调!😐

思路二

既然 MGLPointAnnotation 类没有 hidden 属性,那么其他类是否有呢?于是找到了 MGLAnnotationView 类:

@interface MGLAnnotationView : UIView <NSSecureCoding>

继承自 UIView,故它是有 hidden 属性的。

于是在思路一的基础上改进:

@interface HTMCustomAnnotationView : MGLAnnotationView<HTMIndoorMapAnnotationViewAutoHide>
@end

@implementation HTMCustomAnnotationView
@synthesize floorID4Annotation = _floorID4Annotation;
@end

SDK 内更新大头针代码:

- (void)pmy_updateAnnotationsWithFloorId:(int)floorID {
    [self.mapView.annotations enumerateObjectsUsingBlock:^(
                                                           id
//                                                           <MGLAnnotation>//必须注释,否则obj无法获取其他协议中属性
                                                           _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj isMemberOfClass:[MGLAnnotationView class]]) {
            if ([obj respondsToSelector:@selector(floorID4Annotation)]) {
                int lFoorID =  [obj floorID4Annotation];
                MGLAnnotationView *lV = (MGLAnnotationView *)obj;
                lV.hidden = !(lFoorID == floorID);
            }else{
                //未遵守 HTMIndoorMapAnnotationViewAutoHide 协议,不管
            }
        }else{
            //不属于 MGLAnnotationView 类,不管
        }
    }];
}	

看起来似乎可行,但是(又来了哈),发现 mapbox 添加大头针的方法是这样的:

/**
 Adds an annotation to the map view.

 @note `MGLMultiPolyline`, `MGLMultiPolygon`, `MGLShapeCollection`, and
    `MGLPointCollection` objects cannot be added to the map view at this time.
    Any multipoint, multipolyline, multipolygon, shape or point collection
    object that is specified is silently ignored.

 @param annotation The annotation object to add to the receiver. This object
    must conform to the `MGLAnnotation` protocol. The map view retains the
    annotation object.

 #### Related examples
 See the <a href="https://docs.mapbox.com/ios/maps/examples/annotation-models/">
 Annotation models</a> and <a href="https://docs.mapbox.com/ios/maps/examples/line-geojson/">
 Add a line annotation from GeoJSON</a> examples to learn how to add an
 annotation to an `MGLMapView` object.
 */
- (void)addAnnotation:(id <MGLAnnotation>)annotation;

只能添加遵守了 MGLAnnotation 协议的类,而 MGLAnnotationView 恰好是没有遵守这个协议的,故不能通过上面方法添加!所以上面 for 循环的代码if ([obj isMemberOfClass:[MGLAnnotationView class]]) ,永远不会生效!

如果考虑把 MGLAnnotationView 对象作为子视图加入到 mapview 对象时,会涉及两个问题:

  • 无法通过 mapbox 提供的代理方法变更大头针的图标(不满足业务需求)

    /** If you want to mark a particular point annotation with a static image instead, omit this method or have it return nil for that annotation, then implement -mapView:imageForAnnotation: instead. */

    - (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id)annotation

  • 当地图子视图很多时,比较费性能

    Using many MGLAnnotationViews can cause slow performance, so if you need to add a large number of annotations, consider using more performant MGLStyleLayers instead, detailed below.

    Style layers are more performant compared to UIView-based annotations. You will need to implement your own gesture recognizers and callouts, but it is the most powerful option if you need to create rich map data visualizations within your app.

探索到这里时,偶然发现 mapbox 居然提供了新的教程:

https://docs.mapbox.com/ios/maps/guides/markers-and-annotations/#using-the-annotation-extension-beta

四种添加大头针的方法对比图:

截屏2021-03-01 下午4.24.26

效果示例图:

截屏2021-03-01 下午4.27.48

哇,MGLCircleStyleLayer的效果很炫酷哦!

根据教程,继续探索。

思路三

图层显隐法,根据不同楼层,创建对应的 MGLSymbolStyleLayer 图层(分类或子类新增一个楼层属性);在切换楼层时,对比楼层,控制图层显隐。
需要更改大头针时,重建楼层对应 MGLSymbolStyleLayer 图层(没找到通过数据源改变样式的方法)。

因想到了思路四,感觉能更快实现需求,故此思路暂未探索。

图层方法添加不可点击图片的方法

思路四

使用现有轮子:MapboxAnnotationExtension

The Mapbox Annotation Extension is a lightweight library you can use with the Mapbox Maps SDK for iOS to quickly add basic shapes, icons, and other annotations to a map.

This extension leverages the power of runtime styling with an object oriented approach to simplify the creation and styling of annotations.

⚠️ This product is currently in active beta development, is not intended for production usage. ⚠️

查了下库的记录,2019 年已经存在了,最近更新记录在 6 个月前,1年半了。而且看 issue 也没有什么大问题,已经比较稳定了。

首先了解此库的主要头文件,发现其有一个很关键的属性:

/**
 The opacity of the symbol style annotation's icon image. Requires `iconImageName`. Defaults to `1`.
 
 This property corresponds to the `icon-opacity` property in the style [Mapbox Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/#paint-symbol-icon-opacity).
 */
@property (nonatomic, assign) CGFloat iconOpacity;

这个属性意味着可以根据不同楼层去对大头针的图片进行显隐操作。

预感可行,探索过程如下。

集成

Create a Podfile with the following specification:

pod 'MapboxAnnotationExtension', '0.0.1-beta.2'

Run pod repo update && pod install and open the resulting Xcode workspace.

代码逻辑

新建自定义类
@interface HTMAutoVisibilityAnnotation : MGLSymbolStyleAnnotation
@property (nonatomic,assign) int floorIdInt;
@end
添加大头针管理控制器
@property (nonatomic,strong) MGLSymbolAnnotationController *annotationAutoVisibiliyCtrl;
增加设置大头针图片素材代理
/// 注册切换楼层时需要自动显隐的大头针信息。key 为图片名,value 为对应 UIImage* 对象。无需此功能时,返回 @{}
- (NSDictionary<NSString *,UIImage *> *)htmMapViewRegisterAnnoInfoOfAutoVisibilityWhenChangeFloor;
SDK内部创建大头针管理控制器
- (void)setAnnotationVC{
    MGLSymbolAnnotationController *lVC = [[MGLSymbolAnnotationController alloc] initWithMapView:self.mapView];
//    lVC.iconAllowsOverlap = YES;
    lVC.iconIgnoresPlacement = YES;
    lVC.annotationsInteractionEnabled = NO;
    
    //使图标不遮挡poi原图标
    lVC.iconTranslation = CGVectorMake(0, -26);
    self.annotationAutoVisibiliyCtrl = lVC;
    
    if ([self.delegateCustom respondsToSelector:@selector(htmMapViewRegisterAnnoInfoOfAutoVisibilityWhenChangeFloor)]) {
        NSDictionary<NSString *,UIImage *> *lDic = [self.delegateCustom htmMapViewRegisterAnnoInfoOfAutoVisibilityWhenChangeFloor];
        [lDic enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, UIImage * _Nonnull obj, BOOL * _Nonnull stop) {
            if (key.length > 0
                && nil != obj) {
                [self.mapView.style setImage:obj forName:key];
            }
        }];
    }
}
SDK内部增加大头针显隐判定
- (void)pmy_updateAnnotationsWithFloorId:(int)floorID {
    NSArray *lArr = self.annotationAutoVisibiliyCtrl.styleAnnotations;
    [lArr enumerateObjectsUsingBlock:^(MGLStyleAnnotation * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj isKindOfClass:[HTMAutoVisibilityAnnotation class]]) {
            HTMAutoVisibilityAnnotation *lAnno = (HTMAutoVisibilityAnnotation *)obj;
            if (lAnno.floorIdInt == floorID) {
                lAnno.iconOpacity = 1;
            }else{
                lAnno.iconOpacity = 0;
            }
        }
    }];
    
    //只有重新添加,图片透明度效果才生效
    [self.annotationAutoVisibiliyCtrl removeStyleAnnotations:lArr];
    [self.annotationAutoVisibiliyCtrl addStyleAnnotations:lArr];
}	
立刻显示与当前显示楼层相同楼层的大头针

效果仅限通过 annotationAutoVisibiliyCtrl 属性管理的 HTMAutoVisibilityAnnotation * 类型的大头针。

注意:自动或手动切换楼层时,会自动调用此方法。

- (void)showAnnotationsOfCurrentShownFloorImmediately{
    [self pmy_updateAnnotationsWithFloorId:self.floorModelMapShowing.floorID];
}

- (void)pmy_updateAnnotationsWithFloorId:(int)floorID {
    NSArray *lArr = self.annotationAutoVisibiliyCtrl.styleAnnotations;
    [lArr enumerateObjectsUsingBlock:^(MGLStyleAnnotation * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj isKindOfClass:[HTMAutoVisibilityAnnotation class]]) {
            HTMAutoVisibilityAnnotation *lAnno = (HTMAutoVisibilityAnnotation *)obj;
            if (lAnno.floorIdInt == floorID) {
                lAnno.iconOpacity = 1;
            }else{
                lAnno.iconOpacity = 0;
            }
        }
    }];
    
    //只有重新添加,图片透明度效果才生效
    [self.annotationAutoVisibiliyCtrl removeStyleAnnotations:lArr];
    [self.annotationAutoVisibiliyCtrl addStyleAnnotations:lArr];
}
Demo主控制器测试代码
- (void)pmy_upateSymbolAnnosWithPoisArr:(NSArray<HTMPoi *> *)poiArr{
    NSMutableArray *lArrM = @[].mutableCopy;
    [poiArr enumerateObjectsUsingBlock:^(HTMPoi *  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        HTMAutoVisibilityAnnotation *lAnno = [[HTMAutoVisibilityAnnotation alloc] initWithCoordinate:(CLLocationCoordinate2DMake(obj.lat, obj.lng)) iconImageName:@"poiAnno"];
        lAnno.iconOpacity = 0.5;
        lAnno.floorIdInt = obj.floorId.intValue;
        [lArrM addObject:lAnno];
    }];
    
    [self.indoorMapView.annotationAutoVisibiliyCtrl removeStyleAnnotations:self.indoorMapView.annotationAutoVisibiliyCtrl.styleAnnotations];
    [self.indoorMapView.annotationAutoVisibiliyCtrl addStyleAnnotations:lArrM];
    

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [SVProgressHUD showWithStatus:@"2s后只显示当前显示楼层大头针!"];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [SVProgressHUD dismiss];
            [self.indoorMapView showAnnotationsOfCurrentShownFloorImmediately];
        });
    });
}

实现新增代理方法(图片名对应图片记得添加到工程中):

- (nonnull NSDictionary<NSString *,UIImage *> *)htmMapViewRegisterAnnoInfoOfAutoVisibilityWhenChangeFloor {
    return @{@"route_icon_start": [UIImage imageNamed:@"route_icon_start"],
             @"route_icon_end": [UIImage imageNamed:@"route_icon_end"],
             @"poiAnno": [UIImage imageNamed:@"poiAnno"],
    };
}	
实测结果

运行工程,切换建筑选择器,确定大头针自动显隐效果可行!

搜索洗手间示例:

IMG_1072

IMG_1071

总结

遇到比较麻烦的需求时,第一时间应该是去查找文档,或是否已有现成的开源方案。如果一开始这样做,就能省下探索思路 1-2 所花费的时间了。

不过结果还是可以的,解决了同事烦扰已久搞不定的需求,也提升了对 mapbox 相关类的进一步理解。

posted @ 2021-03-02 09:15  Dast1  阅读(335)  评论(0编辑  收藏  举报