Преглед на файлове

[new]增加轨迹查询以及当前位置显示功能

zk преди 8 месеца
родител
ревизия
fe090c5de9
променени са 63 файла, в които са добавени 1998 реда и са изтрити 109 реда
  1. BIN
      assets/images/bg_track_location_tie.webp
  2. BIN
      assets/images/icon_track_location_now.webp
  3. BIN
      assets/images/icon_track_select_time_arrow.webp
  4. 14 0
      assets/string/base/string.xml
  5. 18 0
      lib/data/api/atmob_api.dart
  6. 156 0
      lib/data/api/atmob_api.g.dart
  7. 22 0
      lib/data/api/request/query_track_request.dart
  8. 22 0
      lib/data/api/response/query_track_response.dart
  9. 45 0
      lib/data/bean/atmob_track_point.dart
  10. 1 0
      lib/data/bean/track_combination_bean.dart
  11. 3 1
      lib/data/consts/constants.dart
  12. 11 0
      lib/data/repositories/friends_repository.dart
  13. 30 0
      lib/data/repositories/track_repository.dart
  14. 8 2
      lib/di/get_it.config.dart
  15. 62 0
      lib/dialog/loading_dialog.dart
  16. 1 1
      lib/module/friend/friend_controller.dart
  17. 1 1
      lib/module/main/main_controller.dart
  18. 272 1
      lib/module/track/track_controller.dart
  19. 319 3
      lib/module/track/track_page.dart
  20. 33 0
      lib/module/track/track_util.dart
  21. 15 0
      lib/resource/assets.gen.dart
  22. 22 0
      lib/resource/string.gen.dart
  23. 2 4
      lib/sdk/map/map_helper.dart
  24. 25 0
      lib/utils/date_util.dart
  25. 44 0
      lib/utils/fixed_size_tab_indicator.dart
  26. 6 0
      lib/utils/pair.dart
  27. 8 1
      lib/widget/relative_time_text.dart
  28. 6 4
      plugins/map/lib/flutter_map.dart
  29. 7 0
      plugins/map/lib/src/consts/map_constants.dart
  30. 3 3
      plugins/map/lib/src/consts/marker_type.dart
  31. 21 5
      plugins/map/lib/src/core/flutter_map.dart
  32. 48 3
      plugins/map/lib/src/widget/map_controller.dart
  33. 19 1
      plugins/map/lib/src/core/map_platform.dart
  34. 21 0
      plugins/map/lib/src/entity/lat_lng.dart
  35. 31 0
      plugins/map/lib/src/entity/map_padding.dart
  36. 1 1
      plugins/map/lib/src/entity/marker.dart
  37. 34 0
      plugins/map/lib/src/entity/trace_location.dart
  38. 12 0
      plugins/map/lib/src/interface/map_fun_interface.dart
  39. 4 8
      plugins/map/lib/src/interface/map_overlays_interface.dart
  40. 9 0
      plugins/map/lib/src/interface/map_polyline_interface.dart
  41. 1 1
      plugins/map/lib/src/interface/map_platform_interface.dart
  42. 8 0
      plugins/map/lib/src/interface/map_trace_interface.dart
  43. 1 1
      plugins/map/lib/src/widget/map_widget.dart
  44. 3 2
      plugins/map_amap_android/android/build.gradle
  45. 3 1
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/MapAmapAndroidPlugin.java
  46. 15 3
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/amap/AmapView.java
  47. 11 3
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/amap/MapController.java
  48. 76 0
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/bean/FTraceLocation.java
  49. 4 4
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/bean/MakerInfo.java
  50. 61 0
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/bean/MapPadding.java
  51. 19 2
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/contants/Constants.java
  52. 31 13
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/overlays/marker/MarkersController.java
  53. 150 0
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/overlays/polyline/PolylineController.java
  54. 105 0
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/trace/TraceClientHelper.java
  55. 60 10
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/util/ParamUtil.java
  56. 31 0
      plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/util/SizeUtil.java
  57. BIN
      plugins/map_amap_android/android/src/main/res/drawable-xxhdpi/bg_map_line.webp
  58. BIN
      plugins/map_amap_android/android/src/main/res/drawable-xxhdpi/icon_track_start.webp
  59. 9 0
      plugins/map_amap_android/android/src/main/res/drawable/icon_marker_track_bottom_circle.xml
  60. 18 0
      plugins/map_amap_android/android/src/main/res/layout/item_track_start_marker.xml
  61. 24 0
      pubspec.lock
  62. 12 0
      pubspec.yaml
  63. 0 30
      test/widget_test.dart

BIN
assets/images/bg_track_location_tie.webp


BIN
assets/images/icon_track_location_now.webp


BIN
assets/images/icon_track_select_time_arrow.webp


+ 14 - 0
assets/string/base/string.xml

@@ -160,4 +160,18 @@
     <string name="dialog_not_login_view_trace_tip">登录即可体验查看轨迹记录</string>
     <string name="friend_not_open_location_share">该好友没有开启位置分享</string>
     <string name="friend_go_instruct_him">去通知Ta</string>
+
+
+    <string name="track_query_path">查询轨迹</string>
+    <string name="track_query_now_location">查询位置</string>
+    <string name="track_history">历史轨迹</string>
+    <string name="track_now_location">当前位置</string>
+    <string name="track_start_time">开始时间:</string>
+    <string name="track_end_time">结束时间:</string>
+    <string name="track_start_location">起点:</string>
+    <string name="track_end_location">终点:</string>
+
+    <string name="track_choose_time_error">时间范围不能超过24小时且开始时间需小于结束时间</string>
+    <string name="track_loading_txt">正在查询中...</string>
+
 </resources>

+ 18 - 0
lib/data/api/atmob_api.dart

@@ -6,11 +6,13 @@ import 'package:location/data/api/request/configs_request.dart';
 import 'package:location/data/api/request/friends_list_request.dart';
 import 'package:location/data/api/request/friends_operation_request.dart';
 import 'package:location/data/api/request/login_request.dart';
+import 'package:location/data/api/request/query_track_request.dart';
 import 'package:location/data/api/request/send_code_request.dart';
 import 'package:location/data/api/response/configs_response.dart';
 import 'package:location/data/api/response/friends_list_response.dart';
 import 'package:location/data/api/response/login_response.dart';
 import 'package:location/data/api/response/member_status_response.dart';
+import 'package:location/data/api/response/query_track_response.dart';
 import 'package:retrofit/error_logger.dart';
 import 'package:retrofit/http.dart';
 
@@ -59,4 +61,20 @@ abstract class AtmobApi {
 
   @POST("/s/v1/friend/request/send")
   Future<BaseResponse> addFriendRequest(@Body() AddFriendRequest request);
+
+  @POST("/s/v1/friend/virtual/track")
+  Future<BaseResponse<QueryTrackResponse>> queryVirtualTrack(
+      @Body() AppBaseRequest request);
+
+  @POST("/s/v1/location/track/query")
+  Future<BaseResponse<QueryTrackResponse>> queryTrack(
+      @Body() QueryTrackRequest request);
+
+  @POST("/s/v1/friend/get")
+  Future<BaseResponse<UserInfo?>> getUserInfoFromId(
+      @Body() FriendsOperationRequest request);
+
+  @POST("/s/v1/friend/virtual")
+  Future<BaseResponse<UserInfo?>> getVirtualFromId(
+      @Body() FriendsOperationRequest request);
 }

+ 156 - 0
lib/data/api/atmob_api.g.dart

@@ -403,6 +403,162 @@ class _AtmobApi implements AtmobApi {
     return _value;
   }
 
+  @override
+  Future<BaseResponse<QueryTrackResponse>> queryVirtualTrack(
+      AppBaseRequest request) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<QueryTrackResponse>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/friend/virtual/track',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<QueryTrackResponse> _value;
+    try {
+      _value = BaseResponse<QueryTrackResponse>.fromJson(
+        _result.data!,
+        (json) => QueryTrackResponse.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
+  Future<BaseResponse<QueryTrackResponse>> queryTrack(
+      QueryTrackRequest request) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<QueryTrackResponse>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/location/track/query',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<QueryTrackResponse> _value;
+    try {
+      _value = BaseResponse<QueryTrackResponse>.fromJson(
+        _result.data!,
+        (json) => QueryTrackResponse.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
+  Future<BaseResponse<UserInfo?>> getUserInfoFromId(
+      FriendsOperationRequest request) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<UserInfo?>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/friend/get',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<UserInfo?> _value;
+    try {
+      _value = BaseResponse<UserInfo?>.fromJson(
+        _result.data!,
+        (json) => json == null
+            ? null
+            : UserInfo.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
+  Future<BaseResponse<UserInfo?>> getVirtualFromId(
+      FriendsOperationRequest request) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<UserInfo?>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/friend/virtual',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<UserInfo?> _value;
+    try {
+      _value = BaseResponse<UserInfo?>.fromJson(
+        _result.data!,
+        (json) => json == null
+            ? null
+            : UserInfo.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
   RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
     if (T != dynamic &&
         !(requestOptions.responseType == ResponseType.bytes ||

+ 22 - 0
lib/data/api/request/query_track_request.dart

@@ -0,0 +1,22 @@
+import 'package:json_annotation/json_annotation.dart';
+import 'package:location/base/app_base_request.dart';
+
+part 'query_track_request.g.dart';
+
+@JsonSerializable()
+class QueryTrackRequest extends AppBaseRequest {
+  @JsonKey(name: "start")
+  final int? startTime;
+
+  @JsonKey(name: "end")
+  final int? endTime;
+
+  @JsonKey(name: "userId")
+  final String? userId;
+
+  QueryTrackRequest(
+      {required this.startTime, required this.endTime, required this.userId});
+
+  @override
+  Map<String, dynamic> toJson() => _$QueryTrackRequestToJson(this);
+}

+ 22 - 0
lib/data/api/response/query_track_response.dart

@@ -0,0 +1,22 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../bean/atmob_track_point.dart';
+
+part 'query_track_response.g.dart';
+
+@JsonSerializable()
+class QueryTrackResponse {
+  @JsonKey(name: "cdnPrefix")
+  String? cdnPrefix;
+
+  @JsonKey(name: "thumbnailSuffix")
+  String? thumbnailSuffix;
+
+  @JsonKey(name: 'list')
+  List<AtmobTrackPoint>? trackPoints;
+
+  QueryTrackResponse({this.trackPoints, this.cdnPrefix, this.thumbnailSuffix});
+
+  factory QueryTrackResponse.fromJson(Map<String, dynamic> json) =>
+      _$QueryTrackResponseFromJson(json);
+}

+ 45 - 0
lib/data/bean/atmob_track_point.dart

@@ -0,0 +1,45 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'atmob_track_point.g.dart';
+
+@JsonSerializable()
+class AtmobTrackPoint {
+  @JsonKey(name: "lng")
+  double longitude;
+
+  @JsonKey(name: "lat")
+  double latitude;
+
+  @JsonKey(name: "ts")
+  int time;
+
+  @JsonKey(name: "speed")
+  double? speed;
+
+  @JsonKey(name: "bearing")
+  double? bearing;
+
+  @JsonKey(name: "addr")
+  String? addr;
+
+  @JsonKey(name: "photo")
+  String? photo;
+
+  @JsonKey(name: "id")
+  int? id;
+
+  AtmobTrackPoint(
+      {required this.longitude,
+      required this.latitude,
+      required this.time,
+      this.speed,
+      this.bearing,
+      this.addr,
+      this.photo,
+      this.id});
+
+  Map<String, dynamic> toJson() => _$AtmobTrackPointToJson(this);
+
+  factory AtmobTrackPoint.fromJson(Map<String, dynamic> json) =>
+      _$AtmobTrackPointFromJson(json);
+}

+ 1 - 0
lib/data/bean/track_combination_bean.dart

@@ -0,0 +1 @@
+class TrackCombinationBean {}

+ 3 - 1
lib/data/consts/constants.dart

@@ -31,7 +31,9 @@ class Constants {
   static const String appChannelId = "app_channel_id";
   static const String appTgPlatformId = "app_tg_platform_id";
 
-  static const String mineLocationId = '-1';
+  static const String mineLocationId = '';
+  static const String traceStartId = '-100';
+  static const String traceEndId = '-200';
 
   static const double blurredX = 4.2;
   static const double blurredY = 4.2;

+ 11 - 0
lib/data/repositories/friends_repository.dart

@@ -125,4 +125,15 @@ class FriendsRepository {
         .friendList(FriendsListRequest(offset: offset, limit: limit))
         .then(HttpHandler.handle(true));
   }
+
+  Future<UserInfo?> getUserInfoFromId(String friendId, {bool? isVirtual}) {
+    if (isVirtual == true) {
+      return atmobApi
+          .getVirtualFromId(FriendsOperationRequest(friendId: friendId))
+          .then(HttpHandler.handle(true));
+    }
+    return atmobApi
+        .getUserInfoFromId(FriendsOperationRequest(friendId: friendId))
+        .then(HttpHandler.handle(true));
+  }
 }

+ 30 - 0
lib/data/repositories/track_repository.dart

@@ -0,0 +1,30 @@
+import 'package:injectable/injectable.dart';
+import 'package:location/base/app_base_request.dart';
+import 'package:location/data/api/atmob_api.dart';
+
+import '../../utils/http_handler.dart';
+import '../api/request/query_track_request.dart';
+import '../api/response/query_track_response.dart';
+
+@lazySingleton
+class TrackRepository {
+  final AtmobApi atmobApi;
+
+  TrackRepository(this.atmobApi);
+
+  Future<QueryTrackResponse> queryVirtualTrack() {
+    return atmobApi
+        .queryVirtualTrack(AppBaseRequest())
+        .then(HttpHandler.handle(true));
+  }
+
+  Future<QueryTrackResponse> queryTrack(
+      {required int? startTime,
+      required int? endTime,
+      required String? userId}) {
+    return atmobApi
+        .queryTrack(QueryTrackRequest(
+            startTime: startTime, endTime: endTime, userId: userId))
+        .then(HttpHandler.handle(true));
+  }
+}

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

@@ -18,6 +18,7 @@ import '../data/repositories/config_repository.dart' as _i825;
 import '../data/repositories/contact_repository.dart' as _i850;
 import '../data/repositories/friends_repository.dart' as _i1053;
 import '../data/repositories/message_repository.dart' as _i791;
+import '../data/repositories/track_repository.dart' as _i240;
 import '../module/add_friend/add_friend_dialog_controller.dart' as _i897;
 import '../module/browser/browser_controller.dart' as _i923;
 import '../module/friend/friend_controller.dart' as _i821;
@@ -44,9 +45,8 @@ extension GetItInjectableX on _i174.GetIt {
     );
     final networkModule = _$NetworkModule();
     gh.factory<_i923.BrowserController>(() => _i923.BrowserController());
-    gh.factory<_i973.SplashController>(() => _i973.SplashController());
     gh.factory<_i269.MemberController>(() => _i269.MemberController());
-    gh.factory<_i518.TrackController>(() => _i518.TrackController());
+    gh.factory<_i973.SplashController>(() => _i973.SplashController());
     gh.singleton<_i361.Dio>(() => networkModule.createDefaultDio());
     gh.lazySingleton<_i220.AtmobLocationClient>(
         () => _i220.AtmobLocationClient());
@@ -60,10 +60,16 @@ extension GetItInjectableX on _i174.GetIt {
         () => _i1053.FriendsRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i791.MessageRepository>(
         () => _i791.MessageRepository(gh<_i243.AtmobApi>()));
+    gh.lazySingleton<_i240.TrackRepository>(
+        () => _i240.TrackRepository(gh<_i243.AtmobApi>()));
     gh.factory<_i1008.LoginController>(
         () => _i1008.LoginController(gh<_i20.AccountRepository>()));
     gh.factory<_i732.MineController>(
         () => _i732.MineController(gh<_i20.AccountRepository>()));
+    gh.factory<_i518.TrackController>(() => _i518.TrackController(
+          gh<_i240.TrackRepository>(),
+          gh<_i1053.FriendsRepository>(),
+        ));
     gh.lazySingleton<_i825.ConfigRepository>(() => _i825.ConfigRepository(
           gh<_i243.AtmobApi>(),
           gh<_i1053.FriendsRepository>(),

+ 62 - 0
lib/dialog/loading_dialog.dart

@@ -0,0 +1,62 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+
+import '../resource/colors.gen.dart';
+
+class LoadingDialog {
+  static void show(String msg) {
+    SmartDialog.showLoading(msg: msg);
+  }
+
+  static void hide() {
+    SmartDialog.dismiss();
+  }
+}
+
+class CustomLoadingDialog {
+  static String tag = "CustomLoadingDialog";
+
+  static void show({String? loadingTxt, bool backDismiss = false}) {
+    SmartDialog.show(
+        tag: tag,
+        backDismiss: backDismiss,
+        clickMaskDismiss: false,
+        builder: (_) {
+          return Container(
+              padding: EdgeInsets.all(20.w),
+              decoration: BoxDecoration(
+                color: ColorName.black70,
+                borderRadius: BorderRadius.circular(12.w),
+              ),
+              child: IntrinsicHeight(
+                child: Column(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    CircularProgressIndicator(
+                      strokeWidth: 3.w,
+                      valueColor:
+                          const AlwaysStoppedAnimation(ColorName.white75),
+                    ),
+                    Visibility(
+                        visible: loadingTxt != null,
+                        child: SizedBox(height: 12.w)),
+                    Builder(builder: (context) {
+                      if (loadingTxt == null) {
+                        return const SizedBox.shrink();
+                      }
+                      return Text(loadingTxt,
+                          style: TextStyle(
+                              fontSize: 13.sp, color: ColorName.white75));
+                    })
+                  ],
+                ),
+              ));
+        });
+  }
+
+  static void hide() {
+    SmartDialog.dismiss(tag: tag);
+  }
+}

+ 1 - 1
lib/module/friend/friend_controller.dart

@@ -88,6 +88,6 @@ class FriendController extends BaseController {
       });
       return;
     }
-    TrackPage.start();
+    TrackPage.start(userInfo);
   }
 }

+ 1 - 1
lib/module/main/main_controller.dart

@@ -257,6 +257,6 @@ class MainController extends BaseController {
       });
       return;
     }
-    TrackPage.start();
+    TrackPage.start(userInfo);
   }
 }

+ 272 - 1
lib/module/track/track_controller.dart

@@ -1,19 +1,290 @@
+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: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/consts/constants.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/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/toast_util.dart';
+import 'package:sliding_sheet2/sliding_sheet2.dart';
+import '../../data/bean/atmob_track_point.dart';
+import '../../data/bean/user_info.dart';
+import '../../utils/date_util.dart';
+import '../../utils/location_convert_marker_util.dart';
+import '../../utils/pair.dart';
 
 @injectable
-class TrackController extends BaseController {
+class TrackController extends BaseController
+    with GetSingleTickerProviderStateMixin {
+  final int errorQueryOriginalDataEmpty = 10; //查询原始数据集为空
+  final int errorQueryOriginalTooFew = 11; //查询原始数据集少于2点
+
+  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分";
+  late TabController tabController;
+  final RxInt _currentIndex = 0.obs;
+
+  int get currentIndex => _currentIndex.value;
+
+  final Rxn<LocationInfo> _currentLocation = Rxn<LocationInfo>();
+
+  LocationInfo? get currentLocation => _currentLocation.value;
+
+  List<LatLng>? points;
+
+  final TrackRepository trackRepository;
+  final FriendsRepository friendsRepository;
+
+  TrackController(this.trackRepository, this.friendsRepository);
+
+  @override
+  void onInit() {
+    final param = Get.arguments;
+    if (param is UserInfo) {
+      _userInfo.value = param;
+    }
+    tabController = TabController(
+        length: 2, vsync: this, initialIndex: _currentIndex.value);
+    tabController.addListener(_handleTabChange);
+    _initTime();
+    _onCurrentLocationQuery(showLoading: false);
+  }
+
   @override
   void onReady() {
     super.onReady();
+    sheetController.expand();
+  }
+
+  void _handleTabChange() {
+    if (tabController.indexIsChanging) return;
+    _currentIndex.value = tabController.index;
+    if (tabController.index == 0) {
+      _showTrack();
+    } else if (tabController.index == 1) {
+      _showCurrentLocation();
+    }
+  }
+
+  void _initTime() {
+    //开始时间往前推一天
+    _trackStartTime.value = DateUtil.getNow(subtract: Duration(days: 1));
+    _trackEndTime.value = DateUtil.getNow();
   }
 
   void back() {
     Get.back();
   }
+
+  void onTrackStartTimeClick(BuildContext context) {
+    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 onTrackEndTimeClick(BuildContext context) {
+    DatePicker.showDatePicker(context,
+        locale: DateTimePickerLocale.zh_cn,
+        initialDateTime: _trackEndTime.value,
+        dateFormat: timeFormat, onConfirm: (dateTime, selectedIndex) {
+      if (trackStartTime != null &&
+          DateUtil.isTimeRangeExceed(dateTime, trackStartTime!, maxDuration)) {
+        ToastUtil.show(StringName.trackChooseTimeError);
+        _trackStartTime.value = dateTime.subtract(maxDuration);
+      }
+      _trackEndTime.value = dateTime;
+    });
+  }
+
+  void onTrackQueryClick() {
+    if (currentIndex == 0) {
+      _onTrackQuery();
+    } else {
+      _onCurrentLocationQuery();
+    }
+  }
+
+  void _onCurrentLocationQuery({bool showLoading = true}) {
+    if (userInfo == null) {
+      return;
+    }
+    if (showLoading) LoadingDialog.show(StringName.trackLoadingTxt);
+    friendsRepository
+        .getUserInfoFromId(userInfo!.id, isVirtual: userInfo!.virtual)
+        .then((userInfo) {
+      LoadingDialog.hide();
+      _currentLocation.value = userInfo?.lastLocation.value;
+      _showCurrentLocation();
+    }).catchError((error) {
+      debugPrint("error: $error");
+      ErrorHandler.toastError(error);
+    });
+  }
+
+  void _onTrackQuery() {
+    if (trackStartTime == null || trackEndTime == null || userInfo == null) {
+      return;
+    }
+    LoadingDialog.show(StringName.trackLoadingTxt);
+    _startAddress.value = '';
+    _endAddress.value = '';
+    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) {
+      LoadingDialog.hide();
+      points = pair.second;
+      _showTrack();
+      _setStartAndEndAddress(start: pair.first.first, end: pair.first.last);
+    }).catchError((error) {
+      LoadingDialog.hide();
+      if (error is TrackQueryException) {
+      } else {
+        ErrorHandler.toastError(error);
+      }
+    });
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+    tabController.dispose();
+  }
+
+  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;
+    }
+    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,
+    ));
+    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));
+    //显示起点标记
+    // drawMarker();
+    //显示终点标记
+  }
+}
+
+class TrackQueryException implements Exception {
+  final int code;
+
+  TrackQueryException(this.code);
 }

+ 319 - 3
lib/module/track/track_page.dart

@@ -1,21 +1,30 @@
 import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter/src/widgets/framework.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:location/base/base_page.dart';
-import 'package:location/dialog/agreement_dialog.dart';
+import 'package:location/data/bean/user_info.dart';
 import 'package:location/module/track/track_controller.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 '../../router/app_pages.dart';
 import '../../widget/common_view.dart';
+import '../../widget/relative_time_text.dart';
 
 class TrackPage extends BasePage<TrackController> {
   const TrackPage({super.key});
 
-  static void start() {
-    Get.toNamed(RoutePath.track);
+  static void start(UserInfo userInfo) {
+    Get.toNamed(RoutePath.track, arguments: userInfo);
   }
 
   @override
@@ -41,6 +50,313 @@ class TrackPage extends BasePage<TrackController> {
                 onTap: controller.back, child: CommonView.getBackBtnView()),
           ),
         ),
+        SlidingSheet(
+          controller: controller.sheetController,
+          elevation: 10,
+          shadowColor: Colors.black.withOpacity(0.1),
+          cornerRadius: 18.w,
+          snapSpec: SnapSpec(
+            initialSnap: 1,
+            // Enable snapping. This is true by default.
+            snap: true,
+            // Set custom snapping points.
+            snappings: [168 / Get.height, 1.0],
+            // Define to what the snappings relate to. In this case,
+            // the total available space that the sheet can expand to.
+            positioning: SnapPositioning.relativeToAvailableSpace,
+          ),
+          footerBuilder: (context, state) {
+            return buildOperationBtn();
+          },
+          builder: (context, state) {
+            return Column(
+              children: [
+                SizedBox(height: 5.w),
+                Align(
+                  alignment: Alignment.center,
+                  child: Container(
+                    width: 32.w,
+                    height: 3.w,
+                    decoration: BoxDecoration(
+                      color: '#D9D9D9'.color,
+                      borderRadius: BorderRadius.circular(49.w),
+                    ),
+                  ),
+                ),
+                SizedBox(height: 25.w),
+                buildTrackHeaderView(),
+                SizedBox(
+                  width: double.infinity,
+                  height: 220.w,
+                  child: TabBarView(
+                      controller: controller.tabController,
+                      children: [
+                        buildTrackHistoryContentView(),
+                        buildTrackNowContentView()
+                      ]),
+                )
+              ],
+            );
+          },
+        )
+      ],
+    );
+  }
+
+  Widget buildOperationBtn() {
+    return GestureDetector(
+      onTap: controller.onTrackQueryClick,
+      child: Container(
+        width: 322.w,
+        height: 46.w,
+        margin: EdgeInsets.only(bottom: 18.w, top: 9.w),
+        decoration: getPrimaryBtnDecoration(46.w),
+        child: Center(
+          child: Obx(() {
+            return Text(
+              controller.currentIndex == 0
+                  ? StringName.trackQueryPath
+                  : StringName.trackNowLocation,
+              style: TextStyle(fontSize: 14.sp, color: Colors.white),
+            );
+          }),
+        ),
+      ),
+    );
+  }
+
+  Widget buildTrackHeaderView() {
+    return Row(
+      children: [
+        SizedBox(width: 14.w),
+        Obx(() {
+          return Image(
+              image: controller.userInfo?.isMine == true
+                  ? Assets.images.iconDefaultMineAvatar.provider()
+                  : Assets.images.iconDefaultFriendAvatar.provider(),
+              width: 32.w,
+              height: 32.w);
+        }),
+        SizedBox(width: 10.w),
+        Expanded(
+          child: Text(
+            '${controller.userInfo?.getUserNickName() ?? ''}的轨迹',
+            style: TextStyle(
+                overflow: TextOverflow.ellipsis,
+                fontSize: 16.sp,
+                color: ColorName.black80,
+                fontWeight: FontWeight.bold),
+          ),
+        ),
+        buildOperationTabBar(),
+        SizedBox(width: 12.w),
+      ],
+    );
+  }
+
+  Widget buildOperationTabBar() {
+    return IntrinsicWidth(
+      child: Container(
+        padding: EdgeInsets.all(2.w),
+        decoration: BoxDecoration(
+          color: '#F3F3F3'.color,
+          borderRadius: BorderRadius.circular(48.w),
+        ),
+        height: 32.w,
+        child: TabBar(
+          controller: controller.tabController,
+          indicator: BoxDecoration(
+            color: ColorName.colorPrimary,
+            borderRadius: BorderRadius.circular(48.w),
+          ),
+          dividerHeight: 0,
+          indicatorSize: TabBarIndicatorSize.tab,
+          unselectedLabelStyle:
+              TextStyle(fontSize: 14.sp, color: '#666666'.color),
+          labelStyle: TextStyle(fontSize: 14.sp, color: ColorName.white),
+          tabs: [
+            Tab(text: StringName.trackHistory),
+            Tab(text: StringName.trackNowLocation)
+          ],
+        ),
+      ),
+    );
+  }
+
+  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));
+          }),
+        )
       ],
     );
   }

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

@@ -0,0 +1,33 @@
+import 'package:flutter_map/flutter_map.dart';
+import 'package:location/data/bean/atmob_track_point.dart';
+
+class TrackUtil {
+  static List<TraceLocation> points2TraceLocation(
+      List<AtmobTrackPoint>? pointsList) {
+    if (pointsList == null) {
+      return [];
+    }
+    List<TraceLocation> traceLocations = [];
+    for (var value in pointsList) {
+      TraceLocation traceLocation = TraceLocation(
+        latitude: value.latitude,
+        longitude: value.longitude,
+        time: value.time,
+        speed: value.speed,
+        bearing: value.bearing,
+      );
+      traceLocations.add(traceLocation);
+    }
+    return traceLocations;
+  }
+
+  static List<LatLng> traceLocation2LatLng(
+      List<TraceLocation>? traceLocations) {
+    if (traceLocations == null) {
+      return [];
+    }
+    return traceLocations
+        .map((e) => LatLng(latitude: e.latitude, longitude: e.longitude))
+        .toList();
+  }
+}

+ 15 - 0
lib/resource/assets.gen.dart

@@ -40,6 +40,10 @@ class $AssetsImagesGen {
   AssetGenImage get bgPageBackground =>
       const AssetGenImage('assets/images/bg_page_background.webp');
 
+  /// File path: assets/images/bg_track_location_tie.webp
+  AssetGenImage get bgTrackLocationTie =>
+      const AssetGenImage('assets/images/bg_track_location_tie.webp');
+
   /// File path: assets/images/icon_agreement_close.webp
   AssetGenImage get iconAgreementClose =>
       const AssetGenImage('assets/images/icon_agreement_close.webp');
@@ -208,6 +212,14 @@ class $AssetsImagesGen {
   AssetGenImage get iconSplashTitle =>
       const AssetGenImage('assets/images/icon_splash_title.webp');
 
+  /// File path: assets/images/icon_track_location_now.webp
+  AssetGenImage get iconTrackLocationNow =>
+      const AssetGenImage('assets/images/icon_track_location_now.webp');
+
+  /// File path: assets/images/icon_track_select_time_arrow.webp
+  AssetGenImage get iconTrackSelectTimeArrow =>
+      const AssetGenImage('assets/images/icon_track_select_time_arrow.webp');
+
   /// File path: assets/images/icon_vip.webp
   AssetGenImage get iconVip =>
       const AssetGenImage('assets/images/icon_vip.webp');
@@ -237,6 +249,7 @@ class $AssetsImagesGen {
         bgLoginHeadContainer,
         bgMineMemberCard,
         bgPageBackground,
+        bgTrackLocationTie,
         iconAgreementClose,
         iconBlackBack,
         iconCheckboxSelected,
@@ -279,6 +292,8 @@ class $AssetsImagesGen {
         iconMineSmallVip,
         iconMineUnlockVip,
         iconSplashTitle,
+        iconTrackLocationNow,
+        iconTrackSelectTimeArrow,
         iconVip,
         iconWhiteBack,
         imgDialogLocationAlwaysTip1,

+ 22 - 0
lib/resource/string.gen.dart

@@ -134,6 +134,18 @@ class StringName {
       'friend_not_open_location_share'.tr; // 该好友没有开启位置分享
   static final String friendGoInstructHim =
       'friend_go_instruct_him'.tr; // 去通知Ta
+  static final String trackQueryPath = 'track_query_path'.tr; // 查询轨迹
+  static final String trackQueryNowLocation =
+      'track_query_now_location'.tr; // 查询位置
+  static final String trackHistory = 'track_history'.tr; // 历史轨迹
+  static final String trackNowLocation = 'track_now_location'.tr; // 当前位置
+  static final String trackStartTime = 'track_start_time'.tr; // 开始时间:
+  static final String trackEndTime = 'track_end_time'.tr; // 结束时间:
+  static final String trackStartLocation = 'track_start_location'.tr; // 起点:
+  static final String trackEndLocation = 'track_end_location'.tr; // 终点:
+  static final String trackChooseTimeError =
+      'track_choose_time_error'.tr; // 时间范围不能超过24小时且开始时间需小于结束时间
+  static final String trackLoadingTxt = 'track_loading_txt'.tr; // 正在查询中...
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -268,6 +280,16 @@ class StringMultiSource {
       'dialog_not_login_view_trace_tip': '登录即可体验查看轨迹记录',
       'friend_not_open_location_share': '该好友没有开启位置分享',
       'friend_go_instruct_him': '去通知Ta',
+      'track_query_path': '查询轨迹',
+      'track_query_now_location': '查询位置',
+      'track_history': '历史轨迹',
+      'track_now_location': '当前位置',
+      'track_start_time': '开始时间:',
+      'track_end_time': '结束时间:',
+      'track_start_location': '起点:',
+      'track_end_location': '终点:',
+      'track_choose_time_error': '时间范围不能超过24小时且开始时间需小于结束时间',
+      'track_loading_txt': '正在查询中...',
     },
   };
 }

+ 2 - 4
lib/sdk/map/map_helper.dart

@@ -8,20 +8,18 @@ class MapHelper {
   static const String tag = "FlutterMapHelper";
 
   static MapLocation? _lastLocation;
-  static late MapPlatform _mapPlatform;
 
   static final List<MapLocationListener> _locationListeners = [];
 
   static Future<void> init() async {
     await FlutterMap.init();
-    _mapPlatform = FlutterMap.getMapPlatform();
     _initLocationClient();
   }
 
   static void _initLocationClient() {
     AtmobLocationClient atmobLocationClient =
         AtmobLocationClient.getAtmobLocationClient();
-    _mapPlatform.receiveLocationStream(listener: (location) {
+    FlutterMap.receiveLocationStream(listener: (location) {
       AtmobLog.d(tag, "onLocationChanged: $location");
       if (location.errorCode == 0) {
         _lastLocation = location;
@@ -38,7 +36,7 @@ class MapHelper {
   }
 
   static Future<void> startLocation() {
-    return _mapPlatform.startLocation();
+    return FlutterMap.startLocation();
   }
 
   static MapLocation? getLastLocation() {

+ 25 - 0
lib/utils/date_util.dart

@@ -7,4 +7,29 @@ class DateUtil {
     final date = DateTime.fromMillisecondsSinceEpoch(endTimestamp);
     return DateFormat(format).format(date);
   }
+
+  static DateTime getNow({Duration? subtract}) {
+    if (subtract == null) {
+      return DateTime.now();
+    } else {
+      return DateTime.now().subtract(subtract);
+    }
+  }
+
+  static String formatDateTime(DateTime dateTime, String format) {
+    return DateFormat(format).format(dateTime);
+  }
+
+  //2个时间之差是否超过指定的时间范围,需考虑时间差为负的情况
+  static bool isTimeRangeExceed(
+      DateTime startTime, DateTime endTime, Duration duration) {
+    final diff = endTime.difference(startTime);
+    return diff.isNegative ? diff.abs() > duration : diff > duration;
+  }
+}
+
+extension DateTimeExt on DateTime {
+  String format(String format) {
+    return DateFormat(format).format(this);
+  }
 }

+ 44 - 0
lib/utils/fixed_size_tab_indicator.dart

@@ -0,0 +1,44 @@
+import 'package:flutter/cupertino.dart';
+
+class FixedSizeTabIndicator extends Decoration {
+  final double width; // Fixed width
+  final double height; // Indicator height
+  final double radius; // Corner radius
+  final Color color; // Indicator color
+
+  const FixedSizeTabIndicator({
+    required this.width,
+    required this.height,
+    required this.radius,
+    required this.color,
+  });
+
+  @override
+  BoxPainter createBoxPainter([VoidCallback? onChanged]) {
+    return _CustomPainter(this, onChanged);
+  }
+}
+
+class _CustomPainter extends BoxPainter {
+  final FixedSizeTabIndicator decoration;
+
+  _CustomPainter(this.decoration, VoidCallback? onChanged) : super(onChanged);
+
+  @override
+  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
+    final Paint paint = Paint();
+    paint.color = decoration.color;
+    paint.style = PaintingStyle.fill;
+
+    final double xPos =
+        offset.dx + (configuration.size!.width / 2) - (decoration.width / 2);
+    final double yPos = configuration.size!.height - decoration.height;
+
+    final Rect rect =
+        Rect.fromLTWH(xPos, yPos, decoration.width, decoration.height);
+    final RRect rRect = RRect.fromRectAndRadius(
+        rect, Radius.circular(decoration.radius)); // Rounded corners
+
+    canvas.drawRRect(rRect, paint);
+  }
+}

+ 6 - 0
lib/utils/pair.dart

@@ -0,0 +1,6 @@
+class Pair<F, S> {
+  final F first;
+  final S second;
+
+  Pair(this.first, this.second);
+}

+ 8 - 1
lib/widget/relative_time_text.dart

@@ -9,11 +9,16 @@ class RelativeTimeText extends StatefulWidget {
   final Duration updateInterval;
   final TextStyle? style;
 
+  final String? startPerchText;
+  final String? endPerchText;
+
   const RelativeTimeText({
     super.key,
     required this.timestamp,
     this.updateInterval = const Duration(minutes: 1),
     this.style,
+    this.startPerchText,
+    this.endPerchText,
   });
 
   @override
@@ -43,6 +48,8 @@ class _RelativeTimeTextState extends State<RelativeTimeText> {
 
   @override
   Widget build(BuildContext context) {
-    return Text(time2TimeDesc(widget.timestamp), style: widget.style);
+    return Text(
+        '${widget.startPerchText ?? ''}${time2TimeDesc(widget.timestamp)}${widget.endPerchText ?? ''}',
+        style: widget.style);
   }
 }

+ 6 - 4
plugins/map/lib/flutter_map.dart

@@ -2,8 +2,7 @@ library flutter_map;
 
 //地图组件
 export 'package:flutter_map/src/widget/map_widget.dart';
-export 'package:flutter_map/src/widget/map_controller.dart';
-export 'package:flutter_map/src/core/map_platform.dart';
+export 'package:flutter_map/src/core/map_controller.dart';
 
 //初始化
 export 'package:flutter_map/src/core/flutter_map.dart';
@@ -16,7 +15,10 @@ 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/interface/map_platform_interface.dart';
-export 'package:flutter_map/src/interface/map_overlays_interface.dart';
+export 'package:flutter_map/src/interface/map_sdk_interface.dart';
+export 'package:flutter_map/src/interface/map_marker_interface.dart';

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

@@ -23,9 +23,16 @@ class MapConstants {
   //地图相关方法名称
   static const String methodMapMoveCamera = "map#moveCamera";
   static const String methodMapAnimateCamera = "map#animateCamera";
+  static const String methodMapClear = "map#clear";
 
   //标记物相关方法名称
   static const String methodUpdateOrAddMarkers = "marker#updateOrAddMarkers";
   static const String methodReplaceAllMarkers = "marker#replaceAllMarkers";
   static const String methodMarkerOnTap = "marker#onTap";
+
+  //轨迹纠偏
+  static const String methodQueryProcessedTrace = "trace#queryProcessedTrace";
+
+  //polyline
+  static const String methodAddPolyline = "polyline#addPolyline";
 }

+ 3 - 3
plugins/map/lib/src/consts/marker_type.dart

@@ -3,9 +3,9 @@ import '../entity/marker.dart';
 enum MarkerType {
   mine(1),
   friend(2),
-  signalStartingPoint(3),
-  signalEndPoint(4),
-  trajectoryTargetPoint(5);
+  traceStartPoint(3),
+  traceEndFriendPoint(4),
+  traceEndMinePoint(5);
 
   final int value;
 

+ 21 - 5
plugins/map/lib/src/core/flutter_map.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/services.dart';
 import '../../flutter_map.dart';
+import 'map_platform.dart';
 
 class FlutterMap {
   FlutterMap._();
@@ -19,10 +20,25 @@ class FlutterMap {
     _mapPlatform = mapPlatform;
   }
 
-  static MapPlatform getMapPlatform() {
-    if (_mapPlatform == null) {
-      throw Exception("地图未初始化,请先调用Map.init()方法初始化地图");
-    }
-    return _mapPlatform!;
+  static Future<String?> getPlatformName() {
+    assert(_mapPlatform != null, "请先初始化地图");
+    return _mapPlatform!.getPlatformName();
+  }
+
+  static void receiveLocationStream({required MapLocationListener listener}) {
+    assert(_mapPlatform != null, "请先初始化地图");
+    _mapPlatform!.receiveLocationStream(listener: listener);
+  }
+
+  static Future<void> startLocation() {
+    assert(_mapPlatform != null, "请先初始化地图");
+    return _mapPlatform!.startLocation();
+  }
+
+  static Future<List<LatLng>?> queryProcessedTrace(
+      {required int lineID, required List<TraceLocation> locations}) {
+    assert(_mapPlatform != null, "请先初始化地图");
+    return _mapPlatform!
+        .queryProcessedTrace(lineID: lineID, locations: locations);
   }
 }

+ 48 - 3
plugins/map/lib/src/widget/map_controller.dart

@@ -2,12 +2,19 @@ import 'dart:convert';
 
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/services.dart';
+import 'package:flutter_map/flutter_map.dart';
 import 'package:flutter_map/src/consts/map_constants.dart';
 import 'package:flutter_map/src/entity/camera_position.dart';
+import 'package:flutter_map/src/entity/lat_lng.dart';
 import 'package:flutter_map/src/entity/marker.dart';
-import 'package:flutter_map/src/interface/map_overlays_interface.dart';
+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';
 
-class MapController extends MapOverlaysInterface {
+import '../entity/map_padding.dart';
+
+class MapController
+    implements MapMarkerInterface, MapFunInterface, MapPolylineInterface {
   final _pendingOperations = <Map<String, dynamic>>[];
   MethodChannel? _channel;
   bool _isDisposed = false;
@@ -31,6 +38,7 @@ class MapController extends MapOverlaysInterface {
   void dispose() {
     _isDisposed = true;
     _pendingOperations.clear();
+    _channel?.setMethodCallHandler(null);
     _channel = null;
   }
 
@@ -88,7 +96,7 @@ class MapController extends MapOverlaysInterface {
 
   @override
   void replaceAllMarkers(List<Marker> markers) {
-    if (_isDisposed || markers.isEmpty) return;
+    if (_isDisposed) return;
     final serialized = jsonEncode(markers);
     final params = {
       'method': MapConstants.methodReplaceAllMarkers,
@@ -101,4 +109,41 @@ class MapController extends MapOverlaysInterface {
       _pendingOperations.add(params);
     }
   }
+
+  @override
+  void clearAllMarkers() {
+    replaceAllMarkers([]);
+  }
+
+  @override
+  void addPolyline(List<LatLng> points,
+      {bool isAnimateCamera = true, MapPadding? mapPadding}) {
+    if (_isDisposed || points.isEmpty) return;
+    Map<String, dynamic> map = {};
+    map['points'] = jsonEncode(points);
+    map['isAnimateCamera'] = isAnimateCamera;
+    if (mapPadding != null) {
+      map['mapPadding'] = jsonEncode(mapPadding);
+    }
+    final params = {'method': MapConstants.methodAddPolyline, 'args': map};
+    debugPrint("addPolyline...params==>$params");
+    if (_channel != null) {
+      _executeMethod(params);
+    } else {
+      _pendingOperations.add(params);
+    }
+  }
+
+  @override
+  void clear() {
+    if (_isDisposed) return;
+    clearAllMarkers();
+    final params = {'method': MapConstants.methodMapClear, 'args': ''};
+    debugPrint("clear...params==>$params");
+    if (_channel != null) {
+      _executeMethod(params);
+    } else {
+      _pendingOperations.add(params);
+    }
+  }
 }

+ 19 - 1
plugins/map/lib/src/core/map_platform.dart

@@ -1,8 +1,10 @@
 import 'dart:async';
+import 'dart:convert';
 import 'package:flutter/services.dart';
+import 'package:flutter_map/src/interface/map_trace_interface.dart';
 import '../../flutter_map.dart';
 
-class MapPlatform extends MapInterface {
+class MapPlatform implements MapSDKInterface, MapTraceInterface {
   final MethodChannel _channel;
 
   final EventChannel _eventChannel =
@@ -41,4 +43,20 @@ class MapPlatform extends MapInterface {
   Future<void> startLocation() {
     return _channel.invokeMethod<void>(MapConstants.startLocation);
   }
+
+  @override
+  Future<List<LatLng>?> queryProcessedTrace(
+      {required int lineID, required List<TraceLocation> locations}) {
+    return _channel.invokeMethod(MapConstants.methodQueryProcessedTrace, {
+      "lineID": lineID,
+      "locations": jsonEncode(locations.map((e) => e.toJson()).toList())
+    }).then((value) {
+      final List<dynamic> data = jsonDecode(value);
+      if (data.isEmpty) {
+        return null;
+      } else {
+        return data.map((e) => LatLng.fromJson(e)).toList();
+      }
+    });
+  }
 }

+ 21 - 0
plugins/map/lib/src/entity/lat_lng.dart

@@ -0,0 +1,21 @@
+class LatLng {
+  double? latitude;
+
+  double? longitude;
+
+  LatLng({required this.latitude, required this.longitude});
+
+  Map<String, dynamic> toJson() {
+    return {
+      'latitude': latitude,
+      'longitude': longitude,
+    };
+  }
+
+  factory LatLng.fromJson(Map<String, dynamic> map) {
+    return LatLng(
+      latitude: map['latitude'],
+      longitude: map['longitude'],
+    );
+  }
+}

+ 31 - 0
plugins/map/lib/src/entity/map_padding.dart

@@ -0,0 +1,31 @@
+class MapPadding {
+  final double left;
+  final double top;
+  final double right;
+  final double bottom;
+
+  MapPadding({this.left = 0, this.top = 0, this.right = 0, this.bottom = 0});
+
+  Map<String, dynamic> toJson() {
+    return {
+      'left': left,
+      'top': top,
+      'right': right,
+      'bottom': bottom,
+    };
+  }
+
+  factory MapPadding.fromJson(Map<String, dynamic> map) {
+    return MapPadding(
+      left: map['left'],
+      top: map['top'],
+      right: map['right'],
+      bottom: map['bottom'],
+    );
+  }
+
+  @override
+  String toString() {
+    return 'MapPadding{left: $left, top: $top, right: $right, bottom: $bottom}';
+  }
+}

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

@@ -19,7 +19,7 @@ class Marker {
     required this.longitude,
     required this.latitude,
     required this.markerType,
-    required this.isSelected,
+    this.isSelected = false,
   });
 
   Map<String, dynamic> toJson() {

+ 34 - 0
plugins/map/lib/src/entity/trace_location.dart

@@ -0,0 +1,34 @@
+class TraceLocation {
+  double? latitude;
+
+  double? longitude;
+
+  int? time;
+
+  double? bearing;
+
+  double? speed;
+
+  TraceLocation(
+      {this.latitude, this.longitude, this.time, this.bearing, this.speed});
+
+  Map<String, dynamic> toJson() {
+    return {
+      'latitude': latitude,
+      'longitude': longitude,
+      'time': time,
+      'bearing': bearing,
+      'speed': speed,
+    };
+  }
+
+  factory TraceLocation.fromJson(Map<String, dynamic> map) {
+    return TraceLocation(
+      latitude: map['latitude'],
+      longitude: map['longitude'],
+      time: map['time'],
+      bearing: map['bearing'],
+      speed: map['speed'],
+    );
+  }
+}

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

@@ -0,0 +1,12 @@
+import '../../flutter_map.dart';
+
+abstract class MapFunInterface {
+  //移动地图
+  void moveCamera(CameraPosition cameraPosition);
+
+  //动态移动地图
+  void animateCamera(CameraPosition cameraPosition);
+
+  //清除地图所有标记
+  void clear();
+}

+ 4 - 8
plugins/map/lib/src/interface/map_overlays_interface.dart

@@ -1,13 +1,7 @@
-import '../entity/camera_position.dart';
+import 'package:flutter_map/flutter_map.dart';
 import '../entity/marker.dart';
 
-abstract class MapOverlaysInterface {
-  //移动地图
-  void moveCamera(CameraPosition cameraPosition);
-
-  //动态移动地图
-  void animateCamera(CameraPosition cameraPosition);
-
+abstract class MapMarkerInterface {
   //修改标记物,需注意id,如果标记物id已存在则执行修改操作,否则执行添加操作
   void updateOrAddMarkers(List<Marker> markers);
 
@@ -15,4 +9,6 @@ abstract class MapOverlaysInterface {
 
   //marker全地图替换,需对比之前缓存的,如果有则更新,没有则移除;当markers为空时,执行清空操作
   void replaceAllMarkers(List<Marker> markers);
+
+  void clearAllMarkers();
 }

+ 9 - 0
plugins/map/lib/src/interface/map_polyline_interface.dart

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

+ 1 - 1
plugins/map/lib/src/interface/map_platform_interface.dart

@@ -2,7 +2,7 @@ import '../entity/map_location.dart';
 
 typedef MapLocationListener = void Function(MapLocation location);
 
-abstract class MapInterface {
+abstract class MapSDKInterface {
   Future<String?> getPlatformName();
 
   Future<bool> init();

+ 8 - 0
plugins/map/lib/src/interface/map_trace_interface.dart

@@ -0,0 +1,8 @@
+import 'package:flutter_map/flutter_map.dart';
+
+abstract class MapTraceInterface {
+  //lineID: 用于标示一条轨迹,支持多轨迹纠偏,如果多条轨迹调起纠偏接口,则lineID需不同。
+  // locations : 一条轨迹的点集合。建议为一条行车GPS高精度定位轨迹。
+  Future<List<LatLng>?> queryProcessedTrace(
+      {required int lineID, required List<TraceLocation> locations});
+}

+ 1 - 1
plugins/map/lib/src/widget/map_widget.dart

@@ -6,7 +6,7 @@ import 'package:flutter/services.dart';
 import 'package:flutter_map/src/consts/map_constants.dart';
 import 'package:flutter_map/src/consts/marker_type.dart';
 import 'package:flutter_map/src/entity/marker.dart';
-import 'package:flutter_map/src/widget/map_controller.dart';
+import 'package:flutter_map/src/core/map_controller.dart';
 
 class MapWidget extends StatefulWidget {
   final MapController? controller;

+ 3 - 2
plugins/map_amap_android/android/build.gradle

@@ -70,8 +70,9 @@ android {
         implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
 
         //高德地图
-        implementation "com.amap.api:3dmap:9.7.0"
-        implementation 'com.amap.api:search:9.7.0'
+//        implementation "com.amap.api:3dmap:9.7.0"
+//        implementation 'com.amap.api:search:9.7.0'
+        implementation 'com.amap.api:3dmap-location-search:10.0.800_loc6.4.5_sea9.7.2'
 
         //gson
         implementation "com.google.code.gson:gson:2.10"

+ 3 - 1
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/MapAmapAndroidPlugin.java

@@ -8,9 +8,9 @@ import androidx.lifecycle.Lifecycle;
 
 import com.atmob.map_amap_android.contants.Constants;
 import com.atmob.map_amap_android.lifecycle.FlutterLifecycleAdapter;
+import com.atmob.map_amap_android.trace.TraceClientHelper;
 import com.atmob.map_amap_android.util.AMapHelper;
 import com.atmob.map_amap_android.util.LogUtil;
-import com.google.gson.Gson;
 
 import java.util.Objects;
 
@@ -62,6 +62,8 @@ public class MapAmapAndroidPlugin implements FlutterPlugin, MethodCallHandler, A
             } else if (Objects.equals(method, Constants.METHOD_START_LOCATION)) {
                 AMapHelper.startLocation(applicationContext);
                 result.success(true);
+            } else if (Objects.equals(method, Constants.METHOD_QUERY_PROCESSED_TRACE)) {
+                TraceClientHelper.queryProcessedTrace(applicationContext, call, result);
             } else {
                 result.notImplemented();
             }

+ 15 - 3
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/amap/AmapView.java

@@ -14,10 +14,12 @@ import androidx.lifecycle.LifecycleOwner;
 
 import com.amap.api.maps.AMap;
 import com.amap.api.maps.CameraUpdateFactory;
+import com.amap.api.maps.MapView;
 import com.amap.api.maps.TextureMapView;
 import com.atmob.map_amap_android.contants.Constants;
 import com.atmob.map_amap_android.overlays.MyMethodCallHandler;
 import com.atmob.map_amap_android.overlays.marker.MarkersController;
+import com.atmob.map_amap_android.overlays.polyline.PolylineController;
 import com.atmob.map_amap_android.util.LogUtil;
 
 import java.util.Map;
@@ -39,7 +41,7 @@ public class AmapView implements PlatformView, DefaultLifecycleObserver, MethodC
 
     private final MethodChannel channel;
 
-    private TextureMapView mapView = null;
+    private MapView mapView = null;
 
     private boolean disposed = false;
 
@@ -49,6 +51,8 @@ public class AmapView implements PlatformView, DefaultLifecycleObserver, MethodC
 
     private MarkersController markersController;
 
+    private PolylineController polylineController;
+
     private final int viewId;
 
 
@@ -59,11 +63,12 @@ public class AmapView implements PlatformView, DefaultLifecycleObserver, MethodC
         myMethodCallHandlerMap = new HashMap<>(8);
 
         try {
-            mapView = new TextureMapView(context);
+            mapView = new MapView(context);
             AMap amap = mapView.getMap();
             initMapDefaultSetting(amap);
-            markersController = new MarkersController(context, channel, amap);
             mapController = new MapController(context, channel, amap);
+            markersController = new MarkersController(context, channel, amap);
+            polylineController = new PolylineController(context, channel, amap);
 
             initMyMethodCallHandlerMap();
 
@@ -88,6 +93,13 @@ public class AmapView implements PlatformView, DefaultLifecycleObserver, MethodC
                 myMethodCallHandlerMap.put(methodId, mapController);
             }
         }
+
+        methodIdArray = polylineController.getRegisterMethodIdArray();
+        if (null != methodIdArray) {
+            for (String methodId : methodIdArray) {
+                myMethodCallHandlerMap.put(methodId, polylineController);
+            }
+        }
     }
 
 

+ 11 - 3
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/amap/MapController.java

@@ -42,15 +42,23 @@ public class MapController implements MyMethodCallHandler {
             case Constants.METHOD_ANIMATE_CAMERA:
                 animateCamera(call, result);
                 break;
+                case Constants.METHOD_MAP_CLEAR:
+                clearMap(result);
+                break;
         }
     }
 
+    private void clearMap(MethodChannel.Result result) {
+        LogUtil.i(TAG, "clearMap");
+        map.clear();
+        result.success(null);
+    }
+
     private void animateCamera(MethodCall call, MethodChannel.Result result) {
-        LogUtil.i(TAG, "animateCamera===>" + call.arguments());
         Map<String, Object> arguments = call.arguments();
         Double longitude = ParamUtil.getDouble(arguments, "longitude");
         Double latitude = ParamUtil.getDouble(arguments, "latitude");
-        Integer zoom = ParamUtil.getInt(arguments, "latitude");
+        Integer zoom = ParamUtil.getInt(arguments, "zoom");
         if (longitude == null || latitude == null) {
             result.error("error", "longitude or latitude is null", null);
             return;
@@ -68,7 +76,7 @@ public class MapController implements MyMethodCallHandler {
         Map<String, Object> arguments = call.arguments();
         Double longitude = ParamUtil.getDouble(arguments, "longitude");
         Double latitude = ParamUtil.getDouble(arguments, "latitude");
-        Integer zoom = ParamUtil.getInt(arguments, "latitude");
+        Integer zoom = ParamUtil.getInt(arguments, "zoom");
         if (longitude == null || latitude == null) {
             result.error("error", "longitude or latitude is null", null);
             return;

+ 76 - 0
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/bean/FTraceLocation.java

@@ -0,0 +1,76 @@
+package com.atmob.map_amap_android.bean;
+
+import androidx.annotation.NonNull;
+
+import com.google.gson.annotations.SerializedName;
+
+public class FTraceLocation {
+
+
+    @SerializedName("latitude")
+    private double latitude;
+
+    @SerializedName("longitude")
+    private double longitude;
+
+    @SerializedName("time")
+    private long time;
+
+    @SerializedName("bearing")
+    private float bearing;
+
+    @SerializedName("speed")
+    private float speed;
+
+    public float getBearing() {
+        return bearing;
+    }
+
+    public void setBearing(float bearing) {
+        this.bearing = bearing;
+    }
+
+    public double getLatitude() {
+        return latitude;
+    }
+
+    public void setLatitude(double latitude) {
+        this.latitude = latitude;
+    }
+
+    public double getLongitude() {
+        return longitude;
+    }
+
+    public void setLongitude(double longitude) {
+        this.longitude = longitude;
+    }
+
+    public float getSpeed() {
+        return speed;
+    }
+
+    public void setSpeed(float speed) {
+        this.speed = speed;
+    }
+
+    public long getTime() {
+        return time;
+    }
+
+    public void setTime(long time) {
+        this.time = time;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "FTraceLocation{" +
+                "latitude=" + latitude +
+                ", longitude=" + longitude +
+                ", time=" + time +
+                ", bearing=" + bearing +
+                ", speed=" + speed +
+                '}';
+    }
+}

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

@@ -80,13 +80,13 @@ public class MakerInfo {
     }
 
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({MarkerType.MINE, MarkerType.FRIEND, MarkerType.SIGNAL_STARTING_POINT, MarkerType.SIGNAL_END_POINT, MarkerType.TRAJECTORY_TARGET_POINT})
+    @IntDef({MarkerType.MINE, MarkerType.FRIEND, MarkerType.TRACE_START_POINT, MarkerType.TRACE_END_FRIEND_POINT, MarkerType.TRACE_END_MINE_POINT})
     public @interface MarkerType {
         int MINE = 1;
         int FRIEND = 2;
-        int SIGNAL_STARTING_POINT = 3;
-        int SIGNAL_END_POINT = 4;
-        int TRAJECTORY_TARGET_POINT = 5;
+        int TRACE_START_POINT = 3;
+        int TRACE_END_FRIEND_POINT = 4;
+        int TRACE_END_MINE_POINT = 5;
     }
 
 

+ 61 - 0
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/bean/MapPadding.java

@@ -0,0 +1,61 @@
+package com.atmob.map_amap_android.bean;
+
+import androidx.annotation.NonNull;
+
+public class MapPadding {
+
+    public float left;
+    public float top;
+    public float right;
+    public float bottom;
+
+    public MapPadding(float left, float top, float right, float bottom) {
+        this.left = left;
+        this.top = top;
+        this.right = right;
+        this.bottom = bottom;
+    }
+
+    public float getBottom() {
+        return bottom;
+    }
+
+    public void setBottom(float bottom) {
+        this.bottom = bottom;
+    }
+
+    public float getLeft() {
+        return left;
+    }
+
+    public void setLeft(float left) {
+        this.left = left;
+    }
+
+    public float getRight() {
+        return right;
+    }
+
+    public void setRight(float right) {
+        this.right = right;
+    }
+
+    public float getTop() {
+        return top;
+    }
+
+    public void setTop(float top) {
+        this.top = top;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "MapPadding{" +
+                "left=" + left +
+                ", top=" + top +
+                ", right=" + right +
+                ", bottom=" + bottom +
+                '}';
+    }
+}

+ 19 - 2
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/contants/Constants.java

@@ -24,11 +24,20 @@ 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_ID_LIST_FOR_MAP = {METHOD_MOVE_CAMERA, METHOD_ANIMATE_CAMERA};
+    public static final String[] METHOD_ID_LIST_FOR_MAP = {METHOD_MOVE_CAMERA, METHOD_ANIMATE_CAMERA,METHOD_MAP_CLEAR};
 
     /**
      * markers
@@ -41,4 +50,12 @@ public class Constants {
     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_UPDATE_OR_ADD_POLYLINE = "polyline#addPolyline";
+
+    public static final String[] METHOD_ID_LIST_FOR_POLYLINE = {METHOD_UPDATE_OR_ADD_POLYLINE};
+
+
 }

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

@@ -20,6 +20,7 @@ import com.atmob.map_amap_android.R;
 import com.atmob.map_amap_android.bean.MakerInfo;
 import com.atmob.map_amap_android.contants.Constants;
 import com.atmob.map_amap_android.databinding.ItemLocationMarkerBinding;
+import com.atmob.map_amap_android.databinding.ItemTrackStartMarkerBinding;
 import com.atmob.map_amap_android.overlays.MyMethodCallHandler;
 import com.atmob.map_amap_android.util.GsonUtil;
 import com.atmob.map_amap_android.util.LogUtil;
@@ -44,12 +45,11 @@ public class MarkersController implements MyMethodCallHandler, AMap.OnMarkerClic
 
     private final MethodChannel methodChannel;
 
-
     private final HashMap<String, Marker> currentMarkers = new HashMap<>(10);
 
     private ItemLocationMarkerBinding markerBinding;
 
-    private Gson gson;
+    private final Gson gson;
 
 
     public MarkersController(Context context, MethodChannel methodChannel, AMap map) {
@@ -153,18 +153,18 @@ public class MarkersController implements MyMethodCallHandler, AMap.OnMarkerClic
                 MakerInfo cacheInfo = (MakerInfo) object;
                 if (!Objects.equals(cacheInfo.getMarkerName(), makerInfo.getMarkerName())
                         || cacheInfo.isSelected() != makerInfo.isSelected()) {
-                    BitmapDescriptor markerBitmap = getMarkerBitmap(makerInfo.getMarkerName(), makerInfo.isSelected(), makerInfo.getMarkerType() == MakerInfo.MarkerType.MINE);
+                    BitmapDescriptor markerBitmap = getMarkerBitmap(makerInfo);
                     marker.setIcon(markerBitmap);
                     marker.setObject(makerInfo);
                 }
             }
             LogUtil.i(TAG, "updateMarkers=marker..updateSuccess,id=" + makerInfo.getId());
         } else {
-            BitmapDescriptor markerBitmap = getMarkerBitmap(makerInfo.getMarkerName(), makerInfo.isSelected(), makerInfo.getMarkerType() == MakerInfo.MarkerType.MINE);
+            BitmapDescriptor markerBitmap = getMarkerBitmap(makerInfo);
             MarkerOptions markerOption = new MarkerOptions()
                     .position(latLng)
                     .icon(markerBitmap)
-                    .anchor(0.5f, 1);
+                    .anchor(0.5f, 0.9f);
             marker = map.addMarker(markerOption);
             marker.setObject(makerInfo);
             currentMarkers.put(makerInfo.getId(), marker);
@@ -179,16 +179,34 @@ public class MarkersController implements MyMethodCallHandler, AMap.OnMarkerClic
     }
 
 
-    public BitmapDescriptor getMarkerBitmap(String name, boolean isSelected, boolean isSelf) {
-        if (markerBinding == null) {
-            markerBinding = ItemLocationMarkerBinding.inflate(LayoutInflater.from(context));
+    public BitmapDescriptor getMarkerBitmap(MakerInfo markerInfo) {
+        if (markerInfo.getMarkerType() == MakerInfo.MarkerType.MINE || markerInfo.getMarkerType() == MakerInfo.MarkerType.FRIEND) {
+            if (markerBinding == null) {
+                markerBinding = ItemLocationMarkerBinding.inflate(LayoutInflater.from(context));
+            }
+            ConstraintLayout view = markerBinding.getRoot();
+            markerBinding.locationMarkerTvName.setText(markerInfo.getMarkerName());
+            markerBinding.ivMarkerAvatar.setImageResource(markerInfo.getMarkerType() == MakerInfo.MarkerType.MINE ? R.drawable.icon_default_mine_avatar : R.drawable.icon_default_friend_avatar);
+            markerBinding.vBottom.setBackgroundResource(markerInfo.isSelected() ? R.drawable.icon_marker_selected_bottom_circle : R.drawable.icon_marker_normal_bottom_circle);
+            markerBinding.locationMarkerIvBg.setImageResource(markerInfo.isSelected() ? R.drawable.icon_location_marker_selected : R.drawable.icon_location_marker_normal);
+            return viewGetBitmapDescriptor(view);
+        } else if (markerInfo.getMarkerType() == MakerInfo.MarkerType.TRACE_START_POINT) {
+            ItemTrackStartMarkerBinding trackStartPointMarkerBinding = ItemTrackStartMarkerBinding.inflate(LayoutInflater.from(context));
+            ConstraintLayout view = trackStartPointMarkerBinding.getRoot();
+            return viewGetBitmapDescriptor(view);
+        } else if (markerInfo.getMarkerType() == MakerInfo.MarkerType.TRACE_END_MINE_POINT || markerInfo.getMarkerType() == MakerInfo.MarkerType.TRACE_END_FRIEND_POINT) {
+            ItemLocationMarkerBinding trackEndPointMarkerBinding = ItemLocationMarkerBinding.inflate(LayoutInflater.from(context));
+            ConstraintLayout view = trackEndPointMarkerBinding.getRoot();
+            trackEndPointMarkerBinding.locationMarkerTvName.setText(markerInfo.getMarkerName());
+            trackEndPointMarkerBinding.ivMarkerAvatar.setImageResource(markerInfo.getMarkerType() == MakerInfo.MarkerType.TRACE_END_MINE_POINT ? R.drawable.icon_default_mine_avatar : R.drawable.icon_default_friend_avatar);
+            trackEndPointMarkerBinding.locationMarkerIvBg.setImageResource(R.drawable.icon_location_marker_normal);
+            trackEndPointMarkerBinding.vBottom.setBackgroundResource(R.drawable.icon_marker_track_bottom_circle);
+            return viewGetBitmapDescriptor(view);
         }
-        ConstraintLayout view = markerBinding.getRoot();
+        return null;
+    }
 
-        markerBinding.locationMarkerTvName.setText(name);
-        markerBinding.ivMarkerAvatar.setImageResource(isSelf ? R.drawable.icon_default_mine_avatar : R.drawable.icon_default_friend_avatar);
-        markerBinding.vBottom.setBackgroundResource(isSelected ? R.drawable.icon_marker_selected_bottom_circle : R.drawable.icon_marker_normal_bottom_circle);
-        markerBinding.locationMarkerIvBg.setImageResource(isSelected ? R.drawable.icon_location_marker_selected : R.drawable.icon_location_marker_normal);
+    private BitmapDescriptor viewGetBitmapDescriptor(View view) {
         view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
         view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());

+ 150 - 0
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/overlays/polyline/PolylineController.java

@@ -0,0 +1,150 @@
+package com.atmob.map_amap_android.overlays.polyline;
+
+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.BitmapDescriptorFactory;
+import com.amap.api.maps.model.LatLng;
+import com.amap.api.maps.model.LatLngBounds;
+import com.amap.api.maps.model.Polyline;
+import com.amap.api.maps.model.PolylineOptions;
+import com.atmob.map_amap_android.R;
+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;
+import io.flutter.plugin.common.MethodChannel;
+
+public class PolylineController implements MyMethodCallHandler {
+
+
+    private final String TAG = "PolylineController";
+
+    private final AMap map;
+
+    private final Context context;
+
+    private final MethodChannel methodChannel;
+    private final Gson gson;
+
+    public PolylineController(Context context, MethodChannel methodChannel, AMap map) {
+        this.context = context;
+        this.methodChannel = methodChannel;
+        this.map = map;
+        gson = GsonUtil.getInstance();
+    }
+
+    @Override
+    public void doMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+        LogUtil.i(TAG, "doMethodCall===>" + call.method + "  arguments===>" + call.arguments);
+        switch (call.method) {
+            case Constants.METHOD_UPDATE_OR_ADD_POLYLINE:
+                addPolyline(call, result);
+                break;
+        }
+    }
+
+    private void addPolyline(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;
+            }
+            drawTrackByPoints(latLngList);
+            if (isAnimateCamera) {
+                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 Polyline drawTrackByPoints(List<LatLng> points) {
+        if (map == null || points == null || points.isEmpty()) {
+            return null;
+        }
+        PolylineOptions polylineOptions = new PolylineOptions()
+                .setCustomTexture(BitmapDescriptorFactory.fromResource(R.drawable.bg_map_line))
+                .setUseTexture(true)
+                .width(SizeUtil.dp2px(context, 8));
+
+        LatLng lastPoint = null;
+        for (int i = 0; i < points.size(); i++) {
+            LatLng currentPoint = points.get(i);
+            if (lastPoint == null || lastPoint.latitude != currentPoint.latitude
+                    || lastPoint.longitude != currentPoint.longitude) {
+                polylineOptions.add(currentPoint);
+                lastPoint = currentPoint;
+            }
+        }
+        return map.addPolyline(polylineOptions);
+    }
+
+    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);
+    }
+
+    @Override
+    public String[] getRegisterMethodIdArray() {
+        return Constants.METHOD_ID_LIST_FOR_POLYLINE;
+    }
+}

+ 105 - 0
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/trace/TraceClientHelper.java

@@ -0,0 +1,105 @@
+package com.atmob.map_amap_android.trace;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.amap.api.maps.model.LatLng;
+import com.amap.api.trace.LBSTraceClient;
+import com.amap.api.trace.TraceListener;
+import com.amap.api.trace.TraceLocation;
+import com.atmob.map_amap_android.bean.FTraceLocation;
+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.google.gson.reflect.TypeToken;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+
+public class TraceClientHelper {
+
+    private static final String TAG = "TraceClientHelper";
+
+    private TraceClientHelper() {
+
+    }
+
+    public static void queryProcessedTrace(Context context, MethodCall call, MethodChannel.Result result) {
+        try {
+            Map<String, Object> arguments = call.arguments();
+            if (arguments == null) {
+                result.error("params is empty", "params is empty", null);
+                return;
+            }
+            Integer lineID = ParamUtil.getInt(arguments, "lineID");
+            if (lineID == null) {
+                result.error("lineID is null", "lineID is null", null);
+                return;
+            }
+            String locations = (String) arguments.get("locations");
+            if (locations == null || locations.isEmpty()) {
+                result.error("locations is empty", "原始数据为空", null);
+                return;
+            }
+            LBSTraceClient lbsTraceClient;
+            try {
+                lbsTraceClient = LBSTraceClient.getInstance(context);
+            } catch (Exception e) {
+                result.error("lbsTraceClient init error", e.getMessage(), null);
+                return;
+            }
+            List<FTraceLocation> originalList = GsonUtil.getInstance().fromJson(locations, new TypeToken<List<FTraceLocation>>() {
+            }.getType());
+            List<TraceLocation> locationList = getTraceLocations(originalList);
+            List<LatLng> queryResultList = new ArrayList<>();
+            lbsTraceClient.queryProcessedTrace(lineID, locationList, LBSTraceClient.TYPE_AMAP, new TraceListener() {
+                @Override
+                public void onRequestFailed(int lineID, String s) {
+                    LogUtil.e(TAG, "onRequestFailed: lineID = " + lineID + ",message: " + s);
+                    if (queryResultList.size() > originalList.size() / 2) {
+                        LogUtil.d(TAG, "onRequestFailed: call success queryResultList.size:" + queryResultList.size() + " originalList.size:" + originalList.size());
+                        result.success(GsonUtil.getInstance().toJson(queryResultList));
+                        return;
+                    }
+                    LogUtil.e(TAG, "onRequestFailed: call error ");
+                    result.error("queryProcessedTrace error lineID:" + lineID, s, null);
+                }
+
+                @Override
+                public void onTraceProcessing(int lineID, int index, List<LatLng> list) {
+                    LogUtil.d(TAG, "onTraceProcessing:" + list.size());
+                    queryResultList.addAll(list);
+                }
+
+                @Override
+                public void onFinished(int i, List<LatLng> list, int i1, int i2) {
+                    LogUtil.d(TAG, "onFinished:" + list.size());
+                    result.success(GsonUtil.getInstance().toJson(queryResultList));
+                }
+            });
+
+        } catch (Exception e) {
+            LogUtil.d(TAG, "error:" + e.getMessage());
+            result.error("error", e.getMessage(), null);
+        }
+    }
+
+    private static @NonNull List<TraceLocation> getTraceLocations(List<FTraceLocation> originalList) {
+        List<TraceLocation> locationList = new ArrayList<>();
+        for (FTraceLocation fTraceLocation : originalList) {
+            TraceLocation traceLocation = new TraceLocation();
+            traceLocation.setLatitude(fTraceLocation.getLatitude());
+            traceLocation.setLongitude(fTraceLocation.getLongitude());
+            traceLocation.setSpeed(fTraceLocation.getSpeed());
+            traceLocation.setBearing(fTraceLocation.getBearing());
+            traceLocation.setTime(fTraceLocation.getTime());
+            locationList.add(traceLocation);
+        }
+        return locationList;
+    }
+}

+ 60 - 10
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/util/ParamUtil.java

@@ -14,10 +14,7 @@ public class ParamUtil {
         if (!map.containsKey(key)) {
             return null;
         }
-        if (map.get(key) instanceof String) {
-            return (String) map.get(key);
-        }
-        return null;
+        return (String) map.get(key);
     }
 
     public static Integer getInt(Map<String, Object> map, String key) {
@@ -27,10 +24,11 @@ public class ParamUtil {
         if (!map.containsKey(key)) {
             return null;
         }
-        if (map.get(key) instanceof Integer) {
-            return (Integer) map.get(key);
+        Object value = map.get(key);
+        if (value == null) {
+            return null;
         }
-        return null;
+        return (int) value;
     }
 
     public static Double getDouble(Map<String, Object> map, String key) {
@@ -40,11 +38,63 @@ public class ParamUtil {
         if (!map.containsKey(key)) {
             return null;
         }
-        if (map.get(key) instanceof Double) {
-            return (Double) map.get(key);
+        Object value = map.get(key);
+        if (value == null) {
+            return null;
+        }
+        return (double) value;
+    }
+
+    public static double getDouble(Map<String, Object> map, String key, double defaultValue) {
+        Double d = getDouble(map, key);
+        if (d == null) {
+            return defaultValue;
         }
-        return null;
+        return d;
     }
 
 
+    public static Long getLong(Map<String, Object> map, String key) {
+        if (map == null) {
+            return null;
+        }
+        if (!map.containsKey(key)) {
+            return null;
+        }
+        Object value = map.get(key);
+        if (value == null) {
+            return null;
+        }
+        return (long) value;
+    }
+
+    public static long getLong(Map<String, Object> map, String key, long defaultValue) {
+        Long l = getLong(map, key);
+        if (l == null) {
+            return defaultValue;
+        }
+        return l;
+    }
+
+    public static Boolean getBoolean(Map<String, Object> map, String key) {
+        if (map == null) {
+            return null;
+        }
+        if (!map.containsKey(key)) {
+            return null;
+        }
+        Object value = map.get(key);
+        if (value == null) {
+            return null;
+        }
+        return (boolean) value;
+    }
+
+    public static boolean getBoolean(Map<String, Object> map, String key, boolean defaultValue) {
+        Boolean b = getBoolean(map, key);
+        if (b == null) {
+            return defaultValue;
+        }
+        return b;
+    }
 }

+ 31 - 0
plugins/map_amap_android/android/src/main/java/com/atmob/map_amap_android/util/SizeUtil.java

@@ -0,0 +1,31 @@
+package com.atmob.map_amap_android.util;
+
+import android.content.Context;
+import android.util.TypedValue;
+
+public class SizeUtil {
+
+    public static float dp2px(Context context, float dp) {
+        return TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP,
+                dp,
+                context.getResources().getDisplayMetrics()
+        );
+    }
+
+    public static float sp2px(Context context, float sp) {
+        return TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_SP,
+                sp,
+                context.getResources().getDisplayMetrics()
+        );
+    }
+
+    public static float px2dp(Context context, float px) {
+        return px / context.getResources().getDisplayMetrics().density;
+    }
+
+    public static float px2sp(Context context, float px) {
+        return px / context.getResources().getDisplayMetrics().scaledDensity;
+    }
+}

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


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


+ 9 - 0
plugins/map_amap_android/android/src/main/res/drawable/icon_marker_track_bottom_circle.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>

+ 18 - 0
plugins/map_amap_android/android/src/main/res/layout/item_track_start_marker.xml

@@ -0,0 +1,18 @@
+<?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">
+
+    <ImageView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:src="@drawable/icon_track_start"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 24 - 0
pubspec.lock

@@ -323,6 +323,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.1.9+2"
+  flutter_cupertino_datetime_picker:
+    dependency: "direct main"
+    description:
+      name: flutter_cupertino_datetime_picker
+      sha256: "631fbd3abed4d1151888e053d2153d905663a3b33ff24bdfa64afbcf750682cb"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.0"
   flutter_gen_core:
     dependency: transitive
     description:
@@ -917,6 +925,14 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.99"
+  sliding_sheet2:
+    dependency: "direct main"
+    description:
+      name: sliding_sheet2
+      sha256: "6ed6b11c071cfdfa55ab1b3ffaeddad034ef6917955c8dbc2f4ad58563bbec84"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.1"
   source_gen:
     dependency: transitive
     description:
@@ -1006,6 +1022,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.5"
+  timelines_plus:
+    dependency: "direct main"
+    description:
+      name: timelines_plus
+      sha256: be31f493402dc24df7fe410dc5f82a605807bb4ca13183de6d4362886449b593
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.6"
   timing:
     dependency: transitive
     description:

+ 12 - 0
pubspec.yaml

@@ -92,6 +92,18 @@ dependencies:
 
   #微信支付、分享
   wechat_kit: ^6.0.1
+
+
+  #抽屉
+  sliding_sheet2: ^2.0.1
+
+  #时间轴
+  timelines_plus: 1.0.6
+
+  #时间滚轴选择器
+  flutter_cupertino_datetime_picker: ^3.0.0
+
+
   ######################地图########################
   flutter_map:
     path: plugins/map

+ 0 - 30
test/widget_test.dart

@@ -1,30 +0,0 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility in the flutter_test package. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:location/main.dart';
-
-void main() {
-  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
-    // Build our app and trigger a frame.
-    await tester.pumpWidget(const MyApp());
-
-    // Verify that our counter starts at 0.
-    expect(find.text('0'), findsOneWidget);
-    expect(find.text('1'), findsNothing);
-
-    // Tap the '+' icon and trigger a frame.
-    await tester.tap(find.byIcon(Icons.add));
-    await tester.pump();
-
-    // Verify that our counter has incremented.
-    expect(find.text('0'), findsNothing);
-    expect(find.text('1'), findsOneWidget);
-  });
-}