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

背景

mapbox 提供的大頭針默認沒有樓層相關屬性,沒法實現切換樓層時,只顯示對應樓層的大頭針效果。客戶端同事沒法解決此問題,但願我在 SDK 端解決此問題,故進行相關探索(🤷‍♀️)。因爲有段時間沒有作地圖 SDK 開發了,故進行了以下各類踩坑嘗試。ios

嘗試思路

在 mapbox 提供的原有類和方法基礎上實現;
儘量不影響客戶端已使用的 mapbox 原有大頭針 api 相關代碼。編程

思路一

思路來源:面向協議編程!json

若是可以新增一個協議,使 mapbox 原大頭針相關類遵照此協議,而後實現樓層屬性,在使用時對樓層屬性賦值,在 SDK 內部進行邏輯斷定,就實現功能就行了!swift

想到這,不由感慨,不愧是我!😆api

因而進行了以下嘗試:數組

新增帶樓層屬性(floorID4Annotation )的協議:緩存

//MARK:protocol
@protocol HTMIndoorMapAnnotationViewAutoHide <NSObject>

/// 大頭針所在樓層id
@property (nonatomic, assign) int floorID4Annotation;

@end

讓須要顯隱的大頭針的類遵照協議,實現樓層屬性(@synthesize floorID4Annotation = _floorID4Annotation;)。eg:app

@interface HTMCustomPointAnnotation : MGLPointAnnotation<HTMIndoorMapAnnotationViewAutoHide>
@end

@implementation HTMCustomPointAnnotation
@synthesize floorID4Annotation = _floorID4Annotation;
@end

使用時,對樓層屬性賦值。而後在切換樓層的相關方法裏遍歷地圖對象大頭針數組,斷定大頭針對象是否響應 floorID4Annotation 方法,對於響應的對象,對比它的樓層屬性和當前顯示樓層是否一致,不一致則隱藏,一致則顯示。相關代碼:ide

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

相關文章
相關標籤/搜索