Net有道

紫冠道人的求道历程

导航

高德地图热力图的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类中,

    public 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;
            }
        }
    }
    View Code

    它的构造函数有个参数,是个地图对象,在前面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用的,

    public 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 Code
    public 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));
            }
    
        }
    }
    View Code

    注意一下对应关系就好。

     

  • iOS部分

     这部分从下往上叙述。

     首先定义结构,由传入的参数构造MAHeatMapTileOverlay对象

    @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.h
    @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
    AMapHeatmap.m

    然后实现一个Controller,把热力图的Overlay加入到map中

    @class AMapHeatmap;
    
    @interface AMapHeatmapController : NSObject
    
    - (instancetype)init:(FlutterMethodChannel*)methodChannel
                mapView:(MAMapView*)mapView;
    
    - (void)addOverlay:(NSDictionary*)nodesToAdd;
    
    @end
    AMapHeatmapController.h
    @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
    AMapHeatmapController.m

    在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插件的改造,

 

posted on 2025-09-18 14:12  lichdr  阅读(79)  评论(0)    收藏  举报