nb_map

这是一款基于国内高德地图的插件, 与国外的谷歌地图和谷歌官方推出的 Atlas地图抽象框架有很好的兼容性,契合度高达百分99%以上, 基本上除了修改插件入口和视图注册Id其它方法无需变更. 插件对 地图的基本设置,遮盖物的添加,修改,和删除, 以及地图可视区域的移动 做了统一的封装和抽象处理. 基于此基础可以定制化更多的功能。

Getting Started

This project is a starting point for a Flutter plug-in package, a specialized package that includes platform-specific implementation code for Android and/or iOS.

For help getting started with Flutter, view our online documentation, which offers tutorials, samples, guidance on mobile development, and a full API reference.

Flutter端实现方式

  1. dart端采用goole flutter官方提供的接口,对国类高德地图的接口实现进行封装和适配.
  2. flutter与native端的交互分为以下四类
  • MKMapView: map#mapOptions: args
  • Circle: circle#circleOptions: args
  • Polygon: polygon#ploygonOptions: args
  • Marker: marker#markerOptoons: args (对应的annotation,为了与安卓代码保持统一)
  • Polyline: polyline#polylineOptions: args

iOS实现

  1. 主要实现类:
├── NBCommonUtility.h        //负责坐标计算
├── NBCommonUtility.m
├── NBJsonConversions.h      //负责基本数据类型的转换
├── NBJsonConversions.m
├── NBMapCircleController.h  //负责原型视图与Flutter端的交互
├── NBMapCircleController.m
├── NBMapController.h        //负责地图大头针视图与Flutter端的交互
├── NBMapController.m
├── NBMapMarkerController.h  //负责控制大头针视图与Flutter端的交互
├── NBMapMarkerController.m
├── NBMapPolygonController.h //负责控制多边形视图与Flutter端的交互
├── NBMapPolygonController.m
├── NBMapPolylineController.h //负责控制折线视图与Flutter端的交互
├── NBMapPolylineController.m
├── MAMapView+CenterAroundVisiableOverlays.h
├── MAMapView+CenterAroundVisiableOverlays.m
├── NBRendersFactory.h        //遮盖物工厂类,定制各类型的OverlayRender
├── NBRendersFactory.m
├── NBMapMarkerController.h   //遮盖物工厂类,定制各类型的Mark试图
├── NBMapMarkerController.m
├── NbMapPlugin.h             //插件入口,注册地图的构造方法
└── NbMapPlugin.m  
  1. 方法命名,统一参照 [object]#[method] args
  • 其中object为需要操作的对象名称,驼峰法命名
  • #为对应和方法名的连接符号
  • method为方法方法名
  • args 为传入的具体参数,可以是基本的数据类型,也可以 是固定的json,由native端执行的方法确定
  1. 视图的交互
  • 每个视图对应唯一的viewId,和一个controller
  • 除了MAMapView外每个子视图的controller分别实现对应的add/change/delete/onTap事件来更新界面UI和响应界面的点击事件
  • Marker视图需额外实现onDragger事件
  • 每个视图的controller管它所持有的子视图的controllers,子视图的controller通过持有它的父视图MapView可以对子视图Overlay进行add/update/delete/select事件
  1. Overlay的刷新
  • 为了避免重复绘制,添加前需要对overlay的进行去重操作
  • 对overlay的操作以最小单元执行,通过overlay对象获取对应的render,通过修改render持有的overlay对象然后标记renderdirty状态等待下次刷新.
MACircle *circle = (MACircle *)self.circles.firstObject;
MACircleRenderer *circleRender = (MACircleRenderer *)[self.mapView rendererForOverlay:circle];

if(circleRender) {
circleRender.circle.coordinate = CLLocationCoordinate2DMake(circleRender.circle.coordinate.latitude + 0.01, circleRender.circle.coordinate.longitude + 0.01);
circleRender.circle.radius += 1000;

[circleRender setNeedsUpdate];
}
  1. 关于视图层级的调整
  • 目前增对视图层级调整先删除该overlay,然后再进行添加.
  1. 关于Google Flutter api兼容性问题
  • Cameramove/idle/moveEnd事件需要寻找替代方法,结合项目实际情况.备选方法regionChanged/``
  • OverlayzIndex事件修改,目前高德提供的接口只能先删除以后再进行添加
  • OverlayonTap/onDragger事件是否enable需要子类化overlay设定.暂定默认都支持,有需要再添加
  • Overlayvisiable属性,高德地图无此方法,可以通过将MAOverlayRendereralpha设置为0替代。
  1. 关于OverlayRenderer的点击事件判定
  • OverlayRenderer继承于NSObject,主要负责将对应的overlay数据模型转换后提交给OpenGL进行渲染,所以它是无法直接响应点击事件的,只能简介通过它的渲染区域的坐标与地图点击的坐标进行判定点击的点位是否落于该点位.
  1. 关于AnnotaionView的Zindex设定
  • MAAnnotationViewzIndex默认为0,大值在上,只有当调用viewForAnnotaion或者didAddAnnotationViews回调设置中才会生效。
  • 以上两个方法都只有第一次设定大头针才会触发。所以只有初始化才能设置zIndex
  1. 关于添加或更新完大头针和遮盖线后地图区域移动问题
  • 由于地图显示区域频繁移动会导致用户看的地图变化频繁,导致很多无效的渲染操作,为解决这个问题,将所有添加的视图根据 visiable属性做统一的cache处理。
  • 具体操作如下:
      • 增加MAMapView+CenterAroundVisiableOverlays.h类,专门用于处理所有可视化视图 add/delete/update之后的地图区域重绘操作.
      • 在所有遮盖物add/remove/visiable change的地图更新当前visiableOverlays & visableMarkers的缓存. 并对缓存进行标记是否需要更新
      • MapViewController中,当接收到MethodChannel事件中遮盖线 Add/update/delete操作时,根据缓存中的标记重新定位当前视图的可视化区域.
  1. 关于Asset资源读取
  • 插件提供了三种方式:
  • 通过package中获取
  • 从当前工程Main Bundle目录的Asset中获取
  • 直接通过流获取
  • 使用方法: lookupKeyForAsset:fromPackage:
  1. 关于地图生命周期内涉及到的几个关键方法如下:
  • 地图初次加载顺序如下:
-[AnnotationViewController mapView:regionWillChangeAnimated:]
-[AnnotationViewController mapView:mapWillZoomByUser:]
-[AnnotationViewController mapView:mapWillMoveByUser:]
-[AnnotationViewController mapViewRegionChanged:]
-[AnnotationViewController mapView:regionDidChangeAnimated:]
-[AnnotationViewController mapView:mapDidZoomByUser:]
-[AnnotationViewController mapView:mapDidMoveByUser:]
-[AnnotationViewController mapViewWillStartLoadingMap:]
-[AnnotationViewController mapViewDidFinishLoadingMap:]
  • 移动地图时触发回调方法顺序如下:
-[AnnotationViewController mapView:regionWillChangeAnimated:]
-[AnnotationViewController mapView:mapWillMoveByUser:]
-[AnnotationViewController mapViewRegionChanged:]
-[AnnotationViewController mapViewRegionChanged:]
-[AnnotationViewController mapViewRegionChanged:]
-[AnnotationViewController mapView:regionDidChangeAnimated:]
-[AnnotationViewController mapView:mapDidMoveByUser:]
  • 缩放地图的时触发回调方法顺序如下:
-[AnnotationViewController mapView:regionWillChangeAnimated:]
-[AnnotationViewController mapView:mapWillZoomByUser:]
-[AnnotationViewController mapViewRegionChanged:]
-[AnnotationViewController mapViewRegionChanged:]
-[AnnotationViewController mapViewRegionChanged:]
-[AnnotationViewController mapViewWillStartLoadingMap:]
-[AnnotationViewController mapViewDidFinishLoadingMap:]

小结: 地图加载顺序如下

  1. region update事件首先被触发.

  2. userAction/mapViewDefaultAction事件触发.

  3. loading, 此过程不是必须的,没有屏幕范围没有超过地图的缓存范围则不会执行.

  4. 多个地图实例同时与原生交互

  • 修改Flutter端MapController的MethodChannel渠道Id和Native端MapController的渠道Id一样
  • App可能同时存在多个视图,所以这里需要根据每次创建的MapView视图id生成对应MapView实例对象的MethodChannel Id。
  • 修改PlatformView中的viewType和 Plugin注册入口的id. 参考如下设置:
 UiKitView(
 viewType: 'plugins.flutter.io/nb_maps',
 onPlatformViewCreated: onPlatformViewCreated,
 gestureRecognizers: widget.gestureRecognizers,
 creationParams: creationParams,
 creationParamsCodec: const StandardMessageCodec(),
 );
 + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
 NBMapFactory* nbMapFactory = [[NBMapFactory alloc] initWithRegistrar:registrar];
 _registrar = registrar;
 [registrar registerViewFactory:nbMapFactory withId:@"plugins.flutter.io/nb_maps"];
 
 }
 //NBMapViewController line 18
 [NSString stringWithFormat:@"plugins.flutter.io/nb_maps_%lld", viewId];
 _channel = [FlutterMethodChannel methodChannelWithName:channelName
 binaryMessenger:registrar.messenger];

总结:

  • 指定地图方法时需要参照MapView生命周期的回调,在对应的的回调和代理方法中做正确的事
  • 注意插件的接口的一致性和命名规则统一性
  • 避免Overlay和marker的冗余操作

iOS issues

  1. 关于参数类异常导致崩溃的问题, 在onMethodCall中打印,method.args,所有传入参数接口参数需要对NSNull和nil进行严格判定。
  2. 关于添加大头针对象,无法触发地图的代理方法问题, 添加完大头针后需将地图的可视范围移动到大头针所在的区域才能触发回调事件。
  3. 关于设置地图可视区域MapViewVisiableRect崩溃问题, 为了避人为的传入错误经纬度以及坐标系转换不统一问题,统一先将坐标转换为 MapView的经纬度坐标系,然后再进行设置.
MACoordinateBounds bounds = ToBounds(data[1]);
MAMapRect visiableRect = MAMapRectForCoordinateRegion(BoundsToRegion(bounds));
CGFloat pd = ToFloat(data[2]);
UIEdgeInsets inset = UIEdgeInsetsMake(pd, pd, pd, pd);
[_mapView setVisibleMapRect:visiableRect edgePadding:inset animated:animate];

Android部分实现

“整体上均参照了google map的实现,对google api的camera进行了兼容。”

  1. 主要实现类:
├── AmapFactory.java
├── AmapMapBuilder.java
├── AmapMapController.java
├── AmapMapOptionsSink.java
├── CircleBuilder.java
├── CircleController.java
├── CircleOptionsSink.java
├── CirclesController.java
├── Convert.java
├── MapPlugin.java
├── MarkerBuilder.java
├── MarkerController.java
├── MarkerOptionsSink.java
├── MarkersController.java
├── PolygonBuilder.java
├── PolygonController.java
├── PolygonOptionsSink.java
├── PolygonsController.java
├── PolylineBuilder.java
├── PolylineController.java
├── PolylineOptionsSink.java
└── PolylinesController.java

Libraries

nb_map