高德地图热力图的Flutter实现
高德地图SDK是有绘制热力图的功能的,但它的Flutter插件并没有封装这一块功能。我们业务中需要这个,就基于原插件修改了一下。
- Android部分
从AMapFlutterMapPlugin这个类开始,发现它是通过AMapPlatformViewFactory这个工厂类来创建View的。
进入AMapPlatformViewFactory的create方法,里面有很多形如
if (params.containsKey("xxx")) { builder.setInitialMarkers(params.get("yyy")); }
的代码,这是提取从Flutter侧传过来的参数。我们加入一句
if (params.containsKey("heatmapTile")){ builder.setHeatmapTile(params.get("heatmapTile")); }
heatmapTile是Flutter传过来的参数名,下面Flutter部分会提到。再到AMapOptionsBuilder中添加字段与相应的set方法
private Object heatmapTile; @Override public void setHeatmapTile(Object heatmapTile) { this.heatmapTile = heatmapTile; }
AMapOptionsBuilder这个类是实现了一个接口,上面这个setHeatmapTile是在接口中定义的,所以这里有个@Override。接口定义的更改这里就不再描述了。
继续深入到AMapOptionsBuilder的build方法中,工厂类是由这个方法创建的视图,在build方法中设置地图的属性,绘制点、线等。绘制热力图当然也在这个方法中,在return之前加入
if (null != heatmapTile){ aMapPlatformView.getHeatmapController().add(heatmapTile); }
当然在AMapPlatformView中加入
private HeatmapController heatmapController; public HeatmapController getHeatmapController() { return heatmapController; }
绘制热力图的方法就在这个HeatmapController类中,
View Codepublic class HeatmapController implements MyMethodCallHandler { private final AMap amap; private static final String CLASS_NAME = "HeatmapController"; public HeatmapController(AMap amap) { this.amap = amap; } public void add(Object mapTileObj){ if (mapTileObj != null && amap != null){ HeatmapOptionsBuilder builder = new HeatmapOptionsBuilder(); HeatmapUtil.parseHeatmapOptions(mapTileObj,builder); if (builder.getPointCount() > 0) { HeatmapTileProvider provider = builder.build(); TileOverlayOptions tileOverlayOptions = new TileOverlayOptions(); tileOverlayOptions.tileProvider(provider); amap.addTileOverlay(tileOverlayOptions); } } } @Override public String[] getRegisterMethodIdArray() { return Const.METHOD_ID_LIST_FOR_HEATMAP; } @Override public void doMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { LogUtil.i(CLASS_NAME, "doMethodCall===>" + call.method); switch (call.method) { case Const.METHOD_HEATMAP_UPDATE: Object tileData = call.argument("heatmapTile"); amap.clear(); add(tileData); result.success(null); break; } } }
它的构造函数有个参数,是个地图对象,在前面AMapPlatformView的构造函数中加入下面一句,
heatmapController = new HeatmapController(amap);在HeatmapController中,除了add方法外,其他二个方法是刷新地图时用的。Const.METHOD_ID_LIST_FOR_HEATMAP中现在只有一个值,就是在doMethodCall中的 Const.METHOD_HEATMAP_UPDATE,字面值是“heatmap#update”,下面iOS及Flutter部分也会出现这个值,大家保持一致即可。然后在 AMapPlatformView的 initMyMethodCallHandlerMap中加入
methodIdArray = heatmapController.getRegisterMethodIdArray(); if (null != methodIdArray && methodIdArray.length > 0) { for (String methodId : methodIdArray) { myMethodCallHandlerMap.put(methodId, heatmapController); } }
把方法名称跟执行对象的关系存起来。在AMapPlatformView的onMethodCall方法中,根据key调用相应对象的doMethodCall。
上面的HeatmapController类中还有二个辅助类,是用外部参数来构造HeatmapTileProvider用的,
View Codepublic class HeatmapOptionsBuilder { HeatmapTileProvider.Builder builder; private int _pointCount; public HeatmapOptionsBuilder() { this.builder = new HeatmapTileProvider.Builder(); } public HeatmapTileProvider build(){ return builder.build(); } public void setData(List<LatLng> data){ builder = builder.data(data); _pointCount = data.size(); } public int getPointCount(){ return _pointCount; } public void setGradient(Gradient gradient){ builder = builder.gradient(gradient); } public void setRadius(int radius){ builder = builder.radius(radius); } public void setTransparency(double transparency){ builder = builder.transparency(transparency); } public void setWeightedData(List<WeightedLatLng> data){ builder = builder.weightedData(data); } }
View Codepublic class HeatmapUtil { public static void parseHeatmapOptions(Object o,HeatmapOptionsBuilder builder){ if (null == o) { return; } final Map<?, ?> data = ConvertUtil.toMap(o); Object radius = data.get("radius"); if (radius != null){ builder.setRadius(ConvertUtil.toInt(radius)); } Object transparency = data.get("transparency"); if (transparency != null){ builder.setTransparency(ConvertUtil.toDouble(transparency)); } Object noWeightData = data.get("data"); if (noWeightData != null){ builder.setData(ConvertUtil.toPoints(noWeightData)); } } }
注意一下对应关系就好。
- iOS部分
这部分从下往上叙述。
首先定义结构,由传入的参数构造MAHeatMapTileOverlay对象
AMapHeatmap.h@interface AMapHeatmap : NSObject { /// 热力图的坐标点数组,key为@"points" CLLocationCoordinate2D *_coords;//坐标的数组指针 NSUInteger _coordCount;//坐标的个数 } ///透明度 @property (nonatomic, assign) double transparency; ///半径 @property (nonatomic, assign) int radius; /// 由以上数据生成的heatMapTileOverlay 对象 @property (nonatomic, strong,readonly) MAHeatMapTileOverlay *heatMapTileOverlay; @end
AMapHeatmap.m@interface AMapHeatmap () @property (nonatomic, strong, readwrite) MAHeatMapTileOverlay *heatMapTileOverlay; @end @implementation AMapHeatmap - (instancetype)init { self = [super init]; self.radius = 12; self.transparency = 0.6; return self; } - (void)postHookWith:(NSDictionary *)dict { NSArray *points = dict[@"data"]; //如果经纬度点已经有值,需要手动释放内存 if (_coords != NULL) { free(_coords); _coords = NULL; } _coordCount = points.count; if (_coordCount > 0){ _coords = (CLLocationCoordinate2D*)malloc(_coordCount * sizeof(CLLocationCoordinate2D)); for (NSUInteger index = 0; index < _coordCount; index ++) { NSArray *point = points[index]; _coords[index] = [AMapConvertUtil coordinateFromArray:point]; } } } - (void)dealloc { if (_coords != NULL) { free(_coords); _coords = NULL; } } - (MAHeatMapTileOverlay *)heatMapTileOverlay { if (_coordCount == 0) return nil; if (_heatMapTileOverlay == nil) { _heatMapTileOverlay = [[MAHeatMapTileOverlay alloc] init]; NSMutableArray* data = [NSMutableArray array]; for (NSUInteger index = 0; index < _coordCount; index++) { MAHeatMapNode *node = [[MAHeatMapNode alloc] init]; node.coordinate = _coords[index]; node.intensity = 1;//设置权重 [data addObject:node]; } _heatMapTileOverlay.data = data; _heatMapTileOverlay.radius = self.radius; _heatMapTileOverlay.opacity = self.transparency; // MAHeatMapGradient *gradient = [[MAHeatMapGradient alloc] initWithColor:@[[UIColor blueColor],[UIColor greenColor], [UIColor redColor]] andWithStartPoints:@[@(0.2),@(0.5),@(0.9)]]; // _heatMapTileOverlay.gradient = gradient; } return _heatMapTileOverlay; } @end
然后实现一个Controller,把热力图的Overlay加入到map中
AMapHeatmapController.h@class AMapHeatmap; @interface AMapHeatmapController : NSObject - (instancetype)init:(FlutterMethodChannel*)methodChannel mapView:(MAMapView*)mapView; - (void)addOverlay:(NSDictionary*)nodesToAdd; @end
AMapHeatmapController.m@interface AMapHeatmapController () @property (nonatomic,strong) FlutterMethodChannel *methodChannel; @property (nonatomic,strong) MAMapView *mapView; @end @implementation AMapHeatmapController - (instancetype)init:(FlutterMethodChannel*)methodChannel mapView:(MAMapView*)mapView { self = [super init]; if (self){ _methodChannel = methodChannel; _mapView = mapView; __weak typeof(self) weakSelf = self; [_methodChannel addMethodName:@"heatmap#update" withHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) { id heatmapToAdd = call.arguments[@"heatmapTile"]; if ([heatmapToAdd isKindOfClass:[NSDictionary class]]) { [weakSelf.mapView removeOverlays:weakSelf.mapView.overlays]; [weakSelf addOverlay:heatmapToAdd]; } result(nil); }]; } return self; } - (void)addOverlay:(NSDictionary*)nodesToAdd { AMapHeatmap *heatmapModel = [AMapJsonUtils modelFromDict:nodesToAdd modelClass:[AMapHeatmap class]]; if (heatmapModel.heatMapTileOverlay == nil) return; [self.mapView addOverlay:heatmapModel.heatMapTileOverlay]; } @end
在Controller中,除了add,同样有涉及刷新的方法。
最后改造一下AMapViewController,先在@interface中加入
@property (nonatomic,strong) AMapHeatmapController *heatmapController;
在initWithFrame方法中,调用addOverlay
_heatmapController = [[AMapHeatmapController alloc] init:_channel mapView: _mapView]; id heatmapToAdd = args[@"heatmapTile"]; if ([heatmapToAdd isKindOfClass:[NSDictionary class]]){ [_heatmapController addOverlay:heatmapToAdd]; }
在rendererForOverlay方法加一个分支
else if ([overlay isKindOfClass:[MATileOverlay class]]) { MATileOverlayRenderer *tileOverlayRenderer = [[MATileOverlayRenderer alloc] initWithTileOverlay:overlay]; return tileOverlayRenderer; }
- Flutter部分
先定义widget中用来传递热力图数据的结构,有三个字段,分别表示数据点的数组、半径、透明度。主要有一个把类对象转化为map的方法
class HeatmapTile extends BaseOverlay { final List<LatLng> data; final int? radius; final double? transparency; HeatmapTile({required this.data, this.radius, this.transparency}); @override Map<String, dynamic> toMap() { final Map<String, dynamic> json = <String, dynamic>{}; void addIfPresent(String fieldName, dynamic value) { if (value != null) { json[fieldName] = value; } } json['data'] = _pointsToJson(); addIfPresent('radius', radius); addIfPresent('transparency', transparency); return json; } dynamic _pointsToJson() { final List<dynamic> result = <dynamic>[]; for (final LatLng point in data) { result.add(point.toJson()); } return result; } }
在AMapWidget这个高德地图的widget中加入
///热力图 final HeatmapTile? heatmapTile;
更新一下构造函数。使用时候把热力图数据给widget的这个heatmapTile参数就行。
如果只是这样,当组件刷新的时候,热力图就没了,所以还需要在_MapState的didUpdateWidget方法中添加
_updateHeatmap();
_updateHeatmap方法如下
void _updateHeatmap() async { final AMapController controller = await _controller.future; var tile = widget.heatmapTile; if (tile != null){ controller._updateHeatmap(tile); } }
当然还要在AMapController中添加
Future<void> _updateHeatmap(HeatmapTile tile){ return _methodChannel.updateHeatmap(tile, mapId: mapId); }
通过methodChannel调用原生的方法,在MethodChannelAMapFlutterMap中添加更新的方法
Future<void> updateHeatmap( HeatmapTile tile, { required int mapId, }) { return channel(mapId).invokeMethod<void>( 'heatmap#update', { 'heatmapTile': tile.toMap() }, ); }
这样就完成了高德地图Flutter插件的改造,
浙公网安备 33010602011771号