大頭針顯隱跟隨樓層功能探索

背景

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="//docs.mapbox.com/ios/maps/examples/annotation-models/">
 Annotation models</a> and <a href="//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 居然提供了新的教程:

//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](//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 相關類的進一步理解。

Tags: