使用本插件,开发者可方便地在其腾讯云 IM Flutter 项目中,引入发送接收展示位置消息能力。本插件提供三个组件:
说明:
上述插件提供为其配套业务逻辑能力,除与地图底层 SDK 交互部分外。
为方便开发者快速接入,我们还提供基于百度地图的完整 Example 代码。具体请查看腾讯云 IM Flutter 或本 Plugin 的 Example。
术语介绍
POI: Point of interesting。即地图上任何非地理意义的有意义的点,每个 POI 均有特定坐标/名称/各级地址/ID。 例如:深圳腾讯大厦,蛇口太子广场。
整体流程
- 选定底层地图 SDK。
- 继承三个抽象类,完成本插件业务代码与地图 SDK 间交互。
- 将继承后的抽象类实例化,传入本插件提供的三个组件中。
选定底层地图 SDK
本插件大部分业务逻辑接口基于百度地图设计,选用百度地图可较快完成项目。但不限制具体使用哪一块地图 SDK,常用地图 Flutter SDK 如下:
开发接入
数据交互数据结构
本插件使用一系列数据结构来定义 SDK 与插件间传递的信息,包含如下:
class LocationMessage {
  final String desc;
  final double longitude;
  final double latitude;
}
/// 代表经纬度
class TIMCoordinate implements TIMLocationBaseModel {
  /// 纬度
  late double latitude;
  /// 经度
  late double longitude;
  Map<String, Object> toMap();
  fromMap(Map map);
}
/// POI信息类
class TIMPoiInfo implements TIMLocationBaseModel {
  /// POI名称
  String? name;
  /// POI坐标
  TIMCoordinate? pt;
  /// POI地址信息
  String? address;
  /// POI唯一标识符uid
  String? uid;
  /// POI所在省份
  String? province;
  /// POI所在城市
  String? city;
  fromMap(Map map);
  Map<String, Object?> toMap();
}
/// 根据地理坐标反向查询结果类
class TIMReverseGeoCodeSearchResult implements TIMLocationBaseModel {
  /// 地址坐标
  TIMCoordinate? location;
  /// 地址名称
  String? address;
  /// 层次化地址信息
  TIMAddressComponent? addressDetail;
  /// 地址周边POI信息,成员类型为BMKPoiInfo
  List<TIMPoiInfo>? poiList;
  /// 结合当前位置POI的语义化结果描述, 用于地址名称字段。例如"腾讯大厦内,招行信息研发大厦附近18米"。
  String? semanticDescription;
  fromMap(Map map);
  Map<String, Object?> toMap();
}
/// 地址结果的层次化信息
class TIMAddressComponent implements TIMLocationBaseModel {
  /// 国家
  String? country;
  /// 省份名称
  String? province;
  /// 城市名称
  String? city;
  /// 区县名称
  String? district;
  /// 乡镇
  String? town;
  fromMap(Map map);
  Map<String, Object?> toMap()
}
/// 枚举:地图区域改变原因
enum TIMRegionChangeReason {
  ///<手势触发导致地图区域变化,如双击、拖拽、滑动地图
  Gesture,
  ///<地图上控件事件,如单击指南针返回2D地图。
  Event,
  ///<开发者调用接口、设置地图参数等导致地图区域变化
  APIs,
}
/// 用于作为外部导航软件的App信息
class NavigationMapItem{
  /// APP 名称
  final String name;
  /// 唤起外部导航APP的方法
  final Function(double longitude, double latitude) jumpFunc;
  NavigationMapItem(this.name, this.jumpFunc);
}
继承抽象类,连接地图 SDK 与插件业务逻辑
若选用百度地图,可直接使用我们的完整 Example 代码,快速完成项目。请参加 详细指南。 请根据选定的地图 SDK,继承以下三个类:
TIMMapService
地图定位及 POI 搜索能力 Service。需要根据地图 SDK 完成交互并将数据提供给插件业务代码。
  /// 【可选】仅当您需要使用地图SDK提供的定位能力,才需要继承本方法。开关:LocationPicker/LocationShow的isUseMapSDKLocation字段。
  /// 需做到根据地图SDK提供的定位能力定位再再通过'moveMapCenter(coordinate)'将地图挪过去,
  /// 并返回含根据新的地图中心查询附近POI的结果及是否出错参数的方法
  void moveToCurrentLocationActionWithSearchPOIByMapSDK({
    required void Function(TIMCoordinate coordinate) moveMapCenter,
    void Function(TIMReverseGeoCodeSearchResult, bool)?
    onGetReverseGeoCodeSearchResult,
  });
  /// 根据关键词搜索POI,优先返回在当前city内的结果,但同时也可以搜到其他city的POI
  void poiCitySearch({
    required void Function(List<TIMPoiInfo>?, bool)
    onGetPoiCitySearchResult,
    required String keyword,
    required String city,
  });
  /// 根据地理坐标查询附近的POI,并返回包含TIMReverseGeoCodeSearchResult及是否报错参数的方法
  void searchPOIByCoordinate(
      {required TIMCoordinate coordinate,
        required void Function(TIMReverseGeoCodeSearchResult, bool)
        onGetReverseGeoCodeSearchResult});
TIMMapWidget
渲染地图的基类,对外暴露地图事件。是 StatefulWidget,需要继承 TIMMapState。 包含地图加载完成回调及地图拖动结束回调。
  final Function? onMapLoadDone;
  final Function(TIMCoordinate? targetGeoPt, TIMRegionChangeReason regionChangeReason)? onMapMoveEnd;
TIMMapState
渲染地图基类的State,提供完成一系列地图交互能力,并对外返回可直接使用的地图实例。
  /// 地图创建完成回调
  void onMapLoadDone(){}
  /// 地图移动结束
  void onMapMoveEnd(TIMCoordinate? targetGeoPt, TIMRegionChangeReason regionChangeReason){}
  /// 移动地图视角
  void moveMapCenter(TIMCoordinate pt){}
  /// 禁用地图交互
  void forbiddenMapFromInteract() {}
  /// 在地图上添加图钉
  void addMarkOnMap(TIMCoordinate pt, String title){}
/// 此处实例化地图
  @override
  Widget build(BuildContext context) {
    return Container(
      child: 某地图的Widget(
        onMapCreated: onMapCreated,
        mapOptions: initMapOptions(),
      ),
    );
  }
使用组件
位置选择器(LocationPicker)
该组件以类似微信的位置选择器页面呈现,允许用户定位当前位置/地图选点/搜索特定 POI /展示选定 POI 周边其他 POI。
  /// 地理位置选择完成后的onChange事件,返回一个LocationMessage,可用于发送消息。
  /// 【特别说明】由于腾讯云IM位置消息仅支持传递一个desc字符串,因此此处的LocationMessage.desc将名称及地址拼接传递,格式:"腾讯大厦/////深圳市南山区深南大道10000号"。
  /// 该拼接格式可被本插件所需地方解析,请放心使用。
  final ValueChanged<LocationMessage> onChange;
  /// 传入根据选定地图SDK实例化后的LocationUtils
  final LocationUtils locationUtils;
  /// 用于还未加载出来定位时,打开页面后,默认的中心点。
  final TIMCoordinate? initCoordinate;
  /// 用于控制是否使用地图SDK定位能力。若使用,请确保moveToCurrentLocationActionWithSearchPOIByMapSDK方法继承正确。
  final bool? isUseMapSDKLocation;
  /// 传入根据选定地图SDK实例化后的地图组件TIMMapWidget
  final TIMMapWidget Function(
      VoidCallback onMapLoadDone,
      Key mapKey,
      Function(TIMCoordinate? targetGeoPt,
              TIMRegionChangeReason regionChangeReason)
          onMapMoveEnd) mapBuilder;
位置消息完整展示器(LocationShow)
该组件以类似微信的位置详情展示页面呈现,使用大地图配上图标,展示接收到的位置。底部显示位置名称/地址,及拉起跳转导航软件的按钮。
  /// 位置名称标题
  final String addressName;
  /// 位置地址
  final String? addressLocation;
  /// 纬度
  final double latitude;
  /// 经度
  final double longitude;
  /// 传入根据选定地图SDK实例化后的LocationUtils
  final LocationUtils locationUtils;
  /// 用于控制是否使用地图SDK定位能力。若使用,请确保moveToCurrentLocationActionWithSearchPOIByMapSDK方法继承正确。
  final bool? isUseMapSDKLocation;
  /// 第三方导航APP列表,如果没传,则默认腾讯/百度/高德/苹果地图。
  final List<NavigationMapItem>? navigationMapList;
  /// 传入根据选定地图SDK实例化后的地图组件TIMMapWidget
  final TIMMapWidget Function(
      VoidCallback onMapLoadDone,
      Key mapKey) mapBuilder;
位置消息列表展示器(LocationMsgElement)
此组件用于在历史消息列表中,展示位置消息。效果类似微信。提供小地图展示/位置名称及地址展示。
  /// 消息ID
  final String? messageID;
  /// V2TimLocationElem消息
  final V2TimLocationElem locationElem;
  /// 是否自己发送
  final bool isFromSelf;
  /// 是否显示被跳转样式
  final bool? isShowJump;
  /// 清除跳转方法
  final VoidCallback? clearJump;
  /// 传入根据选定地图SDK实例化后的LocationUtils
  final LocationUtils locationUtils;
  /// 传入根据选定地图SDK实例化后的地图组件TIMMapWidget
  final TIMMapWidget Function(VoidCallback onMapLoadDone, Key mapKey)
      mapBuilder;
配合 TUIKit 使用接入
此部分完整代码可在 IM Flutter Demo 中得到。
历史消息列表渲染小地图(LocationMsgElement)
请在 TIMUIKitChat 中新增如下字段。该小地图可以自动单击跳转至完整版详细地图(LocationShow)。 TIMMapWidget 和 TIMMapService 需要替换为自己的实例化对象。
        locationMessageItemBuilder:
          (locationElem, isFromSelf, isShowJump, clearJump, messageID) =>
          LocationMsgElement(
            messageID: messageID,
            locationElem: locationElem,
            isFromSelf: isFromSelf,
            isShowJump: isShowJump,
            clearJump: clearJump,
            mapBuilder: (onMapLoadDone, mapKey) => TIMMapWidget(
              onMapLoadDone: onMapLoadDone,
              key: mapKey,
            ),
            locationUtils: LocationUtils(TIMMapService()),
          ),
加号面板增加位置 item 并跳转至位置选择器(LocationMsgElement)
请在 TIMUIKitChat 中新增如下字段。该 extraAction 可跳转至位置选择器,并发送消息。 TIMMapWidget 和 TIMMapService 需要替换为自己的实例化对象。
      morePanelConfig: MorePanelConfig(
        extraAction: [
          MorePanelItem(
              id: "location",
              title: ("位置"),
              onTap: (c) {
                  Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => LocationPicker(
            onChange: (LocationMessage location) async {
              // 此处消息发送逻辑需要根据业务框架适当修改
              final locationMessageInfo = await sdkInstance.v2TIMMessageManager.createLocationMessage(
                  desc: location.desc, longitude: location.longitude, latitude: location.latitude);
              final messageInfo = locationMessageInfo.data!.messageInfo;
              _timuiKitChatController.sendMessage(
                  convID: _getConvID()!,
                  convType: _getConvType(),
                  messageInfo: messageInfo
              );
            },
            mapBuilder: (onMapLoadDone, mapKey, onMapMoveEnd) => TIMMapWidget(
              onMapMoveEnd: onMapMoveEnd,
              onMapLoadDone: onMapLoadDone,
              key: mapKey,
            ),
            locationUtils: LocationUtils(TIMMapService()),
          ),
        ));
              },
              icon: Container(
                height: 64,
                width: 64,
                margin: const EdgeInsets.only(bottom: 4),
                decoration: const BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.all(Radius.circular(5))),
                child: Icon(
                  Icons.location_on,
                  color: hexToColor("5c6168"),
                  size: 32,
                ),
              ))
        ],
      ),