Pārlūkot izejas kodu

Merge branch 'v1.1.0' of git.atmob.com:Atmob-Flutter/location2025 into v1.1.0

“HeShaoZe” 6 mēneši atpakaļ
vecāks
revīzija
106f4e3195
40 mainītis faili ar 1227 papildinājumiem un 680 dzēšanām
  1. 8 0
      lib/data/bean/track_daily_bean.dart
  2. 4 0
      lib/data/bean/track_daily_bean.g.dart
  3. 5 0
      lib/data/consts/constants.dart
  4. 8 8
      lib/di/get_it.config.dart
  5. 16 3
      lib/module/main/main_controller.dart
  6. 95 72
      lib/module/main/main_page.dart
  7. 118 253
      lib/module/track/track_controller.dart
  8. 115 94
      lib/module/track/track_day_detail/track_daily_item.dart
  9. 144 8
      lib/module/track/track_day_detail/track_day_detail_controller.dart
  10. 8 5
      lib/module/track/track_day_detail/track_day_detail_view.dart
  11. 36 184
      lib/module/track/track_page.dart
  12. 28 0
      lib/module/track/track_util.dart
  13. 59 0
      lib/utils/async_util.dart
  14. 3 1
      plugins/map/lib/flutter_map.dart
  15. 4 0
      plugins/map/lib/src/consts/map_constants.dart
  16. 4 1
      plugins/map/lib/src/consts/marker_type.dart
  17. 9 0
      plugins/map/lib/src/consts/polyline_type.dart
  18. 61 4
      plugins/map/lib/src/core/map_controller.dart
  19. 6 1
      plugins/map/lib/src/entity/marker.dart
  20. 27 0
      plugins/map/lib/src/entity/popup.dart
  21. 3 0
      plugins/map/lib/src/interface/map_fun_interface.dart
  22. 3 0
      plugins/map/lib/src/interface/map_marker_interface.dart
  23. 6 2
      plugins/map/lib/src/interface/map_polyline_interface.dart
  24. 90 1
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/amap/MapController.java
  25. 15 2
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/bean/MakerInfo.java
  26. 56 0
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/bean/Popup.java
  27. 6 5
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/contants/Constants.java
  28. 86 31
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/overlays/marker/MarkersController.java
  29. 44 5
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/overlays/polyline/PolylineController.java
  30. 15 0
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/overlays/polyline/PolylineType.java
  31. BIN
      plugins/map_amap_android/android/src/main/res/drawable-xxhdpi/bg_map_error_line.webp
  32. BIN
      plugins/map_amap_android/android/src/main/res/drawable-xxhdpi/bg_map_selected_line.webp
  33. BIN
      plugins/map_amap_android/android/src/main/res/drawable-xxhdpi/bg_track_popup_bubble.9.png
  34. BIN
      plugins/map_amap_android/android/src/main/res/drawable-xxhdpi/icon_track_stay.webp
  35. 6 0
      plugins/map_amap_android/android/src/main/res/drawable/bg_track_error_point.xml
  36. 9 0
      plugins/map_amap_android/android/src/main/res/drawable/icon_marker_error.xml
  37. 9 0
      plugins/map_amap_android/android/src/main/res/drawable/icon_marker_passing.xml
  38. 35 0
      plugins/map_amap_android/android/src/main/res/layout/item_track_error_marker.xml
  39. 15 0
      plugins/map_amap_android/android/src/main/res/layout/item_track_passing_marker.xml
  40. 71 0
      plugins/map_amap_android/android/src/main/res/layout/item_track_selected_marker.xml

+ 8 - 0
lib/data/bean/track_daily_bean.dart

@@ -22,6 +22,12 @@ class TrackDailyBean {
   @JsonKey(name: 'status')
   int status;
 
+  @JsonKey(name: 'lng')
+  double? lng;
+
+  @JsonKey(name: 'lat')
+  double? lat;
+
   @JsonKey(name: 'totalUnlock')
   int? totalUnlock;
 
@@ -34,6 +40,8 @@ class TrackDailyBean {
     required this.end,
     required this.duration,
     this.network,
+    this.lat,
+    this.lng,
     required this.status,
     this.totalUnlock,
     this.highUnlock,

+ 4 - 0
lib/data/bean/track_daily_bean.g.dart

@@ -13,6 +13,8 @@ TrackDailyBean _$TrackDailyBeanFromJson(Map<String, dynamic> json) =>
       end: (json['end'] as num).toInt(),
       duration: (json['duration'] as num).toInt(),
       network: json['network'] as String?,
+      lat: (json['lat'] as num?)?.toDouble(),
+      lng: (json['lng'] as num?)?.toDouble(),
       status: (json['status'] as num).toInt(),
       totalUnlock: (json['totalUnlock'] as num?)?.toInt(),
       highUnlock: (json['highUnlock'] as num?)?.toInt(),
@@ -26,6 +28,8 @@ Map<String, dynamic> _$TrackDailyBeanToJson(TrackDailyBean instance) =>
       'duration': instance.duration,
       'network': instance.network,
       'status': instance.status,
+      'lng': instance.lng,
+      'lat': instance.lat,
       'totalUnlock': instance.totalUnlock,
       'highUnlock': instance.highUnlock,
     };

+ 5 - 0
lib/data/consts/constants.dart

@@ -39,6 +39,11 @@ class Constants {
   static const String mineLocationId = '';
   static const String traceStartId = '-100';
   static const String traceEndId = '-200';
+  static const String tracePopupId = '-300';
+  static const String traceErrorId = '-400';
+
+  static const String traceNormalLineId = '-1000';
+  static const String traceSelectLineId = '-2000';
 
   static const double blurredX = 4.2;
   static const double blurredY = 4.2;

+ 8 - 8
lib/di/get_it.config.dart

@@ -57,14 +57,14 @@ extension GetItInjectableX on _i174.GetIt {
       environmentFilter,
     );
     final networkModule = _$NetworkModule();
-    gh.factory<_i973.SplashController>(() => _i973.SplashController());
-    gh.factory<_i756.TrackDetailController>(
-        () => _i756.TrackDetailController());
     gh.factory<_i256.AboutController>(() => _i256.AboutController());
-    gh.factory<_i769.FeedBackController>(() => _i769.FeedBackController());
     gh.factory<_i923.BrowserController>(() => _i923.BrowserController());
+    gh.factory<_i769.FeedBackController>(() => _i769.FeedBackController());
     gh.factory<_i108.PermissionSettingController>(
         () => _i108.PermissionSettingController());
+    gh.factory<_i973.SplashController>(() => _i973.SplashController());
+    gh.factory<_i756.TrackDetailController>(
+        () => _i756.TrackDetailController());
     gh.singleton<_i361.Dio>(() => networkModule.createDefaultDio());
     gh.lazySingleton<_i220.AtmobLocationClient>(
         () => _i220.AtmobLocationClient());
@@ -78,10 +78,10 @@ extension GetItInjectableX on _i174.GetIt {
         .provideAtmobStreamApi(gh<_i361.Dio>(instanceName: 'stream')));
     gh.lazySingleton<_i20.AccountRepository>(
         () => _i20.AccountRepository(gh<_i243.AtmobApi>()));
-    gh.lazySingleton<_i1053.FriendsRepository>(
-        () => _i1053.FriendsRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i850.ContactRepository>(
         () => _i850.ContactRepository(gh<_i243.AtmobApi>()));
+    gh.lazySingleton<_i1053.FriendsRepository>(
+        () => _i1053.FriendsRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i791.MessageRepository>(
         () => _i791.MessageRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i983.UrgentContactRepository>(
@@ -117,10 +117,10 @@ extension GetItInjectableX on _i174.GetIt {
               gh<_i983.UrgentContactRepository>(),
               gh<_i20.AccountRepository>(),
             ));
-    gh.factory<_i492.FriendSettingController>(
-        () => _i492.FriendSettingController(gh<_i1053.FriendsRepository>()));
     gh.factory<_i897.AddFriendDialogController>(
         () => _i897.AddFriendDialogController(gh<_i1053.FriendsRepository>()));
+    gh.factory<_i492.FriendSettingController>(
+        () => _i492.FriendSettingController(gh<_i1053.FriendsRepository>()));
     gh.lazySingleton<_i814.MemberRepository>(() => _i814.MemberRepository(
           gh<_i243.AtmobApi>(),
           gh<_i20.AccountRepository>(),

+ 16 - 3
lib/module/main/main_controller.dart

@@ -90,6 +90,11 @@ class MainController extends BaseController {
   String? lastCheckFriendId;
   bool isExecuteAutoSelect = false;
 
+  final RxBool _todayTrackRequesting = RxBool(false);
+  double? _friendTrackVisibleFraction;
+
+  bool get todayTrackRequesting => _todayTrackRequesting.value;
+
   MainController(
       this.friendsRepository,
       this.accountRepository,
@@ -324,7 +329,6 @@ class MainController extends BaseController {
   }
 
   void onViewTraceClick(UserInfo userInfo) {
-    print("userInfosfsdf--${userInfo}");
     if (!accountRepository.isLogin.value) {
       showTraceTipsDialog(onConfirm: () {
         LoginPage.start();
@@ -396,8 +400,8 @@ class MainController extends BaseController {
   //弹出求助提示
   void onShowRequestHelpTip() {
     String? memberPageKeyStr = KVUtil.getString(_kChickHelpAlertKey, '');
-    if (memberStatusInfo.value?.expired ==
-        true && (memberPageKeyStr ?? '').isEmpty) {
+    if (memberStatusInfo.value?.expired == true &&
+        (memberPageKeyStr ?? '').isEmpty) {
       ///永久化存储
       KVUtil.putString(_kChickHelpAlertKey, _kChickHelpAlertKey);
       UrgentContactClickHelpDialog.show(confirmOnTap: () {
@@ -448,6 +452,15 @@ class MainController extends BaseController {
     }
   }
 
+  void onFriendVisibleFraction(double visibleFraction) {
+    _friendTrackVisibleFraction = visibleFraction;
+    if (visibleFraction > 0.2) {
+      _requestSelectedFriendTrack();
+    }
+  }
+
+  void _requestSelectedFriendTrack() {}
+
   @override
   void onClose() {
     mineLocationSubscription?.cancel();

+ 95 - 72
lib/module/main/main_page.dart

@@ -12,8 +12,10 @@ import 'package:location/module/main/view.dart';
 import 'package:location/resource/assets.gen.dart';
 import 'package:location/resource/colors.gen.dart';
 import 'package:location/resource/string.gen.dart';
+import 'package:location/utils/atmob_log.dart';
 import 'package:location/utils/common_expand.dart';
 import 'package:sliding_sheet2/sliding_sheet2.dart';
+import 'package:visibility_detector/visibility_detector.dart';
 import '../../data/consts/constants.dart';
 import '../../router/app_pages.dart';
 import '../../utils/common_style.dart';
@@ -45,12 +47,18 @@ class MainPage extends BasePage<MainController> {
       onPopInvokedWithResult: (bool didPop, dynamic result) {
         controller.onAppBack();
       },
-      child: Stack(
+      child: Column(
         children: [
-          buildMapView(),
-          buildMapFunView(),
-          buildMainBottomView(),
-          buildFriendListView(),
+          Expanded(
+              child: Stack(
+            children: [
+              buildMapView(),
+              buildMapFunView(),
+              buildMainBottomView(),
+              buildFriendListView(),
+            ],
+          )),
+          buildTabContainer()
         ],
       ),
     );
@@ -75,94 +83,109 @@ class MainPage extends BasePage<MainController> {
       shadowColor: Colors.black.withOpacity(0.1),
       cornerRadius: 18.w,
       snapSpec: SnapSpec(
-        initialSnap: SnapSpec.headerFooterSnap,
+        initialSnap: SnapSpec.headerSnap,
         // Enable snapping. This is true by default.
         snap: true,
         // Set custom snapping points.
-        snappings: [SnapSpec.headerFooterSnap, SnapSpec.expanded],
+        snappings: [SnapSpec.headerSnap, SnapSpec.expanded],
         // Define to what the snappings relate to. In this case,
         // the total available space that the sheet can expand to.
         positioning: SnapPositioning.relativeToAvailableSpace,
       ),
-      footerBuilder: buildFooterBuilder,
       headerBuilder: buildHeaderBuilder,
       builder: buildTrackEntranceBuilder,
     );
   }
 
   Widget buildTrackEntranceBuilder(BuildContext context, SheetState state) {
-    return Container(
-      margin: EdgeInsets.only(left: 12.w, right: 12.w, bottom: 12.w),
-      padding: EdgeInsets.only(left: 12.w, right: 12.w, top: 12.w, bottom: 9.w),
-      decoration: BoxDecoration(
-          color: Colors.white, borderRadius: BorderRadius.circular(20.r)),
-      child: AspectRatio(
-        aspectRatio: 336 / 134,
-        child: Column(
+    return VisibilityDetector(
+      key: Key('main_today_track'),
+      onVisibilityChanged: (VisibilityInfo info) {
+        final visibleFraction = info.visibleFraction;
+        controller.onFriendVisibleFraction(visibleFraction);
+      },
+      child: Container(
+        margin: EdgeInsets.only(left: 12.w, right: 12.w, bottom: 12.w),
+        padding:
+            EdgeInsets.only(left: 12.w, right: 12.w, top: 12.w, bottom: 9.w),
+        decoration: BoxDecoration(
+            color: Colors.white, borderRadius: BorderRadius.circular(20.r)),
+        child: AspectRatio(
+          aspectRatio: 336 / 134,
+          child: Column(
+            children: [
+              Row(
+                children: [
+                  Text(StringName.todaySimpleTrack,
+                      style: TextStyle(
+                          fontSize: 13.sp,
+                          color: '#333333'.color,
+                          fontWeight: FontWeight.bold)),
+                  Spacer(),
+                  Assets.images.iconMainTrackArrow
+                      .image(width: 10.w, height: 10.w),
+                ],
+              ),
+              SizedBox(height: 7.w),
+              buildSelectedFriendTodayTrackDetailView(),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget buildSelectedFriendTodayTrackDetailView() {
+    return Expanded(child: Obx(() {
+      if (controller.todayTrackRequesting) {
+        return Center(child: Text('加载中...'));
+      } else {
+        return Row(
           children: [
-            Row(
+            Container(
+              margin: EdgeInsets.only(bottom: 3.w, top: 3.w),
+              child: AspectRatio(
+                aspectRatio: 1,
+                child: Assets.images.imgTrackExample.image(),
+              ),
+            ),
+            SizedBox(width: 10.w),
+            Column(
+              mainAxisAlignment: MainAxisAlignment.center,
               children: [
-                Text(StringName.todaySimpleTrack,
-                    style: TextStyle(
-                        fontSize: 13.sp,
-                        color: '#333333'.color,
-                        fontWeight: FontWeight.bold)),
-                Spacer(),
-                Assets.images.iconMainTrackArrow
-                    .image(width: 10.w, height: 10.w),
+                SizedBox(height: 6.w),
+                getMainTrackDot('#15CBA1'.color),
+                Expanded(
+                  child: Container(
+                    margin: EdgeInsets.symmetric(vertical: 4.w),
+                    width: 1.w,
+                    height: double.infinity,
+                    decoration: BoxDecoration(
+                      color: '#F0F0F0'.color,
+                      borderRadius: BorderRadius.circular(100.r),
+                    ),
+                  ),
+                ),
+                getMainTrackDot('#E94949'.color),
+                SizedBox(height: 6.w),
               ],
             ),
-            SizedBox(height: 7.w),
+            SizedBox(width: 8.w),
             Expanded(
-              child: Row(
+              child: Column(
                 children: [
-                  Container(
-                    margin: EdgeInsets.only(bottom: 3.w, top: 3.w),
-                    child: AspectRatio(
-                      aspectRatio: 1,
-                      child: Assets.images.imgTrackExample.image(),
-                    ),
-                  ),
-                  SizedBox(width: 10.w),
-                  Column(
-                    mainAxisAlignment: MainAxisAlignment.center,
-                    children: [
-                      SizedBox(height: 6.w),
-                      getMainTrackDot('#15CBA1'.color),
-                      Expanded(
-                        child: Container(
-                          margin: EdgeInsets.symmetric(vertical: 4.w),
-                          width: 1.w,
-                          height: double.infinity,
-                          decoration: BoxDecoration(
-                            color: '#F0F0F0'.color,
-                            borderRadius: BorderRadius.circular(100.r),
-                          ),
-                        ),
-                      ),
-                      getMainTrackDot('#E94949'.color),
-                      SizedBox(height: 6.w),
-                    ],
-                  ),
-                  SizedBox(width: 8.w),
-                  Expanded(
-                    child: Column(
-                      children: [
-                        buildTrackPoint(MainTrackType.startPoint,
-                            '广东省广州市天河区高唐路235号靠近C栋2楼时代E-PARK'),
-                        Spacer(),
-                        buildTrackPoint(
-                            MainTrackType.errorPoint, '广东省广州市海珠区滨江东路驾驶花园C栋601'),
-                      ],
-                    ),
-                  )
+                  buildTrackPoint(MainTrackType.startPoint,
+                      '广东省广州市天河区高唐路235号靠近C栋2楼时代E-PARK'),
+                  Spacer(),
+                  buildTrackPoint(
+                      MainTrackType.errorPoint, '广东省广州市海珠区滨江东路驾驶花园C栋601'),
                 ],
               ),
-            ),
+            )
           ],
-        ),
-      ),
-    );
+        );
+      }
+    }));
   }
 
   Widget buildFriendListView() {
@@ -190,7 +213,7 @@ class MainPage extends BasePage<MainController> {
   Widget buildMapFunView() {
     return Positioned(
       right: 0.w,
-      bottom: 230.w,
+      bottom: 140.w,
       child: Column(
         children: [
           GestureDetector(

+ 118 - 253
lib/module/track/track_controller.dart

@@ -1,79 +1,51 @@
-import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_cupertino_datetime_picker/flutter_cupertino_datetime_picker.dart';
 import 'package:flutter_map/flutter_map.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:get/get_core/src/get_main.dart';
 import 'package:injectable/injectable.dart';
 import 'package:location/base/base_controller.dart';
-import 'package:location/data/bean/location_info.dart';
 import 'package:location/data/bean/track_days.dart';
 import 'package:location/data/consts/constants.dart';
-import 'package:location/data/consts/error_code.dart';
 import 'package:location/data/repositories/account_repository.dart';
 import 'package:location/data/repositories/friends_repository.dart';
 import 'package:location/data/repositories/track_repository.dart';
-import 'package:location/dialog/loading_dialog.dart';
 import 'package:location/handler/error_handler.dart';
-import 'package:location/module/member/member_page.dart';
-import 'package:location/module/track/track_detail/track_detail_page.dart';
-import 'package:location/module/track/track_util.dart';
 import 'package:location/resource/string.gen.dart';
-import 'package:location/utils/atmob_log.dart';
-import 'package:location/utils/common_expand.dart';
-import 'package:location/utils/http_handler.dart';
-import 'package:location/utils/toast_util.dart';
 import 'package:sliding_sheet2/sliding_sheet2.dart';
-import '../../data/bean/atmob_track_point.dart';
 import '../../data/bean/user_info.dart';
-import '../../dialog/common_confirm_dialog_impl.dart';
-import '../../utils/date_util.dart';
-import '../../utils/pair.dart';
+import '../../dialog/location_permission_dialog.dart';
+import '../../sdk/map/map_helper.dart';
+import '../../utils/permission_util.dart';
 
 @injectable
-class TrackController extends BaseController {
-  final int errorQueryOriginalDataEmpty = 10; //查询原始数据集为空
-  final int errorQueryOriginalTooFew = 11; //查询原始数据集少于2点
-
+class TrackController extends BaseController
+    with GetSingleTickerProviderStateMixin {
   final Rxn<UserInfo> _userInfo = Rxn<UserInfo>();
 
   UserInfo? get userInfo => _userInfo.value;
 
   final MapController mapController = MapController();
 
-  final Rxn<DateTime> _trackStartTime = Rxn<DateTime>();
-  final Rxn<DateTime> _trackEndTime = Rxn<DateTime>();
-
-  DateTime? get trackStartTime => _trackStartTime.value;
-
-  DateTime? get trackEndTime => _trackEndTime.value;
   SheetController sheetController = SheetController();
 
-  final Rxn<String> _startAddress = Rxn<String>();
-  final Rxn<String> _endAddress = Rxn<String>();
-
-  String? get startAddress => _startAddress.value;
-
-  String? get endAddress => _endAddress.value;
-
-  final Duration maxDuration = Duration(days: 1);
-  final String timeFormat = "yyyy年-MM月-dd日 HH时:mm分";
-  final RxInt _currentIndex = 0.obs;
+  final RxList<TrackDays> daysList = RxList<TrackDays>();
 
-  int get currentIndex => _currentIndex.value;
+  final Rxn<TabController> _tabController = Rxn<TabController>();
 
-  final Rxn<LocationInfo> _currentLocation = Rxn<LocationInfo>();
+  TabController? get tabController => _tabController.value;
 
-  LocationInfo? get currentLocation => _currentLocation.value;
+  final Rxn<TrackDays> currentTrackDay = Rxn();
 
-  List<LatLng>? points;
-  List<AtmobTrackPoint>? originPoints;
+  final RxDouble _sheetProgress = 0.0.obs;
 
-  final RxBool _isShowTraceDetailBtn = false.obs;
+  double get sheetProgress => _sheetProgress.value;
 
-  bool get isShowTraceDetailBtn => _isShowTraceDetailBtn.value;
+  final mapPadding =
+      MapPadding(left: 50.w, top: 100.w, right: 50.w, bottom: Get.height / 2);
 
-  final RxList<TrackDays> daysList = RxList<TrackDays>();
+  final selectPadding =
+      MapPadding(left: 80.w, top: 150.w, right: 80.w, bottom: Get.height / 2);
 
   final TrackRepository trackRepository;
   final FriendsRepository friendsRepository;
@@ -88,8 +60,6 @@ class TrackController extends BaseController {
     if (param is UserInfo) {
       _userInfo.value = param;
     }
-    _initTime();
-    _onCurrentLocationQuery(isShow: false);
     _onRequestTrackDateList();
   }
 
@@ -103,11 +73,31 @@ class TrackController extends BaseController {
   void _onRequestTrackDateList() {
     trackRepository.getLocationTrackDays().then((list) {
       daysList.assignAll(list);
+      _createTabController();
     }).catchError((error) {
       ErrorHandler.toastError(error);
     });
   }
 
+  void _createTabController() {
+    tabController?.dispose();
+    final tab = TabController(
+      length: daysList.length,
+      vsync: this,
+    );
+    _tabController.value = tab;
+    tab.addListener(() {
+      if (tab.indexIsChanging == false) {
+        //控制请求当前页数据或者数据已有时跟换数据
+        mapController.clear();
+        currentTrackDay.value = daysList[tab.index];
+      }
+    });
+    Future.delayed(Duration(milliseconds: 100), () {
+      currentTrackDay.value = daysList.isNotEmpty ? daysList[0] : null;
+    });
+  }
+
   ///记录查看轨迹的次数
   void _recordNumberTrajectoryViewed() {
     if (accountRepository.memberStatusInfo.value?.trialed == true &&
@@ -116,245 +106,120 @@ class TrackController extends BaseController {
     }
   }
 
-  void _handleTabChange() {}
+  ///显示轨迹以及标记点
+  void showMapTrack(List<LatLng> points, List<Marker> markers) {
+    mapController.clear();
+    //画折线
+    if (points.length > 1) {
+      mapController.addPolyline(Constants.traceNormalLineId, points,
+          mapPadding: mapPadding);
+    }
+    //画标记点
+    if (markers.isNotEmpty) {
+      mapController.updateOrAddMarkers(markers);
+    }
+  }
 
-  void _initTime() {
-    //开始时间往前推一天
-    _trackStartTime.value = DateUtil.getNow(subtract: Duration(days: 1));
-    _trackEndTime.value = DateUtil.getNow();
+  void showSelectMarker(List<LatLng> points, Marker selectMarker) {
+    _clearSelectMapMarker();
+    mapController.updateOrAddMarker(selectMarker);
+    mapController.moveToSuitableLocation(points, mapPadding: selectPadding);
   }
 
   void back() {
     Get.back();
   }
 
-  void onTrackStartTimeClick(BuildContext context) {
-    if (userInfo?.virtual == true && accountRepository.memberIsExpired()) {
-      MemberPage.start();
-      return;
-    }
-    DatePicker.showDatePicker(context,
-        locale: DateTimePickerLocale.zh_cn,
-        initialDateTime: _trackStartTime.value,
-        dateFormat: timeFormat, onConfirm: (dateTime, selectedIndex) {
-      if (trackEndTime != null &&
-          DateUtil.isTimeRangeExceed(dateTime, trackEndTime!, maxDuration)) {
-        ToastUtil.show(StringName.trackChooseTimeError);
-        _trackEndTime.value = dateTime.add(maxDuration);
-      }
-      _trackStartTime.value = dateTime;
-    });
+  void showMovingTrack(List<LatLng> movingPoints) {
+    _clearSelectMapMarker();
+    mapController.addPolyline(Constants.traceSelectLineId, movingPoints,
+        lineType: PolylineType.selected, mapPadding: mapPadding);
   }
 
-  void onTrackEndTimeClick(BuildContext context) {
-    if (userInfo?.virtual == true && accountRepository.memberIsExpired()) {
-      MemberPage.start();
-      return;
-    }
-    DatePicker.showDatePicker(context,
-        locale: DateTimePickerLocale.zh_cn,
-        initialDateTime: _trackEndTime.value,
-        dateFormat: timeFormat, onConfirm: (dateTime, selectedIndex) {
-      if (trackStartTime != null &&
-          DateUtil.isTimeRangeExceed(trackStartTime!, dateTime, maxDuration)) {
-        ToastUtil.show(StringName.trackChooseTimeError);
-        _trackStartTime.value = dateTime.subtract(maxDuration);
-      }
-      _trackEndTime.value = dateTime;
-    });
+  void showTrackError(List<LatLng> errorPoints, Marker errorMarker) {
+    _clearSelectMapMarker();
+    mapController.updateOrAddMarker(errorMarker);
+    mapController.addPolyline(Constants.traceSelectLineId, errorPoints,
+        lineType: PolylineType.error, mapPadding: mapPadding);
   }
 
-  void onTrackQueryClick() {
-    if (userInfo?.virtual == false && accountRepository.memberIsExpired()) {
-      MemberPage.start();
-      return;
-    }
-    if (currentIndex == 0) {
-      _onTrackQuery();
-    } else {
-      _onCurrentLocationQuery();
-    }
+  void _clearSelectMapMarker() {
+    mapController.removeMarker(Constants.mineLocationId);
+    mapController.removePolyline(Constants.traceSelectLineId);
+    mapController.removeMarker(Constants.tracePopupId);
+    mapController.removeMarker(Constants.traceErrorId);
   }
 
-  void _onCurrentLocationQuery({bool isShow = true}) {
-    if (userInfo == null) {
-      return;
-    }
-    if (userInfo?.isMine == true) {
-      _currentLocation.value =
-          accountRepository.mineUserInfo.value.lastLocation.value;
-      if (isShow) {
-        _showCurrentLocation();
-      }
-    } else {
-      if (isShow) LoadingDialog.show(StringName.trackLoadingTxt);
-      friendsRepository
-          .getUserInfoFromId(userInfo!.id, isVirtual: userInfo!.virtual)
-          .then((location) {
-        LoadingDialog.hide();
-        _currentLocation.value = userInfo?.lastLocation.value;
-        if (isShow) {
-          _showCurrentLocation();
-        }
-      }).catchError((error) {
-        debugPrint("error: $error");
-        ErrorHandler.toastError(error);
-      });
-    }
+  setSheetProgress(double progress) {
+    _sheetProgress.value = progress;
   }
 
-  void _onTrackQuery() {
-    if (trackStartTime == null || trackEndTime == null || userInfo == null) {
-      return;
+  void onCurrentLocationClick() async {
+    //权限检查
+    bool isGranted = await PermissionUtil.checkLocationPermission();
+    if (!isGranted) {
+      LocationPermissionDialog.show(onNextStep: _requestLocationPermission);
+    } else {
+      _updateCurrentLocation();
     }
-    LoadingDialog.show(StringName.trackLoadingTxt);
-    _startAddress.value = '';
-    _endAddress.value = '';
-    originPoints = null;
-    points = null;
-    Future.value().then((_) {
-      if (userInfo?.virtual == true) {
-        return trackRepository.queryVirtualTrack();
-      } else {
-        return trackRepository.queryTrack(
-            startTime: trackStartTime?.millisecondsSinceEpoch,
-            endTime: trackEndTime?.millisecondsSinceEpoch,
-            userId: userInfo?.id);
-      }
-    }).map((trackResponse) {
-      final pointsList = trackResponse.trackPoints;
-      if (pointsList == null || pointsList.isEmpty) {
-        throw TrackQueryException(errorQueryOriginalDataEmpty);
-      }
-      if (pointsList.length < 2) {
-        throw TrackQueryException(errorQueryOriginalTooFew);
-      }
-      if (userInfo?.virtual == true) {
-        int nowMill = DateUtil.getNow().millisecondsSinceEpoch;
-        int firstMill = pointsList.first.time;
-        int differ = nowMill - firstMill;
-        pointsList.first.time = nowMill;
-        for (var element in pointsList) {
-          element.time = element.time + differ;
-        }
-      }
-      return pointsList;
-    }).then((pointsList) async {
-      final list = TrackUtil.points2TraceLocation(pointsList);
-      List<LatLng>? convertList;
-      try {
-        convertList = await FlutterMap.queryProcessedTrace(
-            lineID: pointsList.hashCode, locations: list);
-      } catch (e) {
-        AtmobLog.e("TrackController", "queryProcessedTrace error: $e");
-      }
-      if (convertList == null || convertList.isEmpty) {
-        //轨迹纠偏失败,使用原始数据
-        convertList = TrackUtil.traceLocation2LatLng(list);
-      }
-      return Pair(pointsList, convertList);
-    }).then((pair) {
-      originPoints = pair.first;
-      points = pair.second;
-      _showTrack();
-      _setStartAndEndAddress(start: pair.first.first, end: pair.first.last);
-    }).catchError((error) {
-      if (error is TrackQueryException) {
-        showQueryErrorDialog();
-      } else if (error is ServerErrorException) {
-        if (error.code == ErrorCode.noMember) {
-          MemberPage.start();
-          ToastUtil.show(StringName.memberExpired);
-        } else {
-          ToastUtil.show(error.message);
-        }
-      } else {
-        ErrorHandler.toastError(error);
-      }
-    }).whenComplete(() {
-      LoadingDialog.hide();
-      _isShowTraceDetailBtn.value = true;
-    });
   }
 
+  void _updateCurrentLocation() {
+    var lastLocation = MapHelper.getLastLocation();
+    if (lastLocation == null) {
+      locationListener(MapLocation location) {
+        MapHelper.removeLocationListener(locationListener);
+        showMineLocationMarker(location);
+      }
 
-  void _setStartAndEndAddress(
-      {required AtmobTrackPoint start, required AtmobTrackPoint end}) {
-    _startAddress.value = start.addr;
-    _endAddress.value = end.addr;
-  }
-
-  void _showCurrentLocation() {
-    mapController.clear();
-    if (currentLocation == null || userInfo == null) {
-      return;
+      MapHelper.addLocationListener(locationListener);
+    } else {
+      showMineLocationMarker(lastLocation);
     }
-    mapController.updateOrAddMarker(Marker(
-      id: userInfo!.id,
-      markerName: userInfo!.getUserNickName(),
-      longitude: userInfo!.lastLocation.value?.longitude,
-      latitude: userInfo!.lastLocation.value?.latitude,
-      markerType: userInfo!.isMine == true
-          ? MarkerType.traceEndMinePoint
-          : MarkerType.traceEndFriendPoint,
-      customAvatarUrl: userInfo!.avatar,
-    ));
-    mapController.animateCamera(CameraPosition(
-        latitude: currentLocation!.latitude,
-        longitude: currentLocation!.longitude,
-        zoom: 18));
   }
 
-  void _showTrack() {
-    mapController.clear();
-    if (points == null || points!.length < 2) {
-      return;
-    }
-    mapController.addPolyline(points!,
-        mapPadding:
-            MapPadding(left: 50, top: 100, right: 50, bottom: Get.height / 2));
-    mapController.updateOrAddMarker(Marker(
-        id: Constants.traceStartId,
-        markerName: '',
-        longitude: points!.first.longitude,
-        latitude: points!.first.latitude,
-        markerType: MarkerType.traceStartPoint));
-    mapController.updateOrAddMarker(Marker(
-      id: Constants.traceEndId,
-      markerName: userInfo?.getUserNickName() ?? '',
-      longitude: points!.last.longitude,
-      latitude: points!.last.latitude,
-      markerType: userInfo?.isMine == true
-          ? MarkerType.traceEndMinePoint
-          : MarkerType.traceEndFriendPoint,
-      customAvatarUrl: userInfo?.avatar,
-    ));
+  void showMineLocationMarker(MapLocation location) {
+    _clearSelectMapMarker();
+    Marker mineMarker = Marker(
+        id: Constants.mineLocationId,
+        latitude: location.latitude,
+        longitude: location.longitude,
+        isSelected: false,
+        customAvatarUrl: accountRepository.mineUserInfo.value.avatar,
+        markerName: StringName.locationMine,
+        markerType: MarkerType.mine);
+    mapController.updateOrAddMarker(mineMarker);
+    animateCamera(latitude: location.latitude, longitude: location.longitude);
   }
 
-  void showQueryErrorDialog() {
-    showTraceNoDataDialog(onConfirm: () {});
+  void animateCamera({required double? latitude, required double? longitude}) {
+    mapController.animateCamera(
+        CameraPosition(longitude: longitude, latitude: latitude, zoom: 18));
   }
 
-  void onTraceDetailClick() {
-    if (accountRepository.memberIsExpired()) {
-      MemberPage.start();
-      return;
+  void _requestLocationPermission() async {
+    bool isGranted = await PermissionUtil.requestLocationPermission();
+    if (isGranted) {
+      _showLocationAlways();
+      _updateCurrentLocation();
     }
-    if (originPoints == null || originPoints!.length < 2) {
-      showTraceNoDataDialog(onConfirm: () {});
-      return;
+  }
+
+  void _showLocationAlways() async {
+    bool isGranted = await PermissionUtil.checkShowLocationAlways();
+    if (!isGranted) {
+      LocationAlwaysPermissionDialog.show(onNextStep: () async {
+        isGranted = await PermissionUtil.requestShowLocationAlways();
+        if (isGranted) {
+          _updateCurrentLocation();
+        }
+      });
     }
-    TrackDetailPage.start(originPoints!);
   }
 
   @override
   void onClose() {
     super.onClose();
+    tabController?.dispose();
   }
 }
-
-class TrackQueryException implements Exception {
-  final int code;
-
-  TrackQueryException(this.code);
-}

+ 115 - 94
lib/module/track/track_day_detail/track_daily_item.dart

@@ -14,7 +14,10 @@ import 'package:location/utils/date_util.dart';
 
 import '../track_status.dart';
 
-Widget buildTrackDailyItem(TrackDailyBean bean, bool isEnd) {
+typedef TrackItemClick = void Function(TrackDailyBean bean);
+
+Widget buildTrackDailyItem(TrackDailyBean bean, bool isEnd,
+    {TrackItemClick? onItemClick}) {
   return Container(
     padding: EdgeInsets.symmetric(horizontal: 12.w),
     margin: EdgeInsets.only(bottom: 8.w),
@@ -23,11 +26,11 @@ Widget buildTrackDailyItem(TrackDailyBean bean, bool isEnd) {
       children: [
         Builder(builder: (context) {
           if (bean.status == TrackStatus.moving) {
-            return _buildMovingTrackDailyItem(bean);
+            return _buildMovingTrackDailyItem(bean, onItemClick: onItemClick);
           } else if (bean.status == TrackStatus.stay) {
-            return buildStayTrackDailyItem(bean);
+            return buildStayTrackDailyItem(bean, onItemClick: onItemClick);
           } else if (bean.status == TrackStatus.error) {
-            return buildErrorTrackDailyItem(bean);
+            return buildErrorTrackDailyItem(bean, onItemClick: onItemClick);
           } else {
             return SizedBox(height: 50.w, child: Text('未知轨迹,请更新最新应用版本'));
           }
@@ -38,7 +41,8 @@ Widget buildTrackDailyItem(TrackDailyBean bean, bool isEnd) {
   );
 }
 
-Widget _buildMovingTrackDailyItem(TrackDailyBean bean) {
+Widget _buildMovingTrackDailyItem(TrackDailyBean bean,
+    {TrackItemClick? onItemClick}) {
   return Column(
     crossAxisAlignment: CrossAxisAlignment.start,
     children: [
@@ -64,28 +68,33 @@ Widget _buildMovingTrackDailyItem(TrackDailyBean bean) {
               ],
             ),
             Expanded(
-                child: Container(
-              height: 50.w,
-              margin: EdgeInsets.only(top: 26.w),
-              decoration: BoxDecoration(
-                  borderRadius: BorderRadius.circular(8.r),
-                  gradient: LinearGradient(colors: [
-                    '#F8F5FF'.color,
-                    ColorName.transparent,
-                  ])),
-              padding: EdgeInsets.symmetric(horizontal: 14.w),
-              child: Row(
-                children: [
-                  Assets.images.iconTrackMoving.image(width: 16.w),
-                  SizedBox(width: 5.w),
-                  Text(
-                    StringName.trackDetailMoving,
-                    style: TextStyle(
-                        fontSize: 12.sp,
-                        color: '#333333'.color,
-                        fontWeight: FontWeight.bold),
-                  )
-                ],
+                child: GestureDetector(
+              onTap: () {
+                onItemClick?.call(bean);
+              },
+              child: Container(
+                height: 50.w,
+                margin: EdgeInsets.only(top: 26.w),
+                decoration: BoxDecoration(
+                    borderRadius: BorderRadius.circular(8.r),
+                    gradient: LinearGradient(colors: [
+                      '#F8F5FF'.color,
+                      ColorName.transparent,
+                    ])),
+                padding: EdgeInsets.symmetric(horizontal: 14.w),
+                child: Row(
+                  children: [
+                    Assets.images.iconTrackMoving.image(width: 16.w),
+                    SizedBox(width: 5.w),
+                    Text(
+                      StringName.trackDetailMoving,
+                      style: TextStyle(
+                          fontSize: 12.sp,
+                          color: '#333333'.color,
+                          fontWeight: FontWeight.bold),
+                    )
+                  ],
+                ),
               ),
             ))
           ],
@@ -97,7 +106,7 @@ Widget _buildMovingTrackDailyItem(TrackDailyBean bean) {
 }
 
 Widget buildStayTrackDailyItem(TrackDailyBean bean,
-    {EdgeInsetsGeometry? contentPadding}) {
+    {EdgeInsetsGeometry? contentPadding, TrackItemClick? onItemClick}) {
   return Column(
     crossAxisAlignment: CrossAxisAlignment.start,
     children: [
@@ -123,40 +132,45 @@ Widget buildStayTrackDailyItem(TrackDailyBean bean,
               ],
             ),
             Expanded(
-                child: Container(
-              padding: EdgeInsets.all(10.w),
-              margin: contentPadding ?? EdgeInsets.only(top: 20.w),
-              decoration: BoxDecoration(
-                  borderRadius: BorderRadius.circular(8.r),
-                  gradient: LinearGradient(colors: [
-                    '#F8F5FF'.color,
-                    ColorName.transparent,
-                  ])),
-              child: ConstrainedBox(
-                constraints: BoxConstraints(minHeight: 60.w),
-                child: Column(
-                  crossAxisAlignment: CrossAxisAlignment.start,
-                  children: [
-                    Expanded(
-                      child: Text(
-                        bean.addr ?? '',
-                        style: TextStyle(
-                            fontSize: 12.sp,
-                            color: '#333333'.color,
-                            fontWeight: FontWeight.bold),
+                child: GestureDetector(
+              onTap: () {
+                onItemClick?.call(bean);
+              },
+              child: Container(
+                padding: EdgeInsets.all(10.w),
+                margin: contentPadding ?? EdgeInsets.only(top: 20.w),
+                decoration: BoxDecoration(
+                    borderRadius: BorderRadius.circular(8.r),
+                    gradient: LinearGradient(colors: [
+                      '#F8F5FF'.color,
+                      ColorName.transparent,
+                    ])),
+                child: ConstrainedBox(
+                  constraints: BoxConstraints(minHeight: 60.w),
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Expanded(
+                        child: Text(
+                          bean.addr ?? '',
+                          style: TextStyle(
+                              fontSize: 12.sp,
+                              color: '#333333'.color,
+                              fontWeight: FontWeight.bold),
+                        ),
                       ),
-                    ),
-                    SizedBox(height: 11.w),
-                    Row(
-                      children: [
-                        _buildStayDesc(bean.duration),
-                        SizedBox(width: 18.w),
-                        _buildLockDesc(bean.highUnlock, bean.totalUnlock),
-                        SizedBox(width: 18.w),
-                        Expanded(child: _buildNetDesc(bean.network))
-                      ],
-                    )
-                  ],
+                      SizedBox(height: 11.w),
+                      Row(
+                        children: [
+                          _buildStayDesc(bean.duration),
+                          SizedBox(width: 18.w),
+                          _buildLockDesc(bean.highUnlock, bean.totalUnlock),
+                          SizedBox(width: 18.w),
+                          Expanded(child: _buildNetDesc(bean.network))
+                        ],
+                      )
+                    ],
+                  ),
                 ),
               ),
             ))
@@ -169,7 +183,7 @@ Widget buildStayTrackDailyItem(TrackDailyBean bean,
 }
 
 Widget buildErrorTrackDailyItem(TrackDailyBean bean,
-    {EdgeInsetsGeometry? contentPadding}) {
+    {EdgeInsetsGeometry? contentPadding, TrackItemClick? onItemClick}) {
   return Column(
     crossAxisAlignment: CrossAxisAlignment.start,
     children: [
@@ -195,39 +209,46 @@ Widget buildErrorTrackDailyItem(TrackDailyBean bean,
               ],
             ),
             Expanded(
-                child: Container(
-              height: 50.w,
-              padding: EdgeInsets.all(10.w),
-              margin: contentPadding ?? EdgeInsets.only(top: 41.w, bottom: 7.w),
-              decoration: BoxDecoration(
-                  borderRadius: BorderRadius.circular(8.r),
-                  gradient: LinearGradient(colors: [
-                    '#FFECEC'.color,
-                    ColorName.white,
-                  ])),
-              child: Row(
-                children: [
-                  Assets.images.iconTrackError.image(width: 19.4.w),
-                  SizedBox(width: 5.5.w),
-                  Text(StringName.trackDetailError,
-                      style: TextStyle(
-                          fontSize: 12.sp,
-                          color: '#333333'.color,
-                          fontWeight: FontWeight.bold)),
-                  SizedBox(width: 10.w),
-                  GestureDetector(
-                    onTap: () {
-                      TrackErrorTipsDialog.show();
-                    },
-                    child: Text(
-                      StringName.trackDetailSeeError,
-                      style: TextStyle(fontSize: 11.sp, color: '#4476FF'.color),
+                child: GestureDetector(
+              onTap: () {
+                onItemClick?.call(bean);
+              },
+              child: Container(
+                height: 50.w,
+                padding: EdgeInsets.all(10.w),
+                margin:
+                    contentPadding ?? EdgeInsets.only(top: 41.w, bottom: 7.w),
+                decoration: BoxDecoration(
+                    borderRadius: BorderRadius.circular(8.r),
+                    gradient: LinearGradient(colors: [
+                      '#FFECEC'.color,
+                      ColorName.white,
+                    ])),
+                child: Row(
+                  children: [
+                    Assets.images.iconTrackError.image(width: 19.4.w),
+                    SizedBox(width: 5.5.w),
+                    Text(StringName.trackDetailError,
+                        style: TextStyle(
+                            fontSize: 12.sp,
+                            color: '#333333'.color,
+                            fontWeight: FontWeight.bold)),
+                    SizedBox(width: 10.w),
+                    GestureDetector(
+                      onTap: () {
+                        TrackErrorTipsDialog.show();
+                      },
+                      child: Text(
+                        StringName.trackDetailSeeError,
+                        style:
+                            TextStyle(fontSize: 11.sp, color: '#4476FF'.color),
+                      ),
                     ),
-                  ),
-                  Spacer(),
-                  Assets.images.imgTrackAiAnalyse.image(width: 73.w),
-                  SizedBox(width: 6.w),
-                ],
+                    Spacer(),
+                    Assets.images.imgTrackAiAnalyse.image(width: 73.w),
+                    SizedBox(width: 6.w),
+                  ],
+                ),
               ),
             ))
           ],

+ 144 - 8
lib/module/track/track_day_detail/track_day_detail_controller.dart

@@ -2,15 +2,18 @@ import 'dart:async';
 import 'dart:convert';
 
 import 'package:flutter/cupertino.dart';
+import 'package:flutter_map/flutter_map.dart';
 import 'package:get/get.dart';
 import 'package:get/get_core/src/get_main.dart';
 import 'package:location/base/base_controller.dart';
 import 'package:location/data/bean/track_daily_bean.dart';
+import 'package:location/data/consts/constants.dart';
 import 'package:location/data/repositories/track_repository.dart';
 import 'package:location/dialog/loading_dialog.dart';
 import 'package:location/handler/error_handler.dart';
 import 'package:location/module/track/track_controller.dart';
 import 'package:location/module/track/track_day_detail/time_proportion/pie_chat_data.dart';
+import 'package:location/module/track/track_util.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/sdk/wechat/wechat_share_util.dart';
 import 'package:location/utils/async_util.dart';
@@ -19,9 +22,9 @@ import 'package:location/utils/pair.dart';
 import 'package:location/utils/toast_util.dart';
 import 'package:url_launcher/url_launcher.dart';
 import '../../../data/api/response/track_daily_summary_response.dart';
+import '../../../data/bean/atmob_track_point.dart';
 import '../../../data/bean/stream_chat_origin_data.dart';
 import '../../../data/bean/track_days.dart';
-import '../../../utils/capture_util.dart';
 import '../../../utils/http_handler.dart';
 import '../../../widget/gradually_print_text.dart';
 import '../track_status.dart';
@@ -68,26 +71,123 @@ class TrackDayDetailController extends BaseController {
 
   final RxnString _summaryError = RxnString();
 
+  CancelableFuture? requestTrackFuture;
+
   String? get summaryError => _summaryError.value;
 
   final GlobalKey shareGlobalKey = GlobalKey();
 
   CancelableFuture? summaryFuture;
 
+  //轨迹相关
+  List<AtmobTrackPoint>? trackOriginPoints; //原始轨迹点
+  List<LatLng> points = []; //根据停留情况合并的轨迹点
+  List<Marker> markers = []; // 停留时间较长的标记点
+
   TrackDayDetailController(this.days, bool isExpand) {
     trackRepository = TrackRepository.getInstance();
     _isExpanded.value = isExpand;
   }
 
   @override
-  void onInit() {
-    super.onInit();
-    _requestTrackDaily();
+  void onReady() {
+    trackController.currentTrackDay.listen((dat) {
+      if (dat == days) {
+        _requestTrackData();
+      }
+    });
     _requestTrackDailySummary();
-    _requestTrackHistoryPoints();
   }
 
-  void _requestTrackHistoryPoints() {}
+  _requestTrackData() {
+    if (isRequested) {
+      trackController.showMapTrack(points, markers);
+      return;
+    }
+    requestTrackFuture?.cancel();
+    requestTrackFuture = AsyncUtil.waitForAll(
+        [_requestTrackHistoryPoints(), _requestTrackDaily()]);
+    requestTrackFuture!.then((_) {
+      //组装地图新的数据
+      points.clear();
+      markers.clear();
+      for (int i = 0; i < trackDailyList.length; i++) {
+        final bean = trackDailyList[i];
+        double? markerLatitude;
+        double? markerLongitude;
+        if (bean.status == TrackStatus.stay) {
+          //停留
+          markerLatitude = bean.lat;
+          markerLongitude = bean.lng;
+          points.add(
+              LatLng(latitude: markerLatitude, longitude: markerLongitude));
+        } else if (bean.status == TrackStatus.moving) {
+          //移动
+          final movePoints = TrackUtil.getTrackMovePoints(
+              trackOriginPoints, bean.start, bean.end);
+          points.addAll(movePoints);
+          continue;
+        } else if (bean.status == TrackStatus.error) {
+          continue;
+        }
+
+        MarkerType markerType;
+        String? markerName;
+        String? customAvatarUrl;
+        if (i == 0) {
+          //起点
+          markerType = MarkerType.traceStartPoint;
+        } else if (i == trackDailyList.length - 1) {
+          //终点
+          final userInfo = trackController.userInfo;
+          markerName = userInfo?.getUserNickName();
+          markerType = userInfo?.isMine == true
+              ? MarkerType.traceEndMinePoint
+              : MarkerType.traceEndMinePoint;
+          customAvatarUrl = userInfo?.avatar;
+        } else {
+          markerType = MarkerType.tracePassingPoint;
+        }
+        markers.add(Marker(
+            id: '${bean.start}',
+            markerName: markerName ?? '',
+            longitude: markerLongitude,
+            latitude: markerLatitude,
+            markerType: markerType,
+            customAvatarUrl: customAvatarUrl));
+      }
+      if (markers.isNotEmpty &&
+          (markers.last.markerType != MarkerType.traceEndMinePoint ||
+              markers.last.markerType != MarkerType.traceEndFriendPoint)) {
+        markers.last.markerType = trackController.userInfo?.isMine == true
+            ? MarkerType.traceEndMinePoint
+            : MarkerType.traceEndFriendPoint;
+        markers.last.markerName =
+            trackController.userInfo?.getUserNickName() ?? '';
+        markers.last.customAvatarUrl = trackController.userInfo?.avatar;
+      }
+      //如果只有一个点,则添加一个起点和终点相同的点,绘制线需要2点
+      if (points.length == 1) {
+        points.add(points.first);
+      }
+      trackController.showMapTrack(points, markers);
+    });
+  }
+
+  Future<void> _requestTrackHistoryPoints() {
+    final userInfo = trackController.userInfo;
+    trackOriginPoints = null;
+    return Future.value().then((_) {
+      if (userInfo?.virtual == true) {
+        return trackRepository.queryVirtualTrack();
+      } else {
+        return trackRepository.queryTrack(
+            startTime: days.start, endTime: days.end, userId: userInfo?.id);
+      }
+    }).then((data) {
+      trackOriginPoints = data.trackPoints;
+    });
+  }
 
   void _requestTrackDailySummary() {
     summaryFuture?.cancel();
@@ -112,10 +212,10 @@ class TrackDayDetailController extends BaseController {
     });
   }
 
-  void _requestTrackDaily() {
+  Future<void> _requestTrackDaily() {
     _isRequested.value = false;
     CustomLoadingDialog.show(loadingTxt: StringName.trackLoadingTxt);
-    trackRepository
+    return trackRepository
         .trackDailyQuery(
             startTime: days.start,
             endTime: days.end,
@@ -282,10 +382,46 @@ class TrackDayDetailController extends BaseController {
     WechatShareUtil.shareWidgetToWeChat(shareGlobalKey);
   }
 
+  void onHistoryTrackItemClick(TrackDailyBean bean) {
+    if (bean.status == TrackStatus.stay) {
+      //停留状态
+      Marker selectMarker = Marker(
+        id: Constants.tracePopupId,
+        markerName: '',
+        latitude: bean.lat,
+        longitude: bean.lng,
+        markerType: MarkerType.tracePopupPoint,
+        tags: Popup(
+          title: bean.addr ?? '',
+          desc: TrackUtil.formatDurationFromMillis(bean.duration),
+        ),
+      );
+      trackController.showSelectMarker(points, selectMarker);
+    } else if (bean.status == TrackStatus.moving) {
+      final movingPoints =
+          TrackUtil.getTrackMovePoints(trackOriginPoints, bean.start, bean.end);
+      trackController.showMovingTrack(movingPoints);
+    } else if (bean.status == TrackStatus.error) {
+      final errorPoints =
+          TrackUtil.getTrackMovePoints(trackOriginPoints, bean.start, bean.end);
+      if (errorPoints.isEmpty) {
+        return;
+      }
+      Marker errorMarker = Marker(
+          id: Constants.traceErrorId,
+          markerName: '',
+          latitude: errorPoints.first.latitude,
+          longitude: errorPoints.first.longitude,
+          markerType: MarkerType.trackErrorPoint);
+      trackController.showTrackError(errorPoints, errorMarker);
+    }
+  }
+
   @override
   void onClose() {
     super.onClose();
     _streamChatSubscription?.cancel();
     graduallyController.dispose();
+    requestTrackFuture?.cancel();
   }
 }

+ 8 - 5
lib/module/track/track_day_detail/track_day_detail_view.dart

@@ -284,11 +284,13 @@ class TrackDayDetailView extends BaseView<TrackDayDetailController> {
                       TrackExpandType.errorNow) {
                 return buildErrorTrackDailyItem(
                     controller.expandSituation!.second,
-                    contentPadding: EdgeInsets.only(top: 46.w, bottom: 12.w));
+                    contentPadding: EdgeInsets.only(top: 46.w, bottom: 12.w),
+                    onItemClick: controller.onHistoryTrackItemClick);
               } else {
                 return buildStayTrackDailyItem(
                     controller.expandSituation!.second,
-                    contentPadding: EdgeInsets.only(top: 35.w));
+                    contentPadding: EdgeInsets.only(top: 35.w),
+                    onItemClick: controller.onHistoryTrackItemClick);
               }
             } else {
               return SizedBox.shrink();
@@ -335,8 +337,8 @@ class TrackDayDetailView extends BaseView<TrackDayDetailController> {
                   children: [
                     Text(
                       controller.isExpanded
-                          ? StringName.trackDetailExpand
-                          : StringName.trackDetailFold,
+                          ? StringName.trackDetailFold
+                          : StringName.trackDetailExpand,
                       style: TextStyle(fontSize: 10.sp, color: '#666666'.color),
                     ),
                     SizedBox(width: 1.w),
@@ -415,6 +417,7 @@ class TrackDayDetailView extends BaseView<TrackDayDetailController> {
 
   Widget buildHistoryTrackItem(BuildContext context, int index) {
     return buildTrackDailyItem(controller.trackDailyList[index],
-        index == controller.trackDailyList.length - 1);
+        index == controller.trackDailyList.length - 1,
+        onItemClick: controller.onHistoryTrackItemClick);
   }
 }

+ 36 - 184
lib/module/track/track_page.dart

@@ -9,17 +9,14 @@ import 'package:location/base/base_page.dart';
 import 'package:location/data/bean/user_info.dart';
 import 'package:location/module/track/track_controller.dart';
 import 'package:location/module/track/track_day_detail/track_day_detail_view.dart';
-import 'package:location/resource/assets.gen.dart';
 import 'package:location/resource/colors.gen.dart';
-import 'package:location/resource/string.gen.dart';
 import 'package:location/utils/common_expand.dart';
 import 'package:location/utils/common_style.dart';
-import 'package:location/utils/date_util.dart';
 import 'package:sliding_sheet2/sliding_sheet2.dart';
+import '../../resource/assets.gen.dart';
 import '../../router/app_pages.dart';
 import '../../utils/fixed_size_tab_indicator.dart';
 import '../../widget/common_view.dart';
-import '../../widget/relative_time_text.dart';
 
 class TrackPage extends BasePage<TrackController> {
   const TrackPage({super.key});
@@ -45,7 +42,11 @@ class TrackPage extends BasePage<TrackController> {
           ),
         ),
         buildBackBtnView(),
+        buildMapFunView(),
         SlidingSheet(
+          listener: (SheetState state) {
+            controller.setSheetProgress(state.progress);
+          },
           color: ColorName.white,
           controller: controller.sheetController,
           elevation: 10,
@@ -72,6 +73,33 @@ class TrackPage extends BasePage<TrackController> {
     );
   }
 
+  Widget buildMapFunView() {
+    return Obx(() {
+      return Positioned(
+        right: 0.w,
+        bottom: 110.w + controller.sheetProgress * 0.77.sh,
+        child: Column(
+          children: [
+            GestureDetector(
+              onTap: controller.onCurrentLocationClick,
+              child: Container(
+                  margin: EdgeInsets.only(right: 12.w),
+                  child: Opacity(
+                    opacity: controller.sheetProgress < 0.6
+                        ? 1.0
+                        : 1.0 -
+                            ((controller.sheetProgress - 0.6) / 0.4)
+                                .clamp(0.0, 1.0),
+                    child: Assets.images.iconMainRefreshMineLocation
+                        .image(width: 42.w, height: 42.w),
+                  )),
+            ),
+          ],
+        ),
+      );
+    });
+  }
+
   Widget buildBackBtnView() {
     return SafeArea(
       child: GestureDetector(
@@ -96,13 +124,14 @@ class TrackPage extends BasePage<TrackController> {
       height: 0.77.sh,
       width: double.infinity,
       child: Obx(() {
-        return DefaultTabController(
-          length: controller.daysList.length,
+        return Visibility(
+          visible: controller.tabController != null,
           child: Column(
             children: [
               SizedBox(
                 width: double.infinity,
                 child: TabBar(
+                  controller: controller.tabController,
                   tabAlignment: TabAlignment.start,
                   isScrollable: true,
                   dividerHeight: 0,
@@ -131,6 +160,7 @@ class TrackPage extends BasePage<TrackController> {
               ),
               Expanded(
                   child: TabBarView(
+                controller: controller.tabController,
                 children: List.generate(
                     controller.daysList.length,
                     (index) => TrackDayDetailView(controller.daysList[index],
@@ -191,182 +221,4 @@ class TrackPage extends BasePage<TrackController> {
       ],
     );
   }
-
-  Widget buildTrackHistoryContentView() {
-    return Column(
-      children: [
-        SizedBox(height: 10.w),
-        Builder(builder: (context) {
-          return GestureDetector(
-            onTap: () {
-              controller.onTrackStartTimeClick(context);
-            },
-            child: Padding(
-              padding: EdgeInsets.symmetric(vertical: 9.w),
-              child: Row(
-                children: [
-                  SizedBox(width: 14.w),
-                  Text(
-                    StringName.trackStartTime,
-                    style: TextStyle(
-                        fontSize: 14.sp,
-                        color: ColorName.black80,
-                        fontWeight: FontWeight.bold),
-                  ),
-                  Spacer(),
-                  Obx(() {
-                    return Text(
-                      controller.trackStartTime?.format('yyyy-MM-dd HH:mm') ??
-                          '',
-                      style:
-                          TextStyle(fontSize: 14.sp, color: ColorName.black50),
-                    );
-                  }),
-                  SizedBox(width: 6.w),
-                  Assets.images.iconTrackSelectTimeArrow
-                      .image(width: 16.w, height: 16.w),
-                  SizedBox(width: 14.w),
-                ],
-              ),
-            ),
-          );
-        }),
-        Builder(builder: (context) {
-          return GestureDetector(
-            onTap: () {
-              controller.onTrackEndTimeClick(context);
-            },
-            child: Padding(
-              padding: EdgeInsets.symmetric(vertical: 9.w),
-              child: Row(
-                children: [
-                  SizedBox(width: 14.w),
-                  Text(
-                    StringName.trackEndTime,
-                    style: TextStyle(
-                        fontSize: 14.sp,
-                        color: ColorName.black80,
-                        fontWeight: FontWeight.bold),
-                  ),
-                  Spacer(),
-                  Obx(() {
-                    return Text(
-                      controller.trackEndTime?.format('yyyy-MM-dd HH:mm') ?? '',
-                      style:
-                          TextStyle(fontSize: 14.sp, color: ColorName.black50),
-                    );
-                  }),
-                  SizedBox(width: 6.w),
-                  Assets.images.iconTrackSelectTimeArrow
-                      .image(width: 16.w, height: 16.w),
-                  SizedBox(width: 14.w),
-                ],
-              ),
-            ),
-          );
-        }),
-        SizedBox(height: 12.w),
-        Obx(() {
-          return Visibility(
-            visible: controller.startAddress != null ||
-                controller.endAddress != null,
-            child: Container(
-                padding: EdgeInsets.all(12.w),
-                margin: EdgeInsets.symmetric(horizontal: 14.w),
-                width: double.infinity,
-                decoration: BoxDecoration(
-                  color: '#E5F8F8F8'.color,
-                  borderRadius: BorderRadius.circular(12.w),
-                ),
-                child: Column(
-                  children: [
-                    buildAddressInfoView(
-                        '#12C172'.color,
-                        StringName.trackStartLocation,
-                        controller.startAddress ?? ''),
-                    Align(
-                        alignment: Alignment.centerLeft,
-                        child: Container(
-                            margin: EdgeInsets.only(left: 3.5.w),
-                            child: Assets.images.bgTrackLocationTie
-                                .image(width: 1.5.w))),
-                    buildAddressInfoView(
-                        '#F3353A'.color,
-                        StringName.trackEndLocation,
-                        controller.endAddress ?? ''),
-                  ],
-                )),
-          );
-        })
-      ],
-    );
-  }
-
-  Widget buildAddressInfoView(Color color, String title, String content) {
-    return Row(
-      children: [
-        Container(
-          width: 10.w,
-          height: 10.w,
-          decoration: BoxDecoration(
-            shape: BoxShape.circle,
-            border: Border.all(color: color, width: 2.w),
-          ),
-        ),
-        SizedBox(width: 11.w),
-        Text(title,
-            style: TextStyle(
-                fontSize: 13.sp,
-                color: ColorName.black80,
-                fontWeight: FontWeight.bold)),
-        Expanded(
-          child: Text(content,
-              style: TextStyle(fontSize: 13.sp, color: ColorName.black70)),
-        )
-      ],
-    );
-  }
-
-  Widget buildTrackNowContentView() {
-    return Column(
-      children: [
-        SizedBox(height: 20.w),
-        Row(
-          children: [
-            SizedBox(width: 12.w),
-            Assets.images.iconTrackLocationNow.image(width: 20.w, height: 20.w),
-            SizedBox(width: 3.w),
-            Obx(() {
-              return RelativeTimeText(
-                  startPerchText: '当前位置·',
-                  endPerchText:
-                      controller.currentLocation?.lastUpdateTime == null
-                          ? ''
-                          : '更新',
-                  timestamp: controller.currentLocation?.lastUpdateTime,
-                  updateInterval: Duration(minutes: 1),
-                  style: TextStyle(
-                      fontSize: 15.sp,
-                      color: '#333333'.color,
-                      fontWeight: FontWeight.bold));
-            })
-          ],
-        ),
-        SizedBox(height: 16.w),
-        Container(
-          width: double.infinity,
-          margin: EdgeInsets.symmetric(horizontal: 12.w),
-          padding: EdgeInsets.all(14.w),
-          decoration: BoxDecoration(
-            color: '#F9F9F9'.color,
-            borderRadius: BorderRadius.circular(6.w),
-          ),
-          child: Obx(() {
-            return Text(controller.currentLocation?.address ?? '--',
-                style: TextStyle(fontSize: 14.sp, color: '#666666'.color));
-          }),
-        )
-      ],
-    );
-  }
 }

+ 28 - 0
lib/module/track/track_util.dart

@@ -44,4 +44,32 @@ class TrackUtil {
       return '${minutes}min';
     }
   }
+
+  static List<LatLng> getTrackMovePoints(
+      List<AtmobTrackPoint>? pointsList, int startTime, int endTime) {
+    if (pointsList == null || pointsList.isEmpty) {
+      return [];
+    }
+    List<LatLng> movePoints = [];
+    for (var point in pointsList) {
+      if (point.time >= startTime && point.time <= endTime) {
+        movePoints
+            .add(LatLng(latitude: point.latitude, longitude: point.longitude));
+      }
+    }
+    return movePoints;
+  }
+
+  static LatLng? getTrackStartPoint(
+      List<AtmobTrackPoint>? pointsList, int startTime) {
+    if (pointsList == null || pointsList.isEmpty) {
+      return null;
+    }
+    for (var point in pointsList) {
+      if (point.time >= startTime) {
+        return LatLng(latitude: point.latitude, longitude: point.longitude);
+      }
+    }
+    return null;
+  }
 }

+ 59 - 0
lib/utils/async_util.dart

@@ -138,6 +138,65 @@ class AsyncUtil {
     }
     return controller;
   }
+
+  static CancelableFuture<List<AsyncResult<T>>> waitForAll<T>(
+      Iterable<Future<T>> futures) {
+    final completers = futures.map((_) => Completer<void>()).toList();
+    final results = <AsyncResult<T>>[];
+    bool isCancelled = false;
+
+    void attempt(Completer<List<AsyncResult<T>>> completer) async {
+      for (int i = 0; i < futures.length; i++) {
+        if (isCancelled) {
+          completer.completeError(CancelledError());
+          return;
+        }
+        try {
+          final value = await futures.elementAt(i);
+          results.add(AsyncResult.success(value));
+        } catch (e, stackTrace) {
+          results.add(AsyncResult.failure(e, stackTrace));
+        } finally {
+          completers[i].complete();
+        }
+      }
+      await Future.wait(completers.map((c) => c.future));
+      if (!completer.isCompleted) {
+        completer.complete(results);
+      }
+    }
+
+    return CancelableFuture<List<AsyncResult<T>>>((completer) {
+      attempt(completer);
+    }, () {
+      isCancelled = true;
+    });
+  }
+}
+
+class AsyncResult<T> {
+  final T? value;
+  final Object? error;
+  final StackTrace? stackTrace;
+
+  bool get isSuccess => error == null;
+
+  AsyncResult.success(this.value)
+      : error = null,
+        stackTrace = null;
+
+  AsyncResult.failure(this.error, this.stackTrace) : value = null;
+
+  void handle({
+    required void Function(T) onSuccess,
+    required void Function(Object, StackTrace?) onError,
+  }) {
+    if (isSuccess) {
+      onSuccess(value as T);
+    } else {
+      onError(error!, stackTrace);
+    }
+  }
 }
 
 abstract interface class Cancelable {

+ 3 - 1
plugins/map/lib/flutter_map.dart

@@ -9,15 +9,17 @@ export 'package:flutter_map/src/core/flutter_map.dart';
 
 //公共配置.
 export 'package:flutter_map/src/consts/map_constants.dart';
+export 'package:flutter_map/src/consts/marker_type.dart';
+export 'package:flutter_map/src/consts/polyline_type.dart';
 
 //实体类
-export 'package:flutter_map/src/consts/marker_type.dart';
 export 'package:flutter_map/src/entity/marker.dart';
 export 'package:flutter_map/src/entity/camera_position.dart';
 export 'package:flutter_map/src/entity/map_location.dart';
 export 'package:flutter_map/src/entity/trace_location.dart';
 export 'package:flutter_map/src/entity/lat_lng.dart';
 export 'package:flutter_map/src/entity/map_padding.dart';
+export 'package:flutter_map/src/entity/popup.dart';
 
 //接口
 export 'package:flutter_map/src/interface/map_sdk_interface.dart';

+ 4 - 0
plugins/map/lib/src/consts/map_constants.dart

@@ -24,15 +24,19 @@ class MapConstants {
   static const String methodMapMoveCamera = "map#moveCamera";
   static const String methodMapAnimateCamera = "map#animateCamera";
   static const String methodMapClear = "map#clear";
+  static const String methodMoveToSuitableLocation =
+      "map#moveToSuitableLocation";
 
   //标记物相关方法名称
   static const String methodUpdateOrAddMarkers = "marker#updateOrAddMarkers";
   static const String methodReplaceAllMarkers = "marker#replaceAllMarkers";
   static const String methodMarkerOnTap = "marker#onTap";
+  static const String methodMarkerRemoveMarker = "marker#removeMarker";
 
   //轨迹纠偏
   static const String methodQueryProcessedTrace = "trace#queryProcessedTrace";
 
   //polyline
   static const String methodAddPolyline = "polyline#addPolyline";
+  static const String methodRemovePolyline = "polyline#removePolyline";
 }

+ 4 - 1
plugins/map/lib/src/consts/marker_type.dart

@@ -5,7 +5,10 @@ enum MarkerType {
   friend(2),
   traceStartPoint(3),
   traceEndFriendPoint(4),
-  traceEndMinePoint(5);
+  traceEndMinePoint(5),
+  tracePopupPoint(6), //popup气泡样式
+  tracePassingPoint(7), //轨迹途径点样式
+  trackErrorPoint(8); //轨迹错误点样式
 
   final int value;
 

+ 9 - 0
plugins/map/lib/src/consts/polyline_type.dart

@@ -0,0 +1,9 @@
+enum PolylineType {
+  normal('normal'),
+  error('error'),
+  selected('selected');
+
+  final String value;
+
+  const PolylineType(this.value);
+}

+ 61 - 4
plugins/map/lib/src/core/map_controller.dart

@@ -13,6 +13,7 @@ import 'package:flutter_map/src/interface/map_fun_interface.dart';
 import 'package:flutter_map/src/interface/map_marker_interface.dart';
 import 'package:flutter_map/src/interface/map_polyline_interface.dart';
 
+import '../consts/polyline_type.dart';
 import '../entity/map_padding.dart';
 
 class MapController
@@ -119,12 +120,16 @@ class MapController
   }
 
   @override
-  void addPolyline(List<LatLng> points,
-      {bool isAnimateCamera = true, MapPadding? mapPadding}) {
+  void addPolyline(String lineId, List<LatLng> points,
+      {bool isAnimateCamera = true,
+      PolylineType lineType = PolylineType.normal,
+      MapPadding? mapPadding}) {
     if (_isDisposed || points.isEmpty) return;
     Map<String, dynamic> map = {};
+    map['lineId'] = lineId;
     map['points'] = _encodeJson(points);
     map['isAnimateCamera'] = isAnimateCamera;
+    map['lineType'] = lineType.value;
     if (mapPadding != null) {
       map['mapPadding'] = _encodeJson(mapPadding);
     }
@@ -150,16 +155,68 @@ class MapController
     }
   }
 
+  @override
+  void moveToSuitableLocation(List<LatLng> points, {MapPadding? mapPadding}) {
+    if (_isDisposed || points.isEmpty) return;
+    Map<String, dynamic> map = {};
+    map['points'] = _encodeJson(points);
+    if (mapPadding != null) {
+      map['mapPadding'] = _encodeJson(mapPadding);
+    }
+    final params = {
+      'method': MapConstants.methodMoveToSuitableLocation,
+      'args': map
+    };
+    debugPrint("moveToSuitableLocation...params==>$params");
+    if (_channel != null) {
+      _executeMethod(params);
+    } else {
+      _pendingOperations.add(params);
+    }
+  }
+
+  @override
+  void removeMarker(String markerId) {
+    if (_isDisposed) return;
+    final params = {
+      'method': MapConstants.methodMarkerRemoveMarker,
+      'args': {'markerId': markerId}
+    };
+    debugPrint("removeMarker...params==>$params");
+    if (_channel != null) {
+      _executeMethod(params);
+    } else {
+      _pendingOperations.add(params);
+    }
+  }
+
+  @override
+  void removePolyline(String lineId) {
+    if (_isDisposed) return;
+    final params = {
+      'method': MapConstants.methodRemovePolyline,
+      'args': {'lineId': lineId}
+    };
+    debugPrint("removePolyline...params==>$params");
+    if (_channel != null) {
+      _executeMethod(params);
+    } else {
+      _pendingOperations.add(params);
+    }
+  }
+
   /// iOS 需要一个Map 而不是json string
   dynamic _encodeJson(dynamic object) {
     // iOS平台直接返回对象或转换为JSON对象
     if (Platform.isIOS) {
       if (object is List) {
-        return object.map((item) => item is Codable ? item.toJson() : item).toList();
+        return object
+            .map((item) => item is Codable ? item.toJson() : item)
+            .toList();
       }
       return object is Codable ? object.toJson() : object;
     }
-    
+
     // 其他平台序列化为JSON字符串
     return jsonEncode(object);
   }

+ 6 - 1
plugins/map/lib/src/entity/marker.dart

@@ -1,5 +1,5 @@
 import '../consts/marker_type.dart';
-import "codable.dart";  
+import "codable.dart";
 
 class Marker implements Codable {
   String id;
@@ -16,6 +16,8 @@ class Marker implements Codable {
 
   String? customAvatarUrl;
 
+  dynamic tags;
+
   Marker({
     required this.id,
     required this.markerName,
@@ -24,6 +26,7 @@ class Marker implements Codable {
     required this.markerType,
     this.isSelected = false,
     this.customAvatarUrl,
+    this.tags,
   });
 
   @override
@@ -36,6 +39,7 @@ class Marker implements Codable {
       'markerType': markerType.value,
       'isSelected': isSelected,
       'customAvatarUrl': customAvatarUrl,
+      'tags': tags,
     };
   }
 
@@ -48,6 +52,7 @@ class Marker implements Codable {
       markerType: MarkerType.fromValue(map['markerType'] as int),
       isSelected: map['isSelected'],
       customAvatarUrl: map['customAvatarUrl'] as String?,
+      tags: map['tags'] ? map['tags'] : null,
     );
   }
 }

+ 27 - 0
plugins/map/lib/src/entity/popup.dart

@@ -0,0 +1,27 @@
+import 'codable.dart';
+
+class Popup implements Codable {
+
+  String title;
+  String desc;
+
+  Popup({
+    required this.title,
+    required this.desc,
+  });
+
+  @override
+  Map<String, dynamic> toJson() {
+    return {
+      'title': title,
+      'desc': desc,
+    };
+  }
+
+  factory Popup.fromJson(Map<String, dynamic> map) {
+    return Popup(
+      title: map['title'] as String,
+      desc: map['desc'] as String,
+    );
+  }
+}

+ 3 - 0
plugins/map/lib/src/interface/map_fun_interface.dart

@@ -9,4 +9,7 @@ abstract class MapFunInterface {
 
   //清除地图所有标记
   void clear();
+
+  //移动至多个点的位置,并提供设置padding距离
+  moveToSuitableLocation(List<LatLng> points, {MapPadding? mapPadding});
 }

+ 3 - 0
plugins/map/lib/src/interface/map_marker_interface.dart

@@ -11,4 +11,7 @@ abstract class MapMarkerInterface {
   void replaceAllMarkers(List<Marker> markers);
 
   void clearAllMarkers();
+
+  //清除指定标记物
+  void removeMarker(String markerId);
 }

+ 6 - 2
plugins/map/lib/src/interface/map_polyline_interface.dart

@@ -1,9 +1,13 @@
 import 'package:flutter_map/src/entity/map_padding.dart';
 
 import '../../flutter_map.dart';
+import '../consts/polyline_type.dart';
 
 abstract class MapPolylineInterface {
   //画线   isAnimateCamera 是否动画移动到线的起点 ,mapPadding 地图边距 单位dp
-  void addPolyline(List<LatLng> points,
-      {bool isAnimateCamera = true, MapPadding? mapPadding});
+  void addPolyline(String lineId, List<LatLng> points,
+      {bool isAnimateCamera, PolylineType lineType, MapPadding? mapPadding});
+
+  //清除制定的线
+  void removePolyline(String lineId);
 }

+ 90 - 1
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/amap/MapController.java

@@ -5,12 +5,21 @@ import android.content.Context;
 import androidx.annotation.NonNull;
 
 import com.amap.api.maps.AMap;
+import com.amap.api.maps.CameraUpdate;
 import com.amap.api.maps.CameraUpdateFactory;
+import com.amap.api.maps.model.LatLng;
+import com.amap.api.maps.model.LatLngBounds;
+import com.atmob.map_amap_android.bean.MapPadding;
 import com.atmob.map_amap_android.contants.Constants;
 import com.atmob.map_amap_android.overlays.MyMethodCallHandler;
+import com.atmob.map_amap_android.util.GsonUtil;
 import com.atmob.map_amap_android.util.LogUtil;
 import com.atmob.map_amap_android.util.ParamUtil;
+import com.atmob.map_amap_android.util.SizeUtil;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
 
+import java.util.List;
 import java.util.Map;
 
 import io.flutter.plugin.common.MethodCall;
@@ -26,10 +35,25 @@ public class MapController implements MyMethodCallHandler {
 
     private final MethodChannel methodChannel;
 
+    int nowMapTypeIndex = 0;
+
+    private final Gson gson;
+
+    private final int[] mapType = {
+            AMap.MAP_TYPE_NORMAL,
+            AMap.MAP_TYPE_SATELLITE,
+            AMap.MAP_TYPE_NIGHT,
+            AMap.MAP_TYPE_NAVI,
+            AMap.MAP_TYPE_BUS,
+            AMap.MAP_TYPE_NAVI_NIGHT,
+    };
+
     public MapController(Context context, MethodChannel methodChannel, AMap map) {
         this.context = context;
         this.methodChannel = methodChannel;
         this.map = map;
+        map.setMapType(mapType[nowMapTypeIndex]);
+        gson = GsonUtil.getInstance();
     }
 
 
@@ -42,12 +66,77 @@ public class MapController implements MyMethodCallHandler {
             case Constants.METHOD_ANIMATE_CAMERA:
                 animateCamera(call, result);
                 break;
-                case Constants.METHOD_MAP_CLEAR:
+            case Constants.METHOD_MAP_CLEAR:
                 clearMap(result);
                 break;
+            case Constants.METHOD_MAP_MOVE_TO_SUITABLE_LOCATION:
+                animateSuitableLocation(call, result);
+                break;
+        }
+    }
+
+    private void animateSuitableLocation(MethodCall call, MethodChannel.Result result) {
+        try {
+            Map<String, Object> arguments = call.arguments();
+            if (arguments == null) {
+                result.error("arguments is null", null, null);
+                return;
+            }
+            boolean isAnimateCamera = ParamUtil.getBoolean(arguments, "isAnimateCamera", true);
+            LogUtil.d(TAG, "isAnimateCamera===>" + isAnimateCamera);
+            String points = (String) arguments.get("points");
+            if (points == null || points.isEmpty()) {
+                result.error("latLngList is null", null, null);
+                return;
+            }
+            List<LatLng> latLngList = gson.fromJson(points, new TypeToken<List<LatLng>>() {
+            }.getType());
+            LogUtil.d(TAG, "latLngList===>" + latLngList);
+            if (latLngList == null || latLngList.isEmpty()) {
+                result.error("latLngList is empty", null, null);
+                return;
+            }
+            String paddingStr = (String) arguments.get("mapPadding");
+
+            MapPadding padding = null;
+            if (paddingStr != null && !paddingStr.isEmpty()) {
+                padding = gson.fromJson(paddingStr, MapPadding.class);
+            }
+            LogUtil.d(TAG, "padding===>" + padding);
+            moveToSuitableLocation(latLngList, padding);
+            result.success(null);
+        } catch (Exception e) {
+            LogUtil.d(TAG, "addPolyline error===>" + e.getMessage());
+            result.error("JsonSyntaxException", e.getMessage(), null);
         }
     }
 
+    private void moveToSuitableLocation(List<LatLng> points, MapPadding padding) {
+        if (map == null) {
+            return;
+        }
+        LatLngBounds.Builder builder = LatLngBounds.builder();
+        for (int i = 0; i < points.size(); i++) {
+            LatLng latLng = points.get(i);
+            builder.include(latLng);
+        }
+        LatLngBounds bounds = builder.build();
+        int left = 0;
+        int top = 0;
+        int right = 0;
+        int bottom = 0;
+        if (padding != null) {
+            left = (int) SizeUtil.dp2px(context, padding.getLeft());
+            top = (int) SizeUtil.dp2px(context, padding.getTop());
+            right = (int) SizeUtil.dp2px(context, padding.getRight());
+            bottom = (int) SizeUtil.dp2px(context, padding.getBottom());
+        }
+        LogUtil.d(TAG, "left===>" + left + "  top===>" + top + "  right===>" + right + "  bottom===>" + bottom);
+        CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngBoundsRect(bounds, left, right, top, bottom);
+        map.animateCamera(cameraUpdate);
+    }
+
+
     private void clearMap(MethodChannel.Result result) {
         LogUtil.i(TAG, "clearMap");
         map.clear();

+ 15 - 2
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/bean/MakerInfo.java

@@ -34,6 +34,16 @@ public class MakerInfo {
     @SerializedName("customAvatarUrl")
     private String customAvatarUrl;
 
+    @SerializedName("tags")
+    private Object tags;
+
+    public Object getTags() {
+        return tags;
+    }
+
+    public void setTags(Object tags) {
+        this.tags = tags;
+    }
 
     public String getCustomAvatarUrl() {
         return customAvatarUrl;
@@ -92,14 +102,16 @@ public class MakerInfo {
     }
 
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({MarkerType.MINE, MarkerType.FRIEND, MarkerType.TRACE_START_POINT, MarkerType.TRACE_END_FRIEND_POINT, MarkerType.TRACE_END_MINE_POINT,MarkerType.TRACE_SELECT_POINT})
+    @IntDef({MarkerType.MINE, MarkerType.FRIEND, MarkerType.TRACE_START_POINT, MarkerType.TRACE_END_FRIEND_POINT, MarkerType.TRACE_END_MINE_POINT, MarkerType.TRACE_POPUP_POINT, MarkerType.TRACE_PASSING_POINT, MarkerType.TRACE_ERROR_POINT})
     public @interface MarkerType {
         int MINE = 1;
         int FRIEND = 2;
         int TRACE_START_POINT = 3;
         int TRACE_END_FRIEND_POINT = 4;
         int TRACE_END_MINE_POINT = 5;
-        int TRACE_SELECT_POINT = 6;
+        int TRACE_POPUP_POINT = 6;
+        int TRACE_PASSING_POINT = 7;
+        int TRACE_ERROR_POINT = 8;
     }
 
 
@@ -114,6 +126,7 @@ public class MakerInfo {
                 ", markerType=" + markerType +
                 ", isSelected=" + isSelected +
                 ", customAvatarUrl='" + customAvatarUrl + '\'' +
+                ", tags=" + tags +
                 '}';
     }
 }

+ 56 - 0
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/bean/Popup.java

@@ -0,0 +1,56 @@
+package com.atmob.map_amap_android.bean;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.Objects;
+
+public class Popup {
+
+    @SerializedName("title")
+    private String title;
+
+    @SerializedName("desc")
+    private String desc;
+
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    public void setDesc(String desc) {
+        this.desc = desc;
+    }
+
+
+    @Override
+    public String toString() {
+        return "Popup{" +
+                "title='" + title + '\'' +
+                ", desc='" + desc + '\'' +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Popup popup = (Popup) o;
+
+        return Objects.equals(title, popup.title) &&
+                Objects.equals(desc, popup.desc);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(title, desc);
+    }
+}

+ 6 - 5
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/contants/Constants.java

@@ -24,20 +24,19 @@ public class Constants {
     public static final String METHOD_START_LOCATION = "startLocation";//开启定位
 
 
-
     /**
      * 轨迹
      */
     public static final String METHOD_QUERY_PROCESSED_TRACE = "trace#queryProcessedTrace";
 
 
-
     /***********************************************地图操作相关********************************/
     public static final String METHOD_MOVE_CAMERA = "map#moveCamera";
     public static final String METHOD_ANIMATE_CAMERA = "map#animateCamera";
     public static final String METHOD_MAP_CLEAR = "map#clear";
+    public static final String METHOD_MAP_MOVE_TO_SUITABLE_LOCATION = "map#moveToSuitableLocation";
 
-    public static final String[] METHOD_ID_LIST_FOR_MAP = {METHOD_MOVE_CAMERA, METHOD_ANIMATE_CAMERA,METHOD_MAP_CLEAR};
+    public static final String[] METHOD_ID_LIST_FOR_MAP = {METHOD_MOVE_CAMERA, METHOD_ANIMATE_CAMERA, METHOD_MAP_CLEAR, METHOD_MAP_MOVE_TO_SUITABLE_LOCATION};
 
     /**
      * markers
@@ -45,17 +44,19 @@ public class Constants {
     public static final String METHOD_UPDATE_OR_ADD_MARKERS = "marker#updateOrAddMarkers";
     public static final String METHOD_REPLACE_ALL_MARKERS = "marker#replaceAllMarkers";
     public static final String METHOD_MARKER_ON_TAP = "marker#onTap";
+    public static final String METHOD_REMOVE_MARKER = "marker#removeMarker";
 
 
-    public static final String[] METHOD_ID_LIST_FOR_MARKER = {METHOD_UPDATE_OR_ADD_MARKERS, METHOD_REPLACE_ALL_MARKERS, METHOD_MARKER_ON_TAP};
+    public static final String[] METHOD_ID_LIST_FOR_MARKER = {METHOD_UPDATE_OR_ADD_MARKERS, METHOD_REPLACE_ALL_MARKERS, METHOD_MARKER_ON_TAP, METHOD_REMOVE_MARKER};
 
 
     /**
      * 线
      */
     public static final String METHOD_UPDATE_OR_ADD_POLYLINE = "polyline#addPolyline";
+    public static final String METHOD_REMOVE_POLYLINE = "polyline#removePolyline";
 
-    public static final String[] METHOD_ID_LIST_FOR_POLYLINE = {METHOD_UPDATE_OR_ADD_POLYLINE};
+    public static final String[] METHOD_ID_LIST_FOR_POLYLINE = {METHOD_UPDATE_OR_ADD_POLYLINE, METHOD_REMOVE_POLYLINE};
 
 
 }

+ 86 - 31
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/overlays/marker/MarkersController.java

@@ -18,20 +18,26 @@ import com.amap.api.maps.model.Marker;
 import com.amap.api.maps.model.MarkerOptions;
 import com.atmob.map_amap_android.R;
 import com.atmob.map_amap_android.bean.MakerInfo;
+import com.atmob.map_amap_android.bean.Popup;
 import com.atmob.map_amap_android.contants.Constants;
 import com.atmob.map_amap_android.databinding.ItemLocationMarkerBinding;
+import com.atmob.map_amap_android.databinding.ItemTrackErrorMarkerBinding;
+import com.atmob.map_amap_android.databinding.ItemTrackPassingMarkerBinding;
+import com.atmob.map_amap_android.databinding.ItemTrackSelectedMarkerBinding;
 import com.atmob.map_amap_android.databinding.ItemTrackStartMarkerBinding;
 import com.atmob.map_amap_android.overlays.MyMethodCallHandler;
 import com.atmob.map_amap_android.util.BitmapCallback;
 import com.atmob.map_amap_android.util.GsonUtil;
 import com.atmob.map_amap_android.util.ImageCacheLoader;
 import com.atmob.map_amap_android.util.LogUtil;
+import com.atmob.map_amap_android.util.ParamUtil;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 import io.flutter.plugin.common.MethodCall;
@@ -72,9 +78,30 @@ public class MarkersController implements MyMethodCallHandler, AMap.OnMarkerClic
             case Constants.METHOD_REPLACE_ALL_MARKERS:
                 replaceAllMarkers(call, result);
                 break;
+            case Constants.METHOD_REMOVE_MARKER:
+                removeMarker(call, result);
+                break;
         }
     }
 
+
+    private void removeMarker(MethodCall call, MethodChannel.Result result) {
+        LogUtil.i(TAG, "removeMarker===>" + call.arguments());
+        Map<String, Object> arguments = call.arguments();
+        if (arguments == null || arguments.isEmpty()) {
+            result.error("-1", "removeMarker.arguments is empty", null);
+            return;
+        }
+        String markerId = ParamUtil.getString(arguments, "markerId");
+        Marker marker = currentMarkers.get(markerId);
+        if (marker != null) {
+            marker.remove();
+            currentMarkers.remove(markerId);
+            LogUtil.i(TAG, "removeMarker===>成功删除marker:" + markerId);
+        }
+        result.success(null);
+    }
+
     private void replaceAllMarkers(MethodCall call, MethodChannel.Result result) {
         LogUtil.i(TAG, "replaceAllMarkers===>" + call.arguments());
         try {
@@ -154,32 +181,38 @@ public class MarkersController implements MyMethodCallHandler, AMap.OnMarkerClic
             Object object = marker.getObject();
             if (object instanceof MakerInfo) {
                 MakerInfo cacheInfo = (MakerInfo) object;
-                boolean nameChanged = !Objects.equals(cacheInfo.getMarkerName(), makerInfo.getMarkerName());
-                boolean selectedChanged = cacheInfo.isSelected() != makerInfo.isSelected();
-                boolean avatarChanged = !Objects.equals(cacheInfo.getCustomAvatarUrl(), makerInfo.getCustomAvatarUrl());
-
-                if (nameChanged || selectedChanged || avatarChanged) {
-                    LogUtil.i(TAG, "updateMarkers==修改头像");
-                    ImageCacheLoader.loadBitmapAsync(context, makerInfo.getCustomAvatarUrl(), new BitmapCallback() {
-                        @Override
-                        public void onStartLoading() {}
-
-                        @Override
-                        public void onBitmapLoaded(Bitmap bitmap) {
-                            BitmapDescriptor markerBitmap = getMarkerBitmap(makerInfo, bitmap);
-                            marker.setIcon(markerBitmap);
-                            LogUtil.i(TAG, "updateMarkers=成功修改头像:id=" + makerInfo.getId());
-                        }
-
-                        @Override
-                        public void onError(String errorMessage) {
-                            LogUtil.i(TAG, "updateMarkers==修改失败:" + errorMessage);
-                        }
-                    });
-                    marker.setObject(makerInfo);
+                if (cacheInfo.getMarkerType() == MakerInfo.MarkerType.TRACE_POPUP_POINT) {
+                    BitmapDescriptor markerBitmap = getMarkerBitmap(makerInfo, null);
+                    marker.setIcon(markerBitmap);
+                    marker.setZIndex(999);
+                } else {
+                    boolean nameChanged = !Objects.equals(cacheInfo.getMarkerName(), makerInfo.getMarkerName());
+                    boolean selectedChanged = cacheInfo.isSelected() != makerInfo.isSelected();
+                    boolean avatarChanged = !Objects.equals(cacheInfo.getCustomAvatarUrl(), makerInfo.getCustomAvatarUrl());
+
+                    if (nameChanged || selectedChanged || avatarChanged) {
+                        LogUtil.i(TAG, "updateMarkers==修改头像");
+                        ImageCacheLoader.loadBitmapAsync(context, makerInfo.getCustomAvatarUrl(), new BitmapCallback() {
+                            @Override
+                            public void onStartLoading() {
+                            }
+
+                            @Override
+                            public void onBitmapLoaded(Bitmap bitmap) {
+                                BitmapDescriptor markerBitmap = getMarkerBitmap(makerInfo, bitmap);
+                                marker.setIcon(markerBitmap);
+                                LogUtil.i(TAG, "updateMarkers=成功修改头像:id=" + makerInfo.getId());
+                            }
+
+                            @Override
+                            public void onError(String errorMessage) {
+                                LogUtil.i(TAG, "updateMarkers==修改失败:" + errorMessage);
+                            }
+                        });
+                    }
+                    marker.setZIndex(makerInfo.isSelected() ? 100 : 0);
                 }
-
-                marker.setZIndex(makerInfo.isSelected() ? 999 : 0);
+                marker.setObject(makerInfo);
             }
         } else {
             // 创建新 marker
@@ -195,7 +228,8 @@ public class MarkersController implements MyMethodCallHandler, AMap.OnMarkerClic
             // 加载头像后再添加 marker
             ImageCacheLoader.loadBitmapAsync(context, makerInfo.getCustomAvatarUrl(), new BitmapCallback() {
                 @Override
-                public void onStartLoading() {}
+                public void onStartLoading() {
+                }
 
                 @Override
                 public void onBitmapLoaded(Bitmap bitmap) {
@@ -213,7 +247,6 @@ public class MarkersController implements MyMethodCallHandler, AMap.OnMarkerClic
     }
 
 
-
     private void addMarkerToMap(MakerInfo makerInfo, LatLng latLng, Bitmap avatarBitmap) {
         BitmapDescriptor markerBitmap = getMarkerBitmap(makerInfo, avatarBitmap);
         MarkerOptions markerOption = new MarkerOptions()
@@ -221,6 +254,12 @@ public class MarkersController implements MyMethodCallHandler, AMap.OnMarkerClic
                 .icon(markerBitmap)
                 .anchor(0.5f, 0.9f);
 
+        if (makerInfo.getMarkerType() == MakerInfo.MarkerType.TRACE_PASSING_POINT || makerInfo.getMarkerType() == MakerInfo.MarkerType.TRACE_ERROR_POINT) {
+            markerOption.anchor(0.5f, 0.5f);
+        } else if (makerInfo.getMarkerType() == MakerInfo.MarkerType.TRACE_POPUP_POINT) {
+            markerOption.anchor(0.5f, 1.1f);
+        }
+
         Marker newMarker = map.addMarker(markerOption);
         newMarker.setObject(makerInfo);
         newMarker.setZIndex(makerInfo.isSelected() ? 999 : 0);
@@ -228,8 +267,6 @@ public class MarkersController implements MyMethodCallHandler, AMap.OnMarkerClic
     }
 
 
-
-
     @Override
     public String[] getRegisterMethodIdArray() {
         return Constants.METHOD_ID_LIST_FOR_MARKER;
@@ -248,9 +285,9 @@ public class MarkersController implements MyMethodCallHandler, AMap.OnMarkerClic
             } else {
                 markerBinding.ivMarkerAvatar.setImageBitmap(customAvatarBitmap);
             }
-            if(markerInfo.getMarkerType() == MakerInfo.MarkerType.MINE){
+            if (markerInfo.getMarkerType() == MakerInfo.MarkerType.MINE) {
                 markerBinding.vBottom.setBackgroundResource(markerInfo.isSelected() ? R.drawable.icon_mine_marker_selected_bottom_circle : R.drawable.icon_mine_marker_normal_bottom_circle);
-            }else{
+            } else {
                 markerBinding.vBottom.setBackgroundResource(markerInfo.isSelected() ? R.drawable.icon_marker_selected_bottom_circle : R.drawable.icon_marker_normal_bottom_circle);
             }
             markerBinding.locationMarkerIvBg.setImageResource(markerInfo.isSelected() ? (markerInfo.getMarkerType() == MakerInfo.MarkerType.MINE ? R.drawable.icon_location_mine_marker_selected : R.drawable.icon_location_marker_selected) : R.drawable.icon_location_marker_normal);
@@ -271,6 +308,24 @@ public class MarkersController implements MyMethodCallHandler, AMap.OnMarkerClic
             trackEndPointMarkerBinding.locationMarkerIvBg.setImageResource(R.drawable.icon_location_marker_normal);
             trackEndPointMarkerBinding.vBottom.setBackgroundResource(R.drawable.icon_marker_track_bottom_circle);
             return viewGetBitmapDescriptor(view);
+        } else if (markerInfo.getMarkerType() == MakerInfo.MarkerType.TRACE_PASSING_POINT) {
+            ItemTrackPassingMarkerBinding trackPassingMarkerBinding = ItemTrackPassingMarkerBinding.inflate(LayoutInflater.from(context));
+            ConstraintLayout view = trackPassingMarkerBinding.getRoot();
+            return viewGetBitmapDescriptor(view);
+        } else if (markerInfo.getMarkerType() == MakerInfo.MarkerType.TRACE_POPUP_POINT) {
+            ItemTrackSelectedMarkerBinding trackSelectedMarkerBinding = ItemTrackSelectedMarkerBinding.inflate(LayoutInflater.from(context));
+            Object tags = markerInfo.getTags();
+            if (tags instanceof Map) {
+                Map<String, Object> tagsMap = (Map<String, Object>) tags;
+                trackSelectedMarkerBinding.tvPopupTitle.setText(ParamUtil.getString(tagsMap, "title"));
+                trackSelectedMarkerBinding.tvPopupDesc.setText(ParamUtil.getString(tagsMap, "desc"));
+            }
+            ConstraintLayout view = trackSelectedMarkerBinding.getRoot();
+            return viewGetBitmapDescriptor(view);
+        } else if (markerInfo.getMarkerType() == MakerInfo.MarkerType.TRACE_ERROR_POINT) {
+            ItemTrackErrorMarkerBinding trackPassingMarkerBinding = ItemTrackErrorMarkerBinding.inflate(LayoutInflater.from(context));
+            ConstraintLayout view = trackPassingMarkerBinding.getRoot();
+            return viewGetBitmapDescriptor(view);
         }
         return null;
     }

+ 44 - 5
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/overlays/polyline/PolylineController.java

@@ -23,8 +23,10 @@ import com.atmob.map_amap_android.util.SizeUtil;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 import io.flutter.plugin.common.MethodCall;
 import io.flutter.plugin.common.MethodChannel;
@@ -38,6 +40,8 @@ public class PolylineController implements MyMethodCallHandler {
 
     private final Context context;
 
+    private final HashMap<String, Polyline> currentPolylineMap = new HashMap<>(10);
+
     private final MethodChannel methodChannel;
     private final Gson gson;
 
@@ -55,6 +59,30 @@ public class PolylineController implements MyMethodCallHandler {
             case Constants.METHOD_UPDATE_OR_ADD_POLYLINE:
                 addPolyline(call, result);
                 break;
+            case Constants.METHOD_REMOVE_POLYLINE:
+                removePolyLine(call, result);
+                break;
+        }
+    }
+
+    private void removePolyLine(MethodCall call, MethodChannel.Result result) {
+        try {
+            Map<String, Object> arguments = call.arguments();
+            if (arguments == null) {
+                result.error("arguments is null", null, null);
+                return;
+            }
+            String lineId = ParamUtil.getString(arguments, "lineId");
+            LogUtil.d(TAG, "removePolyline lineId===>" + lineId);
+            Polyline polyline = currentPolylineMap.get(lineId);
+            if (polyline != null) {
+                polyline.remove();
+                currentPolylineMap.remove(lineId);
+            }
+            result.success(null);
+        } catch (Exception e) {
+            LogUtil.d(TAG, "removePolyline error===>" + e.getMessage());
+            result.error("JsonSyntaxException", e.getMessage(), null);
         }
     }
 
@@ -65,6 +93,7 @@ public class PolylineController implements MyMethodCallHandler {
                 result.error("arguments is null", null, null);
                 return;
             }
+            String lineId = ParamUtil.getString(arguments, "lineId");
             boolean isAnimateCamera = ParamUtil.getBoolean(arguments, "isAnimateCamera", true);
             LogUtil.d(TAG, "isAnimateCamera===>" + isAnimateCamera);
             String points = (String) arguments.get("points");
@@ -79,7 +108,8 @@ public class PolylineController implements MyMethodCallHandler {
                 result.error("latLngList is empty", null, null);
                 return;
             }
-            drawTrackByPoints(latLngList);
+            String lineType = ParamUtil.getString(arguments, "lineType");
+            drawTrackByPoints(lineId, latLngList, lineType);
             if (isAnimateCamera) {
                 String paddingStr = (String) arguments.get("mapPadding");
 
@@ -97,12 +127,20 @@ public class PolylineController implements MyMethodCallHandler {
         }
     }
 
-    private Polyline drawTrackByPoints(List<LatLng> points) {
+    private void drawTrackByPoints(String lineId, List<LatLng> points, @PolylineType String lineType) {
         if (map == null || points == null || points.isEmpty()) {
-            return null;
+            return;
+        }
+        int lineDrawableId;
+        if (Objects.equals(PolylineType.ERROR, lineType)) {
+            lineDrawableId = R.drawable.bg_map_error_line;
+        } else if (Objects.equals(PolylineType.SELECTED, lineType)) {
+            lineDrawableId = R.drawable.bg_map_selected_line;
+        } else {
+            lineDrawableId = R.drawable.bg_map_line;
         }
         PolylineOptions polylineOptions = new PolylineOptions()
-                .setCustomTexture(BitmapDescriptorFactory.fromResource(R.drawable.bg_map_line))
+                .setCustomTexture(BitmapDescriptorFactory.fromResource(lineDrawableId))
                 .setUseTexture(true)
                 .width(SizeUtil.dp2px(context, 8));
 
@@ -115,7 +153,8 @@ public class PolylineController implements MyMethodCallHandler {
                 lastPoint = currentPoint;
             }
         }
-        return map.addPolyline(polylineOptions);
+        Polyline polyline = map.addPolyline(polylineOptions);
+        currentPolylineMap.put(lineId, polyline);
     }
 
     private void moveToSuitableLocation(List<LatLng> points, MapPadding padding) {

+ 15 - 0
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/overlays/polyline/PolylineType.java

@@ -0,0 +1,15 @@
+package com.atmob.map_amap_android.overlays.polyline;
+
+import androidx.annotation.StringDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.SOURCE)
+@StringDef({PolylineType.NORMAL, PolylineType.ERROR, PolylineType.SELECTED})
+public @interface PolylineType {
+
+    String NORMAL = "normal";
+    String ERROR = "error";
+    String SELECTED = "selected";
+}

BIN
plugins/map_amap_android/android/src/main/res/drawable-xxhdpi/bg_map_error_line.webp


BIN
plugins/map_amap_android/android/src/main/res/drawable-xxhdpi/bg_map_selected_line.webp


BIN
plugins/map_amap_android/android/src/main/res/drawable-xxhdpi/bg_track_popup_bubble.9.png


BIN
plugins/map_amap_android/android/src/main/res/drawable-xxhdpi/icon_track_stay.webp


+ 6 - 0
plugins/map_amap_android/android/src/main/res/drawable/bg_track_error_point.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <corners android:radius="100dp" />
+    <solid android:color="#2BFF5555" />
+</shape>

+ 9 - 0
plugins/map_amap_android/android/src/main/res/drawable/icon_marker_error.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <corners android:radius="100dp" />
+    <solid android:color="#FF5555" />
+    <stroke
+        android:width="2dp"
+        android:color="#fff" />
+</shape>

+ 9 - 0
plugins/map_amap_android/android/src/main/res/drawable/icon_marker_passing.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <corners android:radius="100dp" />
+    <solid android:color="#15CBA1" />
+    <stroke
+        android:width="2dp"
+        android:color="#fff" />
+</shape>

+ 35 - 0
plugins/map_amap_android/android/src/main/res/layout/item_track_error_marker.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <View
+        android:id="@+id/v_bg1"
+        android:layout_width="30dp"
+        android:layout_height="30dp"
+        android:background="@drawable/bg_track_error_point"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <View
+        android:id="@+id/v_bg2"
+        android:layout_width="22dp"
+        android:layout_height="22dp"
+        android:background="@drawable/bg_track_error_point"
+        app:layout_constraintBottom_toBottomOf="@+id/v_bg1"
+        app:layout_constraintEnd_toEndOf="@+id/v_bg1"
+        app:layout_constraintStart_toStartOf="@+id/v_bg1"
+        app:layout_constraintTop_toTopOf="@+id/v_bg1" />
+
+    <View
+        android:layout_width="14dp"
+        android:layout_height="14dp"
+        android:background="@drawable/icon_marker_error"
+        app:layout_constraintBottom_toBottomOf="@+id/v_bg2"
+        app:layout_constraintEnd_toEndOf="@+id/v_bg2"
+        app:layout_constraintStart_toStartOf="@+id/v_bg2"
+        app:layout_constraintTop_toTopOf="@+id/v_bg2" />
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 15 - 0
plugins/map_amap_android/android/src/main/res/layout/item_track_passing_marker.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <View
+        android:layout_width="14dp"
+        android:layout_height="14dp"
+        android:background="@drawable/icon_marker_passing"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 71 - 0
plugins/map_amap_android/android/src/main/res/layout/item_track_selected_marker.xml

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/ll_popup"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/bg_track_popup_bubble"
+        android:paddingHorizontal="12dp"
+        android:paddingTop="8dp"
+        android:paddingBottom="26dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintWidth_max="170dp">
+
+
+        <TextView
+            android:id="@+id/tv_popup_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="#333333"
+            android:textSize="11sp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="广东省广州市天河区龙洞网鱼网咖网吧" />
+
+
+        <ImageView
+            android:id="@+id/iv_time_stay"
+            android:layout_width="12dp"
+            android:layout_height="12dp"
+            android:layout_marginTop="10dp"
+            android:src="@drawable/icon_track_stay"
+            app:layout_constraintStart_toStartOf="@+id/tv_popup_title"
+            app:layout_constraintTop_toBottomOf="@+id/tv_popup_title" />
+
+
+        <TextView
+            android:id="@+id/tv_popup_desc"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="2dp"
+            android:textColor="#666666"
+            android:textSize="10sp"
+            app:layout_constraintBottom_toBottomOf="@+id/iv_time_stay"
+            app:layout_constraintStart_toEndOf="@+id/iv_time_stay"
+            app:layout_constraintTop_toTopOf="@+id/iv_time_stay"
+            tools:text="11h30min" />
+
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+<!--    <View-->
+<!--        android:id="@+id/v_marker"-->
+<!--        android:layout_width="14dp"-->
+<!--        android:layout_height="14dp"-->
+<!--        android:background="@drawable/icon_marker_passing"-->
+<!--        app:layout_constraintBottom_toBottomOf="parent"-->
+<!--        app:layout_constraintEnd_toEndOf="parent"-->
+<!--        app:layout_constraintStart_toStartOf="parent"-->
+<!--        app:layout_constraintTop_toBottomOf="@+id/ll_popup" />-->
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>