From 1c4112e95dadd254033640e367665cf60011d96b Mon Sep 17 00:00:00 2001 From: Arina Date: Tue, 20 Aug 2024 13:56:06 +0300 Subject: [PATCH] feat: controls :wq --- .gitignore | 1 + assets/icons/dgis_bullet.svg | 3 + assets/icons/dgis_camera_back.svg | 4 + assets/icons/dgis_camera_both.svg | 7 + assets/icons/dgis_camera_front.svg | 4 + assets/icons/dgis_camera_stop.svg | 6 + assets/icons/dgis_chevron.svg | 3 + assets/icons/dgis_menu.svg | 3 + assets/icons/dgis_my_location.svg | 4 +- assets/icons/dgis_parking.svg | 3 + assets/icons/dgis_route.svg | 3 + assets/icons/dgis_sound.svg | 5 + assets/icons/dgis_swap_points.svg | 3 + assets/icons/dgis_traffic.svg | 3 + assets/icons/dgis_traffic_icon.svg | 1 - assets/icons/dgis_zoom_in.svg | 4 +- assets/icons/dgis_zoom_out.svg | 4 +- .../icons/maneuvers/dgis_crossroad_left.svg | 4 + .../icons/maneuvers/dgis_crossroad_right.svg | 4 + .../maneuvers/dgis_crossroad_sharply_left.svg | 5 + .../dgis_crossroad_sharply_right.svg | 5 + .../dgis_crossroad_slightly_left.svg | 4 + .../dgis_crossroad_slightly_right.svg | 4 + .../icons/maneuvers/dgis_crossroad_uturn.svg | 4 + assets/icons/maneuvers/dgis_finish.svg | 3 + assets/icons/maneuvers/dgis_left.svg | 5 + assets/icons/maneuvers/dgis_right.svg | 5 + .../maneuvers/dgis_ringroad_backward.svg | 5 + assets/icons/maneuvers/dgis_ringroad_exit.svg | 5 + .../icons/maneuvers/dgis_ringroad_forward.svg | 5 + .../maneuvers/dgis_ringroad_left_135.svg | 5 + .../icons/maneuvers/dgis_ringroad_left_45.svg | 5 + .../icons/maneuvers/dgis_ringroad_left_90.svg | 5 + .../maneuvers/dgis_ringroad_right_135.svg | 5 + .../maneuvers/dgis_ringroad_right_45.svg | 5 + .../maneuvers/dgis_ringroad_right_90.svg | 5 + assets/icons/maneuvers/dgis_start.svg | 4 + example/assets/icons/dgis_bicycle_routing.svg | 3 + example/assets/icons/dgis_car_routing.svg | 3 + .../assets/icons/dgis_pedestrian_routing.svg | 3 + example/lib/main.dart | 78 +- example/lib/pages/all_map_controls.dart | 11 +- example/lib/pages/navigator.dart | 148 ++++ example/lib/pages/routes.dart | 165 +++++ example/pubspec.lock | 8 + lib/dgis.dart | 18 +- lib/src/generated/optional.dart | 1 + lib/src/platform/map/map.dart | 7 +- lib/src/platform/map/map_theme.dart | 2 +- lib/src/util/color_ramp.dart | 29 + lib/src/util/format_duration.dart | 21 + lib/src/util/fromat_distance.dart | 47 ++ lib/src/util/measure_size.dart | 42 ++ lib/src/util/no_overscroll_behavior.dart | 8 + lib/src/util/rounded_corners.dart | 45 ++ lib/src/widgets/map/base_map_control.dart | 180 +++++ lib/src/widgets/map/compass_widget.dart | 74 +- lib/src/widgets/map/indoor_widget.dart | 59 +- ...olor_scheme.dart => map_widget_theme.dart} | 6 +- lib/src/widgets/map/my_location_widget.dart | 143 ++-- lib/src/widgets/map/parking_widget.dart | 87 +++ .../map/themed_map_controlling_widget.dart | 48 +- .../themed_map_controlling_widget_state.dart | 45 -- lib/src/widgets/map/traffic_widget.dart | 288 ++++---- lib/src/widgets/map/zoom_button_widget.dart | 91 --- lib/src/widgets/map/zoom_widget.dart | 125 ++-- lib/src/widgets/map_widget_box_shadow.dart | 17 - .../widgets/navigator/dashboard_widget.dart | 672 ++++++++++++++++++ .../widgets/navigator/maneuver_widget.dart | 333 +++++++++ .../widgets/navigator/speed_limit_widget.dart | 535 ++++++++++++++ lib/src/widgets/routing/route_card.dart | 189 +++++ .../widgets/routing/routes_list_widget.dart | 441 ++++++++++++ pubspec.yaml | 6 +- 73 files changed, 3528 insertions(+), 603 deletions(-) create mode 100644 assets/icons/dgis_bullet.svg create mode 100644 assets/icons/dgis_camera_back.svg create mode 100644 assets/icons/dgis_camera_both.svg create mode 100644 assets/icons/dgis_camera_front.svg create mode 100644 assets/icons/dgis_camera_stop.svg create mode 100644 assets/icons/dgis_chevron.svg create mode 100644 assets/icons/dgis_menu.svg create mode 100644 assets/icons/dgis_parking.svg create mode 100644 assets/icons/dgis_route.svg create mode 100644 assets/icons/dgis_sound.svg create mode 100644 assets/icons/dgis_swap_points.svg create mode 100644 assets/icons/dgis_traffic.svg delete mode 100644 assets/icons/dgis_traffic_icon.svg create mode 100644 assets/icons/maneuvers/dgis_crossroad_left.svg create mode 100644 assets/icons/maneuvers/dgis_crossroad_right.svg create mode 100644 assets/icons/maneuvers/dgis_crossroad_sharply_left.svg create mode 100644 assets/icons/maneuvers/dgis_crossroad_sharply_right.svg create mode 100644 assets/icons/maneuvers/dgis_crossroad_slightly_left.svg create mode 100644 assets/icons/maneuvers/dgis_crossroad_slightly_right.svg create mode 100644 assets/icons/maneuvers/dgis_crossroad_uturn.svg create mode 100644 assets/icons/maneuvers/dgis_finish.svg create mode 100644 assets/icons/maneuvers/dgis_left.svg create mode 100644 assets/icons/maneuvers/dgis_right.svg create mode 100644 assets/icons/maneuvers/dgis_ringroad_backward.svg create mode 100644 assets/icons/maneuvers/dgis_ringroad_exit.svg create mode 100644 assets/icons/maneuvers/dgis_ringroad_forward.svg create mode 100644 assets/icons/maneuvers/dgis_ringroad_left_135.svg create mode 100644 assets/icons/maneuvers/dgis_ringroad_left_45.svg create mode 100644 assets/icons/maneuvers/dgis_ringroad_left_90.svg create mode 100644 assets/icons/maneuvers/dgis_ringroad_right_135.svg create mode 100644 assets/icons/maneuvers/dgis_ringroad_right_45.svg create mode 100644 assets/icons/maneuvers/dgis_ringroad_right_90.svg create mode 100644 assets/icons/maneuvers/dgis_start.svg create mode 100644 example/assets/icons/dgis_bicycle_routing.svg create mode 100644 example/assets/icons/dgis_car_routing.svg create mode 100644 example/assets/icons/dgis_pedestrian_routing.svg create mode 100644 example/lib/pages/navigator.dart create mode 100644 example/lib/pages/routes.dart create mode 100644 lib/src/util/color_ramp.dart create mode 100644 lib/src/util/format_duration.dart create mode 100644 lib/src/util/fromat_distance.dart create mode 100644 lib/src/util/measure_size.dart create mode 100644 lib/src/util/no_overscroll_behavior.dart create mode 100644 lib/src/util/rounded_corners.dart create mode 100644 lib/src/widgets/map/base_map_control.dart rename lib/src/widgets/map/{map_widget_color_scheme.dart => map_widget_theme.dart} (73%) create mode 100644 lib/src/widgets/map/parking_widget.dart delete mode 100644 lib/src/widgets/map/themed_map_controlling_widget_state.dart delete mode 100644 lib/src/widgets/map/zoom_button_widget.dart delete mode 100644 lib/src/widgets/map_widget_box_shadow.dart create mode 100644 lib/src/widgets/navigator/dashboard_widget.dart create mode 100644 lib/src/widgets/navigator/maneuver_widget.dart create mode 100644 lib/src/widgets/navigator/speed_limit_widget.dart create mode 100644 lib/src/widgets/routing/route_card.dart create mode 100644 lib/src/widgets/routing/routes_list_widget.dart diff --git a/.gitignore b/.gitignore index ac5aa98..7c3c32e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ migrate_working_dir/ **/doc/api/ .dart_tool/ build/ +.fvm diff --git a/assets/icons/dgis_bullet.svg b/assets/icons/dgis_bullet.svg new file mode 100644 index 0000000..c8c1393 --- /dev/null +++ b/assets/icons/dgis_bullet.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/dgis_camera_back.svg b/assets/icons/dgis_camera_back.svg new file mode 100644 index 0000000..44c48bd --- /dev/null +++ b/assets/icons/dgis_camera_back.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/dgis_camera_both.svg b/assets/icons/dgis_camera_both.svg new file mode 100644 index 0000000..335ead4 --- /dev/null +++ b/assets/icons/dgis_camera_both.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/dgis_camera_front.svg b/assets/icons/dgis_camera_front.svg new file mode 100644 index 0000000..bd91aa2 --- /dev/null +++ b/assets/icons/dgis_camera_front.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/dgis_camera_stop.svg b/assets/icons/dgis_camera_stop.svg new file mode 100644 index 0000000..35759c4 --- /dev/null +++ b/assets/icons/dgis_camera_stop.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/dgis_chevron.svg b/assets/icons/dgis_chevron.svg new file mode 100644 index 0000000..ee00303 --- /dev/null +++ b/assets/icons/dgis_chevron.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/dgis_menu.svg b/assets/icons/dgis_menu.svg new file mode 100644 index 0000000..8254937 --- /dev/null +++ b/assets/icons/dgis_menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/dgis_my_location.svg b/assets/icons/dgis_my_location.svg index e76ac8f..cc24c11 100644 --- a/assets/icons/dgis_my_location.svg +++ b/assets/icons/dgis_my_location.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/assets/icons/dgis_parking.svg b/assets/icons/dgis_parking.svg new file mode 100644 index 0000000..5b8e990 --- /dev/null +++ b/assets/icons/dgis_parking.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/dgis_route.svg b/assets/icons/dgis_route.svg new file mode 100644 index 0000000..7af16d6 --- /dev/null +++ b/assets/icons/dgis_route.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/dgis_sound.svg b/assets/icons/dgis_sound.svg new file mode 100644 index 0000000..60294f1 --- /dev/null +++ b/assets/icons/dgis_sound.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/dgis_swap_points.svg b/assets/icons/dgis_swap_points.svg new file mode 100644 index 0000000..869c210 --- /dev/null +++ b/assets/icons/dgis_swap_points.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/dgis_traffic.svg b/assets/icons/dgis_traffic.svg new file mode 100644 index 0000000..592668e --- /dev/null +++ b/assets/icons/dgis_traffic.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/dgis_traffic_icon.svg b/assets/icons/dgis_traffic_icon.svg deleted file mode 100644 index 5a333e2..0000000 --- a/assets/icons/dgis_traffic_icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/icons/dgis_zoom_in.svg b/assets/icons/dgis_zoom_in.svg index 4f94e23..1d6d8e2 100644 --- a/assets/icons/dgis_zoom_in.svg +++ b/assets/icons/dgis_zoom_in.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/assets/icons/dgis_zoom_out.svg b/assets/icons/dgis_zoom_out.svg index 0208b47..481b420 100644 --- a/assets/icons/dgis_zoom_out.svg +++ b/assets/icons/dgis_zoom_out.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/assets/icons/maneuvers/dgis_crossroad_left.svg b/assets/icons/maneuvers/dgis_crossroad_left.svg new file mode 100644 index 0000000..08dbcf9 --- /dev/null +++ b/assets/icons/maneuvers/dgis_crossroad_left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/maneuvers/dgis_crossroad_right.svg b/assets/icons/maneuvers/dgis_crossroad_right.svg new file mode 100644 index 0000000..23f4686 --- /dev/null +++ b/assets/icons/maneuvers/dgis_crossroad_right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/maneuvers/dgis_crossroad_sharply_left.svg b/assets/icons/maneuvers/dgis_crossroad_sharply_left.svg new file mode 100644 index 0000000..361f5cb --- /dev/null +++ b/assets/icons/maneuvers/dgis_crossroad_sharply_left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_crossroad_sharply_right.svg b/assets/icons/maneuvers/dgis_crossroad_sharply_right.svg new file mode 100644 index 0000000..991ca52 --- /dev/null +++ b/assets/icons/maneuvers/dgis_crossroad_sharply_right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_crossroad_slightly_left.svg b/assets/icons/maneuvers/dgis_crossroad_slightly_left.svg new file mode 100644 index 0000000..f6c06a5 --- /dev/null +++ b/assets/icons/maneuvers/dgis_crossroad_slightly_left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/maneuvers/dgis_crossroad_slightly_right.svg b/assets/icons/maneuvers/dgis_crossroad_slightly_right.svg new file mode 100644 index 0000000..5b790a6 --- /dev/null +++ b/assets/icons/maneuvers/dgis_crossroad_slightly_right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/maneuvers/dgis_crossroad_uturn.svg b/assets/icons/maneuvers/dgis_crossroad_uturn.svg new file mode 100644 index 0000000..6e206dd --- /dev/null +++ b/assets/icons/maneuvers/dgis_crossroad_uturn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/maneuvers/dgis_finish.svg b/assets/icons/maneuvers/dgis_finish.svg new file mode 100644 index 0000000..ce311f9 --- /dev/null +++ b/assets/icons/maneuvers/dgis_finish.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/maneuvers/dgis_left.svg b/assets/icons/maneuvers/dgis_left.svg new file mode 100644 index 0000000..1adb485 --- /dev/null +++ b/assets/icons/maneuvers/dgis_left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_right.svg b/assets/icons/maneuvers/dgis_right.svg new file mode 100644 index 0000000..9cbf36d --- /dev/null +++ b/assets/icons/maneuvers/dgis_right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_ringroad_backward.svg b/assets/icons/maneuvers/dgis_ringroad_backward.svg new file mode 100644 index 0000000..9fffa6a --- /dev/null +++ b/assets/icons/maneuvers/dgis_ringroad_backward.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_ringroad_exit.svg b/assets/icons/maneuvers/dgis_ringroad_exit.svg new file mode 100644 index 0000000..cdabb85 --- /dev/null +++ b/assets/icons/maneuvers/dgis_ringroad_exit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_ringroad_forward.svg b/assets/icons/maneuvers/dgis_ringroad_forward.svg new file mode 100644 index 0000000..7fc244c --- /dev/null +++ b/assets/icons/maneuvers/dgis_ringroad_forward.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_ringroad_left_135.svg b/assets/icons/maneuvers/dgis_ringroad_left_135.svg new file mode 100644 index 0000000..7efe377 --- /dev/null +++ b/assets/icons/maneuvers/dgis_ringroad_left_135.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_ringroad_left_45.svg b/assets/icons/maneuvers/dgis_ringroad_left_45.svg new file mode 100644 index 0000000..ba5c3d0 --- /dev/null +++ b/assets/icons/maneuvers/dgis_ringroad_left_45.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_ringroad_left_90.svg b/assets/icons/maneuvers/dgis_ringroad_left_90.svg new file mode 100644 index 0000000..87da2f2 --- /dev/null +++ b/assets/icons/maneuvers/dgis_ringroad_left_90.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_ringroad_right_135.svg b/assets/icons/maneuvers/dgis_ringroad_right_135.svg new file mode 100644 index 0000000..4414f84 --- /dev/null +++ b/assets/icons/maneuvers/dgis_ringroad_right_135.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_ringroad_right_45.svg b/assets/icons/maneuvers/dgis_ringroad_right_45.svg new file mode 100644 index 0000000..d25f713 --- /dev/null +++ b/assets/icons/maneuvers/dgis_ringroad_right_45.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_ringroad_right_90.svg b/assets/icons/maneuvers/dgis_ringroad_right_90.svg new file mode 100644 index 0000000..e21a0df --- /dev/null +++ b/assets/icons/maneuvers/dgis_ringroad_right_90.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maneuvers/dgis_start.svg b/assets/icons/maneuvers/dgis_start.svg new file mode 100644 index 0000000..c09fa31 --- /dev/null +++ b/assets/icons/maneuvers/dgis_start.svg @@ -0,0 +1,4 @@ + + + + diff --git a/example/assets/icons/dgis_bicycle_routing.svg b/example/assets/icons/dgis_bicycle_routing.svg new file mode 100644 index 0000000..03571da --- /dev/null +++ b/example/assets/icons/dgis_bicycle_routing.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/assets/icons/dgis_car_routing.svg b/example/assets/icons/dgis_car_routing.svg new file mode 100644 index 0000000..3f14697 --- /dev/null +++ b/example/assets/icons/dgis_car_routing.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/assets/icons/dgis_pedestrian_routing.svg b/example/assets/icons/dgis_pedestrian_routing.svg new file mode 100644 index 0000000..50d0c73 --- /dev/null +++ b/example/assets/icons/dgis_pedestrian_routing.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/lib/main.dart b/example/lib/main.dart index 77edaf0..da46775 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'pages/add_objects.dart'; import 'pages/all_map_controls.dart'; import 'pages/benchmark.dart'; @@ -12,12 +14,12 @@ import 'pages/map_gestures.dart'; import 'pages/map_objects_identification.dart'; import 'pages/map_snapshot.dart'; import 'pages/route_editor.dart'; +import 'pages/routes.dart'; import 'pages/search_page.dart'; import 'pages/stateless_screen_with_map.dart'; import 'pages/traffic_widget.dart'; -import 'package:flutter/material.dart'; - import 'pages/common.dart'; +import 'pages/navigator.dart'; void main() { runApp(const MyApp()); @@ -82,11 +84,26 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => RouteEditorPage(title: 'Route editor')), + MaterialPageRoute(builder: (context) => RouteEditorPage(title: 'Route editor')), + ); + }, + ), + ListTile( + title: buildPageTitle('Routes list'), + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => RoutingPage(title: 'Routes list')), + ), + ), + ListTile( + title: buildPageTitle('Navigator Example'), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => NavigatorPage(title: 'Navigator Example')), ); }, - ) + ), ], ); } @@ -99,8 +116,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => AddObjectsPage(title: 'Add Objects')), + MaterialPageRoute(builder: (context) => AddObjectsPage(title: 'Add Objects')), ); }, ), @@ -109,9 +125,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => - AllMapControlsPage(title: 'All map widgets')), + MaterialPageRoute(builder: (context) => AllMapControlsPage(title: 'All map widgets')), ); }, ), @@ -120,8 +134,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => BenchmarkPage(title: 'Benchmark')), + MaterialPageRoute(builder: (context) => BenchmarkPage(title: 'Benchmark')), ); }, ), @@ -130,9 +143,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => - CalcPositionPage(title: 'Calc position')), + MaterialPageRoute(builder: (context) => CalcPositionPage(title: 'Calc position')), ); }, ), @@ -141,8 +152,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => CameraMovesPage(title: 'Camera moves')), + MaterialPageRoute(builder: (context) => CameraMovesPage(title: 'Camera moves')), ); }, ), @@ -151,8 +161,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => ClusteringPage(title: 'Clustering')), + MaterialPageRoute(builder: (context) => ClusteringPage(title: 'Clustering')), ); }, ), @@ -161,8 +170,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => CopyrightPage(title: 'Copyright')), + MaterialPageRoute(builder: (context) => CopyrightPage(title: 'Copyright')), ); }, ), @@ -171,9 +179,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => - DownloadTerritoriesPage(title: 'Download territories')), + MaterialPageRoute(builder: (context) => DownloadTerritoriesPage(title: 'Download territories')), ); }, ), @@ -191,9 +197,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => - IndoorWidgetPage(title: 'Indoor Widget')), + MaterialPageRoute(builder: (context) => IndoorWidgetPage(title: 'Indoor Widget')), ); }, ), @@ -202,8 +206,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => MapGesturesPage(title: 'Map gestures')), + MaterialPageRoute(builder: (context) => MapGesturesPage(title: 'Map gestures')), ); }, ), @@ -213,8 +216,7 @@ class _MyHomePageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => MapObjectsIdentificationPage( - title: 'Map objects identification')), + builder: (context) => MapObjectsIdentificationPage(title: 'Map objects identification')), ); }, ), @@ -223,8 +225,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => MapSnapshotPage(title: 'Map snapshot')), + MaterialPageRoute(builder: (context) => MapSnapshotPage(title: 'Map snapshot')), ); }, ), @@ -233,9 +234,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => SimpleMapScreen( - title: 'Simple map screen (stateless widget)')), + MaterialPageRoute(builder: (context) => SimpleMapScreen(title: 'Simple map screen (stateless widget)')), ); }, ), @@ -244,9 +243,7 @@ class _MyHomePageState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => - TrafficWidgetPage(title: 'Traffic widget')), + MaterialPageRoute(builder: (context) => TrafficWidgetPage(title: 'Traffic widget')), ); }, ), @@ -263,7 +260,8 @@ class _MyHomePageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => SearchPage(title: 'Search')), + builder: (context) => SearchPage(title: 'Search'), + ), ); }, ), diff --git a/example/lib/pages/all_map_controls.dart b/example/lib/pages/all_map_controls.dart index dd10737..fc18b30 100644 --- a/example/lib/pages/all_map_controls.dart +++ b/example/lib/pages/all_map_controls.dart @@ -50,7 +50,16 @@ class _AllMapControlsPageState extends State { children: [ Align( alignment: Alignment.topRight, - child: sdk.TrafficWidget(), + child: Column( + children: [ + sdk.TrafficWidget( + roundedCorners: sdk.RoundedCorners.top(), + ), + sdk.ParkingWidget( + roundedCorners: sdk.RoundedCorners.bottom(), + ), + ], + ), ), Spacer(), Align( diff --git a/example/lib/pages/navigator.dart b/example/lib/pages/navigator.dart new file mode 100644 index 0000000..795880a --- /dev/null +++ b/example/lib/pages/navigator.dart @@ -0,0 +1,148 @@ +import 'dart:async'; + +import 'package:dgis_mobile_sdk_full/dgis.dart' as sdk; +import 'package:flutter/material.dart'; + +class NavigatorPage extends StatefulWidget { + final String title; + + const NavigatorPage({required this.title, super.key}); + + @override + State createState() => _NavigatorPageState(); +} + +class _NavigatorPageState extends State { + final mapWidgetController = sdk.MapWidgetController(); + final sdkContext = sdk.DGis.initialize(); + + late sdk.NavigationManager navigationManager; + late sdk.TrafficRouter trafficRouter; + + final _startPoint = sdk.RouteSearchPoint( + coordinates: sdk.GeoPoint( + latitude: sdk.Latitude(55.749451), + longitude: sdk.Longitude(37.542824), + ), + ); + final _finishPoint = sdk.RouteSearchPoint( + coordinates: sdk.GeoPoint( + latitude: sdk.Latitude(55.757670), + longitude: sdk.Longitude(37.660160), + ), + ); + final _options = sdk.RouteSearchOptions.car( + sdk.CarRouteSearchOptions(), + ); + + @override + void initState() { + navigationManager = sdk.NavigationManager(sdkContext); + trafficRouter = sdk.TrafficRouter(sdkContext); + + super.initState(); + mapWidgetController.getMapAsync((map) { + unawaited( + _startNavigation(map), + ); + }); + } + + Future _startNavigation(sdk.Map map) async { + final routes = await trafficRouter + .findRoute( + _startPoint, + _finishPoint, + _options, + ) + .valueOrCancellation(); + + if (routes != null) { + navigationManager.mapManager.addMap(map); + + map.addSource( + sdk.MyLocationMapObjectSource( + sdkContext, + ), + ); + map.camera.addFollowController( + sdk.StyleZoomFollowController(), + ); + + final route = routes.first; + + navigationManager.simulationSettings.speedMode = sdk.SimulationSpeedMode.overSpeed( + sdk.SimulationAutoWithOverSpeed(10), + ); + navigationManager.startSimulation( + sdk.RouteBuildOptions( + finishPoint: _finishPoint, + routeSearchOptions: _options, + ), + route, + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: sdk.MapWidget( + sdkContext: sdkContext, + // TODO: пофиксить краш при const sdk.MapOptions() + // ignore: prefer_const_constructors + mapOptions: sdk.MapOptions(), + controller: mapWidgetController, + child: Stack( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 6, vertical: 6), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + sdk.ManeuverWidget( + navigationManager: navigationManager, + ), + Spacer(), + sdk.SpeedLimitWidget( + navigationManager: navigationManager, + ), + ], + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Spacer(), + Align( + alignment: Alignment.centerRight, + child: Column( + children: [ + sdk.ZoomWidget(), + Padding( + padding: EdgeInsets.only(top: 8), + child: sdk.MyLocationWidget(), + ), + ], + ), + ), + Spacer(), + ], + ), + sdk.DashboardWidget( + navigationManager: navigationManager, + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/routes.dart b/example/lib/pages/routes.dart new file mode 100644 index 0000000..8d7fcaa --- /dev/null +++ b/example/lib/pages/routes.dart @@ -0,0 +1,165 @@ +import 'dart:async'; + +import 'package:dgis_mobile_sdk_full/dgis.dart' as sdk; +import 'package:flutter/material.dart'; + +class RoutingPage extends StatefulWidget { + final String title; + + const RoutingPage({required this.title, super.key}); + + @override + State createState() => _RoutingPageState(); +} + +final kTabs = [ + sdk.RouteTypeTab( + icon: 'assets/icons/dgis_car_routing.svg', + duration: Duration.zero, + options: sdk.RouteSearchOptions.car( + sdk.CarRouteSearchOptions(), + ), + ), + sdk.RouteTypeTab( + icon: 'assets/icons/dgis_pedestrian_routing.svg', + duration: Duration.zero, + options: sdk.RouteSearchOptions.pedestrian( + sdk.PedestrianRouteSearchOptions(), + ), + ), + sdk.RouteTypeTab( + icon: 'assets/icons/dgis_bicycle_routing.svg', + duration: Duration.zero, + options: sdk.RouteSearchOptions.bicycle( + sdk.BicycleRouteSearchOptions(), + ), + ), +]; + +class _RoutingPageState extends State { + final mapWidgetController = sdk.MapWidgetController(); + final sdkContext = sdk.DGis.initialize(); + + final _routesModel = ValueNotifier( + sdk.RoutesListModel( + routes: [], + tabs: kTabs, + startLabel: 'Моё местоположение', + finishLabel: 'Burger King', + ), + ); + + late sdk.TrafficRouter trafficRouter; + + sdk.RouteSearchOptions _options = sdk.RouteSearchOptions.car( + sdk.CarRouteSearchOptions(), + ); + + sdk.RouteSearchPoint _startPoint = sdk.RouteSearchPoint( + coordinates: sdk.GeoPoint( + latitude: sdk.Latitude(55.749451), + longitude: sdk.Longitude(37.542824), + ), + ); + sdk.RouteSearchPoint _finishPoint = sdk.RouteSearchPoint( + coordinates: sdk.GeoPoint( + latitude: sdk.Latitude(55.757670), + longitude: sdk.Longitude(37.660160), + ), + ); + + @override + void initState() { + trafficRouter = sdk.TrafficRouter(sdkContext); + + super.initState(); + _searchRoutes(); + _tabDurations(); + } + + Future _tabDurations() async { + _routesModel.value = _routesModel.value.copyWith(tabs: kTabs); + + List tabs = []; + + for (final tab in kTabs) { + final briefInfo = await trafficRouter.findBriefRouteInfos( + [ + sdk.BriefRouteInfoSearchPoints(startPoint: _startPoint, finishPoint: _finishPoint), + ], + tab.options, + ).valueOrCancellation(); + final duration = briefInfo?.first?.duration ?? Duration.zero; + + tabs.add( + sdk.RouteTypeTab( + icon: tab.icon, + duration: duration, + options: tab.options, + ), + ); + } + + _routesModel.value = _routesModel.value.copyWith(tabs: tabs); + } + + Future _searchRoutes() async { + _routesModel.value = _routesModel.value.copyWith(routes: []); + final routes = await trafficRouter + .findRoute( + _startPoint, + _finishPoint, + _options, + ) + .valueOrCancellation() ?? + []; + _routesModel.value = _routesModel.value.copyWith(routes: routes); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: sdk.MapWidget( + sdkContext: sdkContext, + mapOptions: sdk.MapOptions(), + controller: mapWidgetController, + child: ValueListenableBuilder( + valueListenable: _routesModel, + builder: (context, model, child) { + return sdk.RoutesListWidget( + model: model, + selectedOptions: _options, + itemBuilder: (route, theme) => sdk.RouteCard( + theme: theme, + route: route, + onGoPressed: (value) {}, + ), + onSwapPoints: () { + final temp = _startPoint; + setState(() { + _startPoint = _finishPoint; + _finishPoint = temp; + }); + _routesModel.value = _routesModel.value.copyWith( + startLabel: _routesModel.value.finishLabel, + finishLabel: _routesModel.value.startLabel, + ); + _searchRoutes(); + _tabDurations(); + }, + onTabChanged: (options) { + setState(() { + _options = options; + }); + _searchRoutes(); + }, + ); + }, + ), + ), + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 6f29dc0..24c5f64 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -150,6 +150,14 @@ packages: description: flutter source: sdk version: "0.0.0" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" leak_tracker: dependency: transitive description: diff --git a/lib/dgis.dart b/lib/dgis.dart index e0eb045..fcf25f4 100644 --- a/lib/dgis.dart +++ b/lib/dgis.dart @@ -31,17 +31,23 @@ export 'src/platform/map/map_appearance.dart'; export 'src/platform/map/map_options.dart'; export 'src/platform/map/map_theme.dart'; export 'src/platform/map/touch_events_observer.dart'; -export 'src/widgets/directory/search.dart' - show DgisSearchWidget, SearchResultBuilder; +export 'src/widgets/directory/search.dart' show DgisSearchWidget, SearchResultBuilder; export 'src/widgets/either.dart'; +export 'src/util/color_ramp.dart'; +export 'src/util/rounded_corners.dart'; export 'src/widgets/map/base_map_state.dart' show BaseMapWidgetState; export 'src/widgets/map/compass_widget.dart'; export 'src/widgets/map/indoor_widget.dart'; export 'src/widgets/map/map_widget.dart' show MapWidget, MapWidgetController; -export 'src/widgets/map/map_widget_color_scheme.dart'; +export 'src/widgets/map/map_widget_theme.dart'; export 'src/widgets/map/my_location_widget.dart'; export 'src/widgets/map/themed_map_controlling_widget.dart'; -export 'src/widgets/map/themed_map_controlling_widget_state.dart'; export 'src/widgets/map/traffic_widget.dart'; -export 'src/widgets/map/zoom_widget.dart' - show ZoomWidget, ZoomWidgetColorScheme; +export 'src/widgets/map/parking_widget.dart'; +export 'src/widgets/map/zoom_widget.dart' show ZoomWidget; +export 'src/widgets/navigator/speed_limit_widget.dart' + show SpeedLimitWidget, SpeedLimitTheme, SpeedometerTheme, CameraProgressTheme, SpeedLimitWidgetTheme; +export 'src/widgets/navigator/maneuver_widget.dart' show ManeuverWidget, ManeuverWidgetTheme; +export 'src/widgets/navigator/dashboard_widget.dart' show DashboardWidget, DashboardWidgetTheme; +export 'src/widgets/routing/routes_list_widget.dart'; +export 'src/widgets/routing/route_card.dart'; diff --git a/lib/src/generated/optional.dart b/lib/src/generated/optional.dart index f4c288f..e79a5d3 100644 --- a/lib/src/generated/optional.dart +++ b/lib/src/generated/optional.dart @@ -1,4 +1,5 @@ class Optional { final T value; + const Optional(this.value); } diff --git a/lib/src/platform/map/map.dart b/lib/src/platform/map/map.dart index ca2ff75..dd927ec 100644 --- a/lib/src/platform/map/map.dart +++ b/lib/src/platform/map/map.dart @@ -22,7 +22,7 @@ extension SetAttributesNavigationParking on sdk.Map { const attributeName = 'navigatorOn'; final attributeValue = attributes.getAttributeValue(attributeName); final oldValue = attributeValue.asBoolean; - if (oldValue != null && oldValue != isOn) { + if (oldValue != isOn) { attributes.setAttributeValue( attributeName, sdk.AttributeValue.boolean(isOn), @@ -31,14 +31,13 @@ extension SetAttributesNavigationParking on sdk.Map { } bool isParkingOn() { - return attributes.getAttributeValue(parkingOnAttributeName).asBoolean ?? - false; + return attributes.getAttributeValue(parkingOnAttributeName).asBoolean ?? false; } void setParkingOn({required bool isOn}) { final attributeValue = attributes.getAttributeValue(parkingOnAttributeName); final oldValue = attributeValue.asBoolean; - if (oldValue != null && oldValue != isOn) { + if (oldValue != isOn) { attributes.setAttributeValue( parkingOnAttributeName, sdk.AttributeValue.boolean(isOn), diff --git a/lib/src/platform/map/map_theme.dart b/lib/src/platform/map/map_theme.dart index 870f8dd..7aba417 100644 --- a/lib/src/platform/map/map_theme.dart +++ b/lib/src/platform/map/map_theme.dart @@ -15,7 +15,7 @@ class MapTheme { /// Признак темы, определяющий режим (темный/светлый), для которого будет /// использоваться тема. /// При использовании ThemedMapControl, эти UI-элементы будут ориентироваться - /// на данный признак при выборе MapControlColorScheme + /// на данный признак при выборе MapControlTheme final MapThemeColorMode colorMode; const MapTheme({ diff --git a/lib/src/util/color_ramp.dart b/lib/src/util/color_ramp.dart new file mode 100644 index 0000000..9e8bbc1 --- /dev/null +++ b/lib/src/util/color_ramp.dart @@ -0,0 +1,29 @@ +import 'dart:ui'; + +class ColorMark { + final Color color; + final T maxValue; + + const ColorMark({ + required this.color, + required this.maxValue, + }); +} + +class ColorRamp { + final List> colors; + + const ColorRamp({ + required this.colors, + }); + + Color getColor(T value) { + int index = 0; + + while (index < colors.length - 1 && colors[index].maxValue < value) { + index++; + } + + return colors[index].color; + } +} diff --git a/lib/src/util/format_duration.dart b/lib/src/util/format_duration.dart new file mode 100644 index 0000000..4abbe92 --- /dev/null +++ b/lib/src/util/format_duration.dart @@ -0,0 +1,21 @@ +String formatDuration(Duration duration) { + var seconds = duration.inSeconds; + final days = seconds ~/ Duration.secondsPerDay; + seconds -= days * Duration.secondsPerDay; + final hours = seconds ~/ Duration.secondsPerHour; + seconds -= hours * Duration.secondsPerHour; + final minutes = seconds ~/ Duration.secondsPerMinute; + + final List tokens = []; + if (days != 0) { + tokens.add('${days} д'); + } + if (tokens.isNotEmpty || hours != 0) { + tokens.add('${hours} ч'); + } + if (tokens.isNotEmpty || minutes != 0) { + tokens.add('${minutes} мин'); + } + + return tokens.join(' '); +} diff --git a/lib/src/util/fromat_distance.dart b/lib/src/util/fromat_distance.dart new file mode 100644 index 0000000..7d45059 --- /dev/null +++ b/lib/src/util/fromat_distance.dart @@ -0,0 +1,47 @@ +import 'dart:math'; + +FormattedMeasure formatMeters(int millis) { + final meters = millis / 1000; + if (meters > 3000) { + // Показываем в километрах с точностью до целых + final kilometers = (meters / 1000).floor(); + return FormattedMeasure(kilometers.toString(), "км"); + } + + if (meters > 1000) { + // Показываем в километрах с точностью до одного десятичного знака + final hundredsOfMeters = (meters / 100).floor(); + final distanceKm = (hundredsOfMeters / 10).floor(); + return FormattedMeasure(distanceKm.floor().toString(), "км"); + } + + if (meters > 500) { + // Показываем с точностью до 100 м + final hundredsOfMeters = (meters / 100).floor(); + final distanceM = hundredsOfMeters * 100; + return FormattedMeasure(distanceM.floor().toString(), "м"); + } + + if (meters > 250) { + // Показываем с точностью до 50 м + final fiftiesOfMeters = (meters / 50).floor(); + final distanceM = fiftiesOfMeters * 50; + return FormattedMeasure(distanceM.floor().toString(), "м"); + } + + if (meters == 0) { + return FormattedMeasure("0", "м"); + } + + // Показываем с точностью до 10 м + final tensOfMeters = max(1, meters / 10).floor(); + final distanceM = tensOfMeters * 10; + return FormattedMeasure(distanceM.floor().toString(), "м"); +} + +class FormattedMeasure { + FormattedMeasure(this.value, this.unit); + + final String value; + final String unit; +} diff --git a/lib/src/util/measure_size.dart b/lib/src/util/measure_size.dart new file mode 100644 index 0000000..5c00c05 --- /dev/null +++ b/lib/src/util/measure_size.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class MeasureSizeRenderObject extends RenderProxyBox { + Size? oldSize; + Function(Size size) onChange; + + MeasureSizeRenderObject(this.onChange); + + @override + void performLayout() { + super.performLayout(); + + Size newSize = child!.size; + if (oldSize == newSize) return; + + oldSize = newSize; + WidgetsBinding.instance.addPostFrameCallback((_) { + onChange(newSize); + }); + } +} + +class MeasureSize extends SingleChildRenderObjectWidget { + final Function(Size size) onChange; + + const MeasureSize({ + Key? key, + required this.onChange, + required Widget child, + }) : super(key: key, child: child); + + @override + RenderObject createRenderObject(BuildContext context) { + return MeasureSizeRenderObject(onChange); + } + + @override + void updateRenderObject(BuildContext context, covariant MeasureSizeRenderObject renderObject) { + renderObject.onChange = onChange; + } +} diff --git a/lib/src/util/no_overscroll_behavior.dart b/lib/src/util/no_overscroll_behavior.dart new file mode 100644 index 0000000..379e09e --- /dev/null +++ b/lib/src/util/no_overscroll_behavior.dart @@ -0,0 +1,8 @@ +import 'package:flutter/widgets.dart'; + +class NoOverscrollBehavior extends ScrollBehavior { + @override + Widget buildOverscrollIndicator(context, child, details) { + return child; + } +} diff --git a/lib/src/util/rounded_corners.dart b/lib/src/util/rounded_corners.dart new file mode 100644 index 0000000..f6588ff --- /dev/null +++ b/lib/src/util/rounded_corners.dart @@ -0,0 +1,45 @@ +class RoundedCorners { + final bool topLeft; + final bool topRight; + final bool bottomLeft; + final bool bottomRight; + + const RoundedCorners.only({ + this.topLeft = false, + this.topRight = false, + this.bottomLeft = false, + this.bottomRight = false, + }); + + const RoundedCorners.left() + : this.only( + topLeft: true, + bottomLeft: true, + ); + + const RoundedCorners.right() + : this.only( + topRight: true, + bottomRight: true, + ); + + const RoundedCorners.top() + : this.only( + topLeft: true, + topRight: true, + ); + + const RoundedCorners.bottom() + : this.only( + bottomLeft: true, + bottomRight: true, + ); + + const RoundedCorners.all() + : this.only( + topLeft: true, + topRight: true, + bottomLeft: true, + bottomRight: true, + ); +} diff --git a/lib/src/widgets/map/base_map_control.dart b/lib/src/widgets/map/base_map_control.dart new file mode 100644 index 0000000..a2d33c7 --- /dev/null +++ b/lib/src/widgets/map/base_map_control.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; + +import '../../util/rounded_corners.dart'; +import 'map_widget_theme.dart'; + +class BaseMapControl extends StatefulWidget { + const BaseMapControl({ + super.key, + required this.theme, + required this.child, + required this.isEnabled, + this.roundedCorners = const RoundedCorners.all(), + this.onTap, + this.onPress, + this.onRelease, + }); + + final bool isEnabled; + final RoundedCorners roundedCorners; + final MapControlTheme theme; + final Widget child; + + final VoidCallback? onTap; + final VoidCallback? onPress; + final VoidCallback? onRelease; + + @override + State createState() => _BaseMapControlState(); +} + +class _BaseMapControlState extends State { + bool isPressed = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.onTap, + onTapDown: (details) { + if (widget.isEnabled) { + setState(() { + isPressed = true; + widget.onPress?.call(); + }); + } + }, + onTapUp: (details) { + setState(() { + widget.onRelease?.call(); + isPressed = false; + }); + }, + onLongPressUp: () { + setState(() { + widget.onRelease?.call(); + isPressed = false; + }); + }, + onLongPressCancel: () { + setState(() { + widget.onRelease?.call(); + isPressed = false; + }); + }, + child: Container( + width: widget.theme.size, + height: widget.theme.size, + decoration: BoxDecoration( + color: isPressed ? widget.theme.surfacePressedColor : widget.theme.surfaceColor, + boxShadow: widget.theme.shadows, + borderRadius: BorderRadius.only( + topLeft: widget.roundedCorners.topLeft ? Radius.circular(widget.theme.borderRadius) : Radius.zero, + topRight: widget.roundedCorners.topRight ? Radius.circular(widget.theme.borderRadius) : Radius.zero, + bottomLeft: widget.roundedCorners.bottomLeft ? Radius.circular(widget.theme.borderRadius) : Radius.zero, + bottomRight: widget.roundedCorners.bottomRight ? Radius.circular(widget.theme.borderRadius) : Radius.zero, + ), + ), + child: widget.child, + ), + ); + } +} + +class MapControlTheme extends MapWidgetTheme { + final double size; + final double borderRadius; + + final Color surfaceColor; + final Color surfacePressedColor; + + final double iconSize; + + final Color iconDisabledColor; + final Color iconInactiveColor; + final Color iconActiveColor; + + final List shadows; + + const MapControlTheme({ + required this.size, + required this.borderRadius, + required this.surfaceColor, + required this.surfacePressedColor, + required this.iconSize, + required this.iconDisabledColor, + required this.iconInactiveColor, + required this.iconActiveColor, + required this.shadows, + }); + + /// Цветовая схема UI–элемента для светлого режима по умолчанию. + static const MapControlTheme defaultLight = MapControlTheme( + size: 44, + borderRadius: 8, + surfaceColor: Color(0xffffffff), + surfacePressedColor: Color(0xffeeeeee), + iconSize: 24, + iconInactiveColor: Color(0xff4d4d4d), + iconDisabledColor: Color(0xffcccccc), + iconActiveColor: Color(0xff057ddf), + shadows: [ + BoxShadow( + color: Color(0x12000000), + blurRadius: 1, + ), + BoxShadow( + color: Color(0x0D000000), + offset: Offset(0, 2), + blurRadius: 4, + ), + ], + ); + + /// Цветовая схема UI–элемента для темного режима по умолчанию. + static const MapControlTheme defaultDark = MapControlTheme( + size: 44, + borderRadius: 8, + surfaceColor: Color(0xff121212), + surfacePressedColor: Color(0xff3C3C3C), + iconSize: 24, + iconInactiveColor: Color(0xffcccccc), + iconDisabledColor: Color(0xff808080), + iconActiveColor: Color(0xff70aee0), + shadows: [ + BoxShadow( + color: Color(0x14000000), + offset: Offset(0, 1), + blurRadius: 4, + ), + BoxShadow( + color: Color(0x0A000000), + spreadRadius: 0.5, + ), + ], + ); + + @override + MapControlTheme copyWith({ + double? size, + double? borderRadius, + Color? surfaceColor, + Color? surfacePressedColor, + double? iconSize, + Color? iconDisabledColor, + Color? iconInactiveColor, + Color? iconActiveColor, + List? shadows, + }) { + return MapControlTheme( + size: size ?? this.size, + borderRadius: borderRadius ?? this.borderRadius, + surfaceColor: surfaceColor ?? this.surfaceColor, + surfacePressedColor: surfacePressedColor ?? this.surfacePressedColor, + iconSize: iconSize ?? this.iconSize, + iconDisabledColor: iconDisabledColor ?? this.iconDisabledColor, + iconInactiveColor: iconInactiveColor ?? this.iconInactiveColor, + iconActiveColor: iconActiveColor ?? this.iconActiveColor, + shadows: shadows ?? this.shadows, + ); + } +} diff --git a/lib/src/widgets/map/compass_widget.dart b/lib/src/widgets/map/compass_widget.dart index f65dbc7..73e16ac 100644 --- a/lib/src/widgets/map/compass_widget.dart +++ b/lib/src/widgets/map/compass_widget.dart @@ -7,39 +7,25 @@ import '../../generated/dart_bindings.dart' as sdk; import '../../generated/stateful_channel.dart'; import '../../util/plugin_name.dart'; -import 'map_widget_color_scheme.dart'; +import 'map_widget_theme.dart'; import 'themed_map_controlling_widget.dart'; -import 'themed_map_controlling_widget_state.dart'; /// Виджет управления компасом. -class CompassWidget - extends ThemedMapControllingWidget { +class CompassWidget extends ThemedMapControllingWidget { const CompassWidget({ super.key, - CompassWidgetColorScheme? light, - CompassWidgetColorScheme? dark, + CompassWidgetTheme? light, + CompassWidgetTheme? dark, }) : super( - light: light ?? defaultLightColorScheme, - dark: dark ?? defaultDarkColorScheme, + light: light ?? CompassWidgetTheme.defaultLight, + dark: dark ?? CompassWidgetTheme.defaultDark, ); - /// Цветовая схема виджета для светлого режима по умолчанию. - static const defaultLightColorScheme = CompassWidgetColorScheme( - surfaceColor: Color(0xffffffff), - ); - - /// Цветовая схема виджета для темного режима по умолчанию. - static const defaultDarkColorScheme = CompassWidgetColorScheme( - surfaceColor: Color(0xff121212), - ); - @override - ThemedMapControllingWidgetState - createState() => _CompassWidgetState(); + ThemedMapControllingWidgetState createState() => _CompassWidgetState(); } -class _CompassWidgetState extends ThemedMapControllingWidgetState { +class _CompassWidgetState extends ThemedMapControllingWidgetState { late sdk.CompassControlModel model; StatefulChannel? bearingSubscription; @@ -68,10 +54,10 @@ class _CompassWidgetState extends ThemedMapControllingWidgetState model.onClicked(), child: Container( - width: 36, - height: 36, + width: theme.size, + height: theme.size, decoration: BoxDecoration( - color: colorScheme.surfaceColor, + color: theme.surfaceColor, shape: BoxShape.circle, ), child: Transform.rotate( @@ -79,8 +65,8 @@ class _CompassWidgetState extends ThemedMapControllingWidgetState { +class IndoorWidget extends ThemedMapControllingWidget { const IndoorWidget({ - IndoorWidgetColorScheme? light, - IndoorWidgetColorScheme? dark, + IndoorWidgetTheme? light, + IndoorWidgetTheme? dark, super.key, }) : super( - light: light ?? defaultLightColorScheme, - dark: dark ?? defaultDarkColorScheme, + light: light ?? defaultLightTheme, + dark: dark ?? defaultDarkTheme, ); /// Цветовая схема UI–элемента для светлого режима по умолчанию. - static const IndoorWidgetColorScheme defaultLightColorScheme = - IndoorWidgetColorScheme( + static const IndoorWidgetTheme defaultLightTheme = IndoorWidgetTheme( surfaceColor: Color(0xffffffff), selectedFloorColor: Color(0xFFCDE5F9), floorTextColor: Color(0xFF000000), @@ -34,8 +32,7 @@ class IndoorWidget extends ThemedMapControllingWidget { ); /// Цветовая схема UI–элемента для темного режима по умолчанию. - static const IndoorWidgetColorScheme defaultDarkColorScheme = - IndoorWidgetColorScheme( + static const IndoorWidgetTheme defaultDarkTheme = IndoorWidgetTheme( surfaceColor: Color(0xff121212), selectedFloorColor: Color(0xFF16232D), floorTextColor: Color(0xffffffff), @@ -43,12 +40,10 @@ class IndoorWidget extends ThemedMapControllingWidget { ); @override - ThemedMapControllingWidgetState - createState() => _IndoorWidgetState(); + ThemedMapControllingWidgetState createState() => _IndoorWidgetState(); } -class _IndoorWidgetState extends ThemedMapControllingWidgetState { +class _IndoorWidgetState extends ThemedMapControllingWidgetState { final scrollController = ScrollController(); static const singleElementHeight = 40.0; @@ -118,7 +113,7 @@ class _IndoorWidgetState extends ThemedMapControllingWidgetState - (countBeforeScroll - 3) * elementHeight) { + if (targetY - scrollController.offset > (countBeforeScroll - 3) * elementHeight) { scrollController.jumpTo( min(maxScrollY, targetY - (countBeforeScroll - 3) * elementHeight), ); @@ -287,8 +279,7 @@ class _IndoorWidgetState extends ThemedMapControllingWidgetState 0) { + if (scrollController.hasClients && scrollController.position.maxScrollExtent > 0) { final shouldShowAnyShadow = levelNames.value.length > 5; final maxScroll = scrollController.position.maxScrollExtent; final currentScroll = scrollController.offset; @@ -299,13 +290,13 @@ class _IndoorWidgetState extends ThemedMapControllingWidgetState { +class MyLocationWidget extends ThemedMapControllingWidget { const MyLocationWidget({ super.key, - MyLocationWidgetColorScheme? light, - MyLocationWidgetColorScheme? dark, + MapControlTheme? light, + MapControlTheme? dark, }) : super( - light: light ?? defaultLightColorScheme, - dark: dark ?? defaultDarkColorScheme, + light: light ?? MapControlTheme.defaultLight, + dark: dark ?? MapControlTheme.defaultDark, ); - /// Цветовая схема UI–элемента для светлого режима по умолчанию. - static const MyLocationWidgetColorScheme defaultLightColorScheme = - MyLocationWidgetColorScheme( - surfaceColor: Color(0xffffffff), - iconInactiveColor: Color(0xff4d4d4d), - iconDisabledColor: Color(0xffcccccc), - iconActiveColor: Color(0xff057ddf), - ); - - /// Цветовая схема UI–элемента для темного режима по умолчанию. - static const MyLocationWidgetColorScheme defaultDarkColorScheme = - MyLocationWidgetColorScheme( - surfaceColor: Color(0xff121212), - iconInactiveColor: Color(0xffcccccc), - iconDisabledColor: Color(0xff808080), - iconActiveColor: Color(0xff70aee0), - ); - @override - ThemedMapControllingWidgetState - createState() => _MyLocationWidgetState(); + ThemedMapControllingWidgetState createState() => _MyLocationWidgetState(); } -class _MyLocationWidgetState extends ThemedMapControllingWidgetState< - MyLocationWidget, MyLocationWidgetColorScheme> { +class _MyLocationWidgetState extends ThemedMapControllingWidgetState { late sdk.MyLocationControlModel model; ValueNotifier isEnabled = ValueNotifier(null); @@ -60,10 +37,8 @@ class _MyLocationWidgetState extends ThemedMapControllingWidgetState< @override void onAttachedToMap(sdk.Map map) { model = sdk.MyLocationControlModel(map); - isEnabledSuscription = - model.isEnabledChannel.listen((state) => isEnabled.value = state); - followStateSubscription = - model.followStateChannel.listen((state) => followState.value = state); + isEnabledSuscription = model.isEnabledChannel.listen((state) => isEnabled.value = state); + followStateSubscription = model.followStateChannel.listen((state) => followState.value = state); } @override @@ -79,82 +54,46 @@ class _MyLocationWidgetState extends ThemedMapControllingWidgetState< return ValueListenableBuilder( valueListenable: isEnabled, builder: (context, isEnabledState, _) { - return GestureDetector( + return BaseMapControl( + theme: theme, onTap: isEnabledState ?? false ? model.onClicked : () {}, - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: colorScheme.surfaceColor, - shape: BoxShape.circle, - boxShadow: const [ - WidgetShadows.mapWidgetBoxShadow, - ], - ), - child: ValueListenableBuilder( - valueListenable: followState, - builder: (context, state, _) { - final iconAssetName = state == - sdk.CameraFollowState.followDirection - ? 'packages/$pluginName/assets/icons/dgis_follow_direction.svg' - : 'packages/$pluginName/assets/icons/dgis_my_location.svg'; - - Color iconColor; - if (isEnabledState != true) { - iconColor = colorScheme.iconDisabledColor; - } else { - switch (state) { - case sdk.CameraFollowState.off: - iconColor = colorScheme.iconInactiveColor; - case sdk.CameraFollowState.followPosition: - iconColor = colorScheme.iconActiveColor; - case sdk.CameraFollowState.followDirection: - iconColor = colorScheme.iconActiveColor; - default: - iconColor = colorScheme.iconInactiveColor; - } + isEnabled: isEnabledState ?? false, + child: ValueListenableBuilder( + valueListenable: followState, + builder: (context, state, _) { + final iconAssetName = state == sdk.CameraFollowState.followDirection + ? 'packages/$pluginName/assets/icons/dgis_follow_direction.svg' + : 'packages/$pluginName/assets/icons/dgis_my_location.svg'; + + Color iconColor; + if (isEnabledState != true) { + iconColor = theme.iconDisabledColor; + } else { + switch (state) { + case sdk.CameraFollowState.off: + iconColor = theme.iconInactiveColor; + case sdk.CameraFollowState.followPosition: + iconColor = theme.iconActiveColor; + case sdk.CameraFollowState.followDirection: + iconColor = theme.iconActiveColor; + default: + iconColor = theme.iconInactiveColor; } + } - return SvgPicture.asset( + return Center( + child: SvgPicture.asset( iconAssetName, - width: 24, - height: 24, + width: theme.iconSize, + height: theme.iconSize, fit: BoxFit.none, colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn), - ); - }, - ), + ), + ); + }, ), ); }, ); } } - -class MyLocationWidgetColorScheme extends MapWidgetColorScheme { - final Color surfaceColor; - final Color iconDisabledColor; - final Color iconInactiveColor; - final Color iconActiveColor; - - const MyLocationWidgetColorScheme({ - required this.surfaceColor, - required this.iconDisabledColor, - required this.iconInactiveColor, - required this.iconActiveColor, - }); - @override - MyLocationWidgetColorScheme copyWith({ - Color? surfaceColor, - Color? iconDisabledColor, - Color? iconInactiveColor, - Color? iconActiveColor, - }) { - return MyLocationWidgetColorScheme( - surfaceColor: surfaceColor ?? this.surfaceColor, - iconDisabledColor: iconDisabledColor ?? this.iconDisabledColor, - iconInactiveColor: iconInactiveColor ?? this.iconInactiveColor, - iconActiveColor: iconActiveColor ?? this.iconActiveColor, - ); - } -} diff --git a/lib/src/widgets/map/parking_widget.dart b/lib/src/widgets/map/parking_widget.dart new file mode 100644 index 0000000..ac3e936 --- /dev/null +++ b/lib/src/widgets/map/parking_widget.dart @@ -0,0 +1,87 @@ +import 'dart:async'; + +import 'package:dgis_mobile_sdk_full/src/platform/map/map.dart'; +import 'package:dgis_mobile_sdk_full/src/util/rounded_corners.dart'; +import 'package:dgis_mobile_sdk_full/src/widgets/map/base_map_control.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../generated/dart_bindings.dart' as sdk; +import '../../util/plugin_name.dart'; + +import 'themed_map_controlling_widget.dart'; + +/// Виджет, переключающий отображение парковок на карте. +/// Может использоваться только как child в MapWidget на любом уровне вложенности. +class ParkingWidget extends ThemedMapControllingWidget { + const ParkingWidget({ + super.key, + this.roundedCorners = const RoundedCorners.all(), + MapControlTheme? light, + MapControlTheme? dark, + }) : super( + light: light ?? MapControlTheme.defaultLight, + dark: dark ?? MapControlTheme.defaultDark, + ); + + final RoundedCorners roundedCorners; + + @override + ThemedMapControllingWidgetState createState() => _TrafficWidgetState(); +} + +class _TrafficWidgetState extends ThemedMapControllingWidgetState { + ValueNotifier isEnabled = ValueNotifier(false); + StreamSubscription>? stateSubscription; + + sdk.Map? map; + + @override + void onAttachedToMap(sdk.Map map) { + this.map = map; + stateSubscription = map.attributes.changed.listen((newState) { + if (newState.contains(SetAttributesNavigationParking.parkingOnAttributeName)) { + isEnabled.value = map.isParkingOn(); + } + }); + isEnabled.value = map.isParkingOn(); + } + + @override + void onDetachedFromMap() { + stateSubscription?.cancel(); + stateSubscription = null; + map = null; + } + + void _toggleParking() { + map?.setParkingOn(isOn: !isEnabled.value); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: isEnabled, + builder: (_, currentState, __) { + return BaseMapControl( + theme: theme, + isEnabled: true, + roundedCorners: widget.roundedCorners, + onTap: _toggleParking, + child: Center( + child: SvgPicture.asset( + 'packages/$pluginName/assets/icons/dgis_parking.svg', + width: theme.iconSize, + height: theme.iconSize, + fit: BoxFit.none, + colorFilter: ColorFilter.mode( + currentState ? theme.iconActiveColor : theme.iconInactiveColor, + BlendMode.srcIn, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/src/widgets/map/themed_map_controlling_widget.dart b/lib/src/widgets/map/themed_map_controlling_widget.dart index 7220a18..1ad1b10 100644 --- a/lib/src/widgets/map/themed_map_controlling_widget.dart +++ b/lib/src/widgets/map/themed_map_controlling_widget.dart @@ -1,12 +1,54 @@ import 'package:flutter/widgets.dart'; -import 'map_widget_color_scheme.dart'; +import 'map_widget_theme.dart'; + +import '../../platform/map/map_theme.dart'; +import 'base_map_state.dart'; +import 'map_widget.dart'; + +/// Базовый класс для реализации стейта виджетов управления картой, подверженным +/// изменениям цветовой схемы в течение жизненного цикла. +/// Помимо объекта sdk.Map, предоставляет доступ к теме карты [MapTheme], а также реагирует на +/// ее изменения для того, чтобы синхронно обновлять цветовую схему. +/// Виджет, использующий этот класс как базовый для своего State, должен быть помещен +/// в child виджета [MapWidget]. В ином случае будет брошено исключение при использовании. +abstract class ThemedMapControllingWidgetState, S extends MapWidgetTheme> + extends BaseMapWidgetState { + late S theme; + MapThemeColorMode? _colorMode; + + @override + void didChangeDependencies() { + final mapTheme = mapThemeOf(context); + if (_colorMode == mapTheme?.colorMode) { + return; + } + if (mapTheme != null) { + _colorMode = mapTheme.colorMode; + } + switch (_colorMode) { + case MapThemeColorMode.light: + setState(() { + theme = widget.light; + }); + case MapThemeColorMode.dark: + setState(() { + theme = widget.dark; + }); + default: + setState(() { + theme = widget.light; + }); + } + + super.didChangeDependencies(); + } +} /// Базовый класс для реализации виджетов карты, способных изменять цветовую схему /// в зависимости от признака colorMode темы карты MapTheme. /// Должен использоваться совместно с ThemedMapControllingWidgetState. -abstract class ThemedMapControllingWidget - extends StatefulWidget { +abstract class ThemedMapControllingWidget extends StatefulWidget { final T light; final T dark; diff --git a/lib/src/widgets/map/themed_map_controlling_widget_state.dart b/lib/src/widgets/map/themed_map_controlling_widget_state.dart deleted file mode 100644 index c97c091..0000000 --- a/lib/src/widgets/map/themed_map_controlling_widget_state.dart +++ /dev/null @@ -1,45 +0,0 @@ -import '../../platform/map/map_theme.dart'; -import 'base_map_state.dart'; -import 'map_widget.dart'; -import 'map_widget_color_scheme.dart'; -import 'themed_map_controlling_widget.dart'; - -/// Базовый класс для реализации стейта виджетов управления картой, подверженным -/// изменениям цветовой схемы в течение жизненного цикла. -/// Помимо объекта sdk.Map, предоставляет доступ к теме карты [MapTheme], а также реагирует на -/// ее изменения для того, чтобы синхронно обновлять цветовую схему. -/// Виджет, использующий этот класс как базовый для своего State, должен быть помещен -/// в child виджета [MapWidget]. В ином случае будет брошено исключение при использовании. -abstract class ThemedMapControllingWidgetState< - T extends ThemedMapControllingWidget, - S extends MapWidgetColorScheme> extends BaseMapWidgetState { - late S colorScheme; - MapThemeColorMode? _colorMode; - - @override - void didChangeDependencies() { - final mapTheme = mapThemeOf(context); - if (_colorMode == mapTheme?.colorMode) { - return; - } - if (mapTheme != null) { - _colorMode = mapTheme.colorMode; - } - switch (_colorMode) { - case MapThemeColorMode.light: - setState(() { - colorScheme = widget.light; - }); - case MapThemeColorMode.dark: - setState(() { - colorScheme = widget.dark; - }); - default: - setState(() { - colorScheme = widget.light; - }); - } - - super.didChangeDependencies(); - } -} diff --git a/lib/src/widgets/map/traffic_widget.dart b/lib/src/widgets/map/traffic_widget.dart index 7055d34..9238117 100644 --- a/lib/src/widgets/map/traffic_widget.dart +++ b/lib/src/widgets/map/traffic_widget.dart @@ -1,62 +1,39 @@ import 'dart:async'; +import 'package:dgis_mobile_sdk_full/src/util/color_ramp.dart'; +import 'package:dgis_mobile_sdk_full/src/util/rounded_corners.dart'; +import 'package:dgis_mobile_sdk_full/src/widgets/map/base_map_control.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../generated/dart_bindings.dart' as sdk; import '../../util/plugin_name.dart'; import '../moving_segment_progress_indicator.dart'; -import '../widget_shadows.dart'; -import 'map_widget_color_scheme.dart'; +import 'map_widget_theme.dart'; import 'themed_map_controlling_widget.dart'; -import 'themed_map_controlling_widget_state.dart'; /// Виджет, отображающий пробочный балл в регионе и переключающий отображение /// пробок на карте. /// Может использоваться только как child в MapWidget на любом уровне вложенности. -class TrafficWidget - extends ThemedMapControllingWidget { +class TrafficWidget extends ThemedMapControllingWidget { const TrafficWidget({ super.key, - TrafficWidgetColorScheme? light, - TrafficWidgetColorScheme? dark, + this.roundedCorners = const RoundedCorners.all(), + TrafficWidgetTheme? light, + TrafficWidgetTheme? dark, }) : super( - light: light ?? defaultLightColorScheme, - dark: dark ?? defaultDarkColorScheme, + light: light ?? TrafficWidgetTheme.defaultLight, + dark: dark ?? TrafficWidgetTheme.defaultDark, ); - @override - ThemedMapControllingWidgetState - createState() => _TrafficWidgetState(); - - /// Цветовая схема UI–элемента для светлого режима по умолчанию. - static const TrafficWidgetColorScheme defaultLightColorScheme = - TrafficWidgetColorScheme( - heavyTrafficColor: Color(0xffd15536), - mediumTrafficColor: Color(0xffffba00), - lightTrafficColor: Color(0xff58a600), - unactiveColor: Color(0xffffffff), - surfaceColor: Color(0xffffffff), - iconColor: Color(0xff4d4d4d), - scoreTextColor: Color(0xff4d4d4d), - ); + final RoundedCorners roundedCorners; - /// Цветовая схема UI–элемента для темного режима по умолчанию. - static const TrafficWidgetColorScheme defaultDarkColorScheme = - TrafficWidgetColorScheme( - heavyTrafficColor: Color(0xffd15536), - mediumTrafficColor: Color(0xffffba00), - lightTrafficColor: Color(0xff58a600), - unactiveColor: Color(0xff121212), - surfaceColor: Color(0xff121212), - iconColor: Color(0xffcccccc), - scoreTextColor: Color(0xffcccccc), - ); + @override + ThemedMapControllingWidgetState createState() => _TrafficWidgetState(); } -class _TrafficWidgetState extends ThemedMapControllingWidgetState { +class _TrafficWidgetState extends ThemedMapControllingWidgetState { final ValueNotifier state = ValueNotifier(null); StreamSubscription? stateSubscription; @@ -85,73 +62,63 @@ class _TrafficWidgetState extends ThemedMapControllingWidgetState model.onClicked(), - child: Stack( - children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - boxShadow: const [ - WidgetShadows.mapWidgetBoxShadow, - ], - color: - currentState?.status == sdk.TrafficControlStatus.enabled - ? _getTrafficColor(currentState?.score) - : colorScheme.surfaceColor, // Inner color - shape: BoxShape.circle, - border: Border.all( - color: currentState?.status == - sdk.TrafficControlStatus.enabled - ? colorScheme.surfaceColor - : _getTrafficColor( - currentState?.score, - ), // Border color - width: 2, // Border width + child: Center( + child: switch ((currentState?.score, currentState?.status)) { + (_, sdk.TrafficControlStatus.loading) => MovingSegmentProgressIndicator( + width: theme.controlTheme.iconSize, + height: theme.controlTheme.iconSize, + thickness: theme.borderWidth, + color: theme.loaderColor, + segmentSize: 0.15, + duration: const Duration(milliseconds: 2500), + ), + (null, _) => Center( + child: SvgPicture.asset( + 'packages/$pluginName/assets/icons/dgis_traffic.svg', + width: 24, + height: 24, + fit: BoxFit.none, + colorFilter: ColorFilter.mode( + switch (currentState?.status) { + sdk.TrafficControlStatus.enabled => theme.controlTheme.iconActiveColor, + sdk.TrafficControlStatus.disabled => theme.controlTheme.iconDisabledColor, + _ => theme.controlTheme.iconInactiveColor, + }, + BlendMode.srcIn, + ), ), ), - child: currentState?.score == null - ? Center( - child: SvgPicture.asset( - 'packages/$pluginName/assets/icons/dgis_traffic_icon.svg', - width: 24, - height: 24, - fit: BoxFit.none, - colorFilter: ColorFilter.mode( - colorScheme.iconColor, - BlendMode.srcIn, - ), - ), - ) - : Center( - child: Text( - currentState!.score.toString(), - textAlign: TextAlign.center, - style: TextStyle( - leadingDistribution: TextLeadingDistribution.even, - height: 1, - color: colorScheme.scoreTextColor, - fontSize: 19, - ), - ), + (_, _) => Container( + width: theme.controlTheme.iconSize, + height: theme.controlTheme.iconSize, + decoration: BoxDecoration( + color: currentState?.status == sdk.TrafficControlStatus.enabled + ? _getTrafficColor(currentState?.score) + : theme.controlTheme.surfaceColor, // Inner color + shape: BoxShape.circle, + border: Border.all( + color: _getTrafficColor( + currentState?.score, ), - ), - Visibility( - visible: - currentState?.status == sdk.TrafficControlStatus.loading, - child: MovingSegmentProgressIndicator( - width: 44, - height: 44, - thickness: 2, - color: colorScheme.lightTrafficColor, - segmentSize: 0.15, - duration: const Duration(milliseconds: 2500), + width: 2, // Border width + ), + ), + child: Center( + child: Text( + currentState!.score.toString(), + textAlign: TextAlign.center, + style: theme.scoreTextStyle, + ), + ), ), - ), - ], + }, ), ), ); @@ -161,54 +128,99 @@ class _TrafficWidgetState extends ThemedMapControllingWidgetState trafficColor; + final double borderWidth; + final Color loaderColor; + final TextStyle scoreTextStyle; + final MapControlTheme controlTheme; + + const TrafficWidgetTheme({ + required this.trafficColor, + required this.borderWidth, + required this.loaderColor, + required this.scoreTextStyle, + required this.controlTheme, }); + static const TrafficWidgetTheme defaultLight = TrafficWidgetTheme( + trafficColor: ColorRamp( + colors: [ + ColorMark( + color: Color(0xff58a600), + maxValue: 3, + ), + ColorMark( + color: Color(0xffffba00), + maxValue: 6, + ), + ColorMark( + color: Color(0xffd15536), + maxValue: 999, + ), + ], + ), + borderWidth: 2, + loaderColor: Color(0xff58a600), + scoreTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + height: 1, + color: Color(0xff4d4d4d), + fontSize: 19, + ), + controlTheme: MapControlTheme.defaultLight, + ); + + /// Цветовая схема UI–элемента для темного режима по умолчанию. + static const TrafficWidgetTheme defaultDark = TrafficWidgetTheme( + trafficColor: ColorRamp( + colors: [ + ColorMark( + color: Color(0xff58a600), + maxValue: 3, + ), + ColorMark( + color: Color(0xffffba00), + maxValue: 6, + ), + ColorMark( + color: Color(0xffd15536), + maxValue: 999, + ), + ], + ), + borderWidth: 2, + loaderColor: Color(0xff58a600), + scoreTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + height: 1, + color: Colors.white, + fontSize: 19, + ), + controlTheme: MapControlTheme.defaultDark, + ); + @override - TrafficWidgetColorScheme copyWith({ - Color? heavyTrafficColor, - Color? mediumTrafficColor, - Color? lightTrafficColor, - Color? unactiveColor, - Color? surfaceColor, - Color? iconColor, - Color? scoreTextColor, + TrafficWidgetTheme copyWith({ + ColorRamp? trafficColor, + double? borderWidth, + Color? loaderColor, + TextStyle? scoreTextStyle, + MapControlTheme? controlTheme, }) { - return TrafficWidgetColorScheme( - heavyTrafficColor: heavyTrafficColor ?? this.heavyTrafficColor, - mediumTrafficColor: mediumTrafficColor ?? this.mediumTrafficColor, - lightTrafficColor: lightTrafficColor ?? this.lightTrafficColor, - unactiveColor: unactiveColor ?? this.unactiveColor, - surfaceColor: surfaceColor ?? this.surfaceColor, - iconColor: iconColor ?? this.iconColor, - scoreTextColor: scoreTextColor ?? this.scoreTextColor, + return TrafficWidgetTheme( + trafficColor: trafficColor ?? this.trafficColor, + borderWidth: borderWidth ?? this.borderWidth, + loaderColor: loaderColor ?? this.loaderColor, + scoreTextStyle: scoreTextStyle ?? this.scoreTextStyle, + controlTheme: controlTheme ?? this.controlTheme, ); } } diff --git a/lib/src/widgets/map/zoom_button_widget.dart b/lib/src/widgets/map/zoom_button_widget.dart deleted file mode 100644 index bbca043..0000000 --- a/lib/src/widgets/map/zoom_button_widget.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -import '../widget_shadows.dart'; - -class ZoomButton extends StatefulWidget { - final Color backgroundColor; - final Color pressedBackgroundColor; - final Color activeIconColor; - final Color inactiveIconColor; - final bool isEnabled; - final VoidCallback onClick; - final VoidCallback onRelease; - final String iconResource; - - const ZoomButton({ - required this.backgroundColor, - required this.pressedBackgroundColor, - required this.activeIconColor, - required this.inactiveIconColor, - required this.onClick, - required this.onRelease, - required this.iconResource, - this.isEnabled = true, - super.key, - }); - - @override - State createState() => _ZoomButtonState(); -} - -class _ZoomButtonState extends State { - bool isPressed = false; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTapDown: (details) { - if (widget.isEnabled) { - setState(() { - isPressed = true; - widget.onClick(); - }); - } - }, - onTapUp: (details) { - setState(() { - widget.onRelease(); - isPressed = false; - }); - }, - onLongPressUp: () { - setState(() { - widget.onRelease(); - isPressed = false; - }); - }, - onLongPressCancel: () { - setState(() { - widget.onRelease(); - isPressed = false; - }); - }, - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: isPressed - ? widget.pressedBackgroundColor - : widget.backgroundColor, - shape: BoxShape.circle, - boxShadow: const [ - WidgetShadows.mapWidgetBoxShadow, - ], - ), - child: Center( - child: SvgPicture.asset( - widget.iconResource, - fit: BoxFit.none, - colorFilter: ColorFilter.mode( - widget.isEnabled - ? widget.activeIconColor - : widget.inactiveIconColor, - BlendMode.srcIn, - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/widgets/map/zoom_widget.dart b/lib/src/widgets/map/zoom_widget.dart index b3008bf..b4e37f2 100644 --- a/lib/src/widgets/map/zoom_widget.dart +++ b/lib/src/widgets/map/zoom_widget.dart @@ -1,49 +1,29 @@ import 'dart:async'; +import 'package:dgis_mobile_sdk_full/src/util/rounded_corners.dart'; +import 'package:dgis_mobile_sdk_full/src/widgets/map/base_map_control.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/svg.dart'; import '../../generated/dart_bindings.dart' as sdk; import '../../util/plugin_name.dart'; -import 'map_widget_color_scheme.dart'; import 'themed_map_controlling_widget.dart'; -import 'themed_map_controlling_widget_state.dart'; -import 'zoom_button_widget.dart'; /// Виджет карты, предоставлящий элементы для управления зумом. /// Может использоваться только как child в MapWidget на любом уровне вложенности. -class ZoomWidget extends ThemedMapControllingWidget { +class ZoomWidget extends ThemedMapControllingWidget { const ZoomWidget({ super.key, - ZoomWidgetColorScheme? light, - ZoomWidgetColorScheme? dark, + MapControlTheme? light, + MapControlTheme? dark, }) : super( - light: light ?? defaultLightColorScheme, - dark: dark ?? defaultDarkColorScheme, + light: light ?? MapControlTheme.defaultLight, + dark: dark ?? MapControlTheme.defaultDark, ); @override - ThemedMapControllingWidgetState - createState() => _ZoomWidgetState(); - - /// Цветовая схема UI–элемента для светлого режима по умолчанию. - static const ZoomWidgetColorScheme defaultLightColorScheme = - ZoomWidgetColorScheme( - backgroundColor: Color(0xffffffff), - pressedBackgroundColor: Color(0xffeeeeee), - activeIconColor: Color(0xff4d4d4d), - inactiveIconColor: Color(0xffcccccc), - ); - - /// Цветовая схема UI–элемента для темного режима по умолчанию. - static const ZoomWidgetColorScheme defaultDarkColorScheme = - ZoomWidgetColorScheme( - backgroundColor: Color(0xff121212), - pressedBackgroundColor: Color(0xff3C3C3C), - activeIconColor: Color(0xffCCCCCC), - inactiveIconColor: Color(0xff808080), - ); + ThemedMapControllingWidgetState createState() => _ZoomWidgetState(); } -class _ZoomWidgetState - extends ThemedMapControllingWidgetState { +class _ZoomWidgetState extends ThemedMapControllingWidgetState { final ValueNotifier isZoomInEnabled = ValueNotifier(false); final ValueNotifier isZoomOutEnabled = ValueNotifier(false); @@ -76,66 +56,49 @@ class _ZoomWidgetState children: [ ValueListenableBuilder( valueListenable: isZoomInEnabled, - builder: (_, isEnabled, __) => ZoomButton( - activeIconColor: colorScheme.activeIconColor, - inactiveIconColor: colorScheme.inactiveIconColor, - backgroundColor: colorScheme.backgroundColor, - pressedBackgroundColor: colorScheme.pressedBackgroundColor, + builder: (_, isEnabled, __) => BaseMapControl( + theme: theme, isEnabled: isEnabled, - onClick: () => model.setPressed(sdk.ZoomControlButton.zoomIn, true), - onRelease: () => - model.setPressed(sdk.ZoomControlButton.zoomIn, false), - iconResource: 'packages/$pluginName/assets/icons/dgis_zoom_in.svg', + onPress: () => model.setPressed(sdk.ZoomControlButton.zoomIn, true), + onRelease: () => model.setPressed(sdk.ZoomControlButton.zoomIn, false), + roundedCorners: RoundedCorners.top(), + child: Center( + child: SvgPicture.asset( + 'packages/$pluginName/assets/icons/dgis_zoom_in.svg', + width: theme.iconSize, + height: theme.iconSize, + fit: BoxFit.none, + colorFilter: ColorFilter.mode( + isEnabled ? theme.iconInactiveColor : theme.iconDisabledColor, + BlendMode.srcIn, + ), + ), + ), ), ), - const SizedBox(height: 8), ValueListenableBuilder( valueListenable: isZoomOutEnabled, - builder: (_, isEnabled, __) => ZoomButton( - activeIconColor: colorScheme.activeIconColor, - inactiveIconColor: colorScheme.inactiveIconColor, - backgroundColor: colorScheme.backgroundColor, - pressedBackgroundColor: colorScheme.pressedBackgroundColor, + builder: (_, isEnabled, __) => BaseMapControl( + theme: theme, isEnabled: isEnabled, - onClick: () => - model.setPressed(sdk.ZoomControlButton.zoomOut, true), - onRelease: () => - model.setPressed(sdk.ZoomControlButton.zoomOut, false), - iconResource: 'packages/$pluginName/assets/icons/dgis_zoom_out.svg', + onPress: () => model.setPressed(sdk.ZoomControlButton.zoomOut, true), + onRelease: () => model.setPressed(sdk.ZoomControlButton.zoomOut, false), + roundedCorners: RoundedCorners.bottom(), + child: Center( + child: SvgPicture.asset( + 'packages/$pluginName/assets/icons/dgis_zoom_out.svg', + width: theme.iconSize, + height: theme.iconSize, + fit: BoxFit.none, + colorFilter: ColorFilter.mode( + isEnabled ? theme.iconInactiveColor : theme.iconDisabledColor, + BlendMode.srcIn, + ), + ), + ), ), ), ], ); } } - -/// Цветовая схема для [ZoomWidget]. -class ZoomWidgetColorScheme extends MapWidgetColorScheme { - final Color backgroundColor; - final Color pressedBackgroundColor; - final Color activeIconColor; - final Color inactiveIconColor; - - const ZoomWidgetColorScheme({ - required this.backgroundColor, - required this.pressedBackgroundColor, - required this.activeIconColor, - required this.inactiveIconColor, - }); - - @override - ZoomWidgetColorScheme copyWith({ - Color? activeIconColor, - Color? inactiveIconColor, - Color? backgroundColor, - Color? pressedBackgroundColor, - }) { - return ZoomWidgetColorScheme( - activeIconColor: activeIconColor ?? this.activeIconColor, - inactiveIconColor: inactiveIconColor ?? this.inactiveIconColor, - backgroundColor: backgroundColor ?? this.backgroundColor, - pressedBackgroundColor: - pressedBackgroundColor ?? this.pressedBackgroundColor, - ); - } -} diff --git a/lib/src/widgets/map_widget_box_shadow.dart b/lib/src/widgets/map_widget_box_shadow.dart deleted file mode 100644 index 0987ab5..0000000 --- a/lib/src/widgets/map_widget_box_shadow.dart +++ /dev/null @@ -1,17 +0,0 @@ -// ignore_for_file: overridden_fields - -import 'package:flutter/material.dart'; - -// Испльзуется как BoxShadow в круглых виджетах карты. -class MapWidgetBoxShadow extends BoxShadow { - @override - final Color color = Colors.black.withOpacity(0.2); - @override - final BlurStyle blurStyle = BlurStyle.normal; - @override - final double spreadRadius = 0; - @override - final double blurRadius = 3; - @override - final Offset offset = const Offset(0, 2); -} diff --git a/lib/src/widgets/navigator/dashboard_widget.dart b/lib/src/widgets/navigator/dashboard_widget.dart new file mode 100644 index 0000000..e0ea0c0 --- /dev/null +++ b/lib/src/widgets/navigator/dashboard_widget.dart @@ -0,0 +1,672 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:dgis_mobile_sdk_full/src/util/format_duration.dart'; +import 'package:dgis_mobile_sdk_full/src/util/fromat_distance.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:intl/intl.dart'; + +import '../../generated/dart_bindings.dart' as sdk; +import '../../util/measure_size.dart'; +import '../../util/no_overscroll_behavior.dart'; +import '../../util/plugin_name.dart'; +import '../map/map_widget_theme.dart'; +import '../map/themed_map_controlling_widget.dart'; + +class DashboardWidget extends ThemedMapControllingWidget { + final sdk.NavigationManager navigationManager; + + const DashboardWidget({ + super.key, + required this.navigationManager, + DashboardWidgetTheme? light, + DashboardWidgetTheme? dark, + }) : super( + light: light ?? DashboardWidgetTheme.defaultLight, + dark: dark ?? DashboardWidgetTheme.defaultDark, + ); + + @override + ThemedMapControllingWidgetState createState() => _DashboardWidgetState(); +} + +class _DashboardWidgetState extends ThemedMapControllingWidgetState + with SingleTickerProviderStateMixin { + late StreamSubscription _routePositionSubscription; + + late final DraggableScrollableController _sheetController; + final ValueNotifier _headerScaleSize = ValueNotifier(0); + + final _dateFormat = DateFormat("HH:mm"); + + double _maxExtent = .36; + int _dashboardSize = 0; + double _headerSize = 60; + final double _minExtent = .1; + + sdk.Map? map; + + final ValueNotifier _dashboardModel = ValueNotifier( + DashboardModel( + distance: 0, + duration: Duration.zero, + soundsEnabled: true, + ), + ); + + @override + void initState() { + _sheetController = DraggableScrollableController(); + _sheetController.addListener(() { + _headerScaleSize.value = (_sheetController.size - _minExtent) * (1 / (_maxExtent - _minExtent)); + }); + + super.initState(); + } + + @override + void onAttachedToMap(sdk.Map map) { + _routePositionSubscription = widget.navigationManager.uiModel.routePositionChannel.listen((position) { + final duration = widget.navigationManager.uiModel.duration(); + final distance = widget.navigationManager.uiModel.distance(); + + _dashboardModel.value = _dashboardModel.value.copyWith( + distance: distance, + duration: duration, + ); + }); + this.map = map; + } + + @override + void onDetachedFromMap() { + _routePositionSubscription.cancel(); + map = null; + } + + bool _soundsEnabled() { + final categories = widget.navigationManager.soundNotificationSettings.enabledSoundCategories; + + return categories.contains(sdk.SoundCategory.instructions); + } + + void _toggleSounds() { + final categories = widget.navigationManager.soundNotificationSettings.enabledSoundCategories; + print(categories); + + if (_soundsEnabled()) { + categories.remove(sdk.SoundCategory.instructions); + } else { + categories.add(sdk.SoundCategory.instructions); + } + widget.navigationManager.soundNotificationSettings.enabledSoundCategories = categories; + + _dashboardModel.value = _dashboardModel.value.copyWith( + soundsEnabled: _soundsEnabled(), + ); + } + + Future _showRoute() async { + if (map == null) { + return; + } + + final geometries = widget.navigationManager.uiModel.route.route.geometry.entries.map((entry) { + return sdk.PointGeometry(entry.value); + }).toList(); + final geometry = sdk.ComplexGeometry(geometries); + + final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; + + final cameraPosition = sdk.calcPositionForGeometry( + map!.camera, + geometry, + null, + sdk.Padding( + top: (32 * devicePixelRatio).round(), + bottom: ((_dashboardSize + 32) * devicePixelRatio).round(), + left: (32 * devicePixelRatio).round(), + right: (32 * devicePixelRatio).round(), + ), + null, + null, + null, + ); + + await map!.camera.moveToCameraPosition(cameraPosition).value; + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return DraggableScrollableSheet( + controller: _sheetController, + snap: true, + initialChildSize: _minExtent, + minChildSize: _minExtent, + maxChildSize: _maxExtent, + snapSizes: [_minExtent, _maxExtent], + builder: (context, scrollController) { + return ScrollConfiguration( + behavior: NoOverscrollBehavior(), + child: ListView( + padding: EdgeInsets.zero, + physics: const ClampingScrollPhysics(), + controller: scrollController, + shrinkWrap: true, + children: [ + MeasureSize( + onChange: (size) { + setState(() { + _headerSize = size.height; + }); + }, + child: Center( + child: ValueListenableBuilder( + valueListenable: _headerScaleSize, + builder: (context, headerScaleSize, child) { + return Container( + width: min( + MediaQuery.of(context).size.width, + MediaQuery.of(context).size.width * (.8 + headerScaleSize), + ), + decoration: BoxDecoration( + color: theme.surfaceColor, + boxShadow: theme.shadows, + borderRadius: BorderRadius.vertical( + top: Radius.circular(theme.borderRadius), + bottom: Radius.circular(theme.borderRadius * (1 - headerScaleSize)), + ), + ), + padding: EdgeInsets.symmetric( + vertical: 8, + horizontal: 10, + ), + child: ValueListenableBuilder( + valueListenable: _dashboardModel, + builder: (context, value, child) { + final distance = formatMeters(value.distance); + final arrivalTime = DateTime.now().add(value.duration); + + return Row( + children: [ + SizedBox( + width: theme.buttonSize, + height: theme.buttonSize, + ), + Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + formatDuration(value.duration), + style: theme.durationTextStyle, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${distance.value}${distance.unit}", + style: theme.distanceArrivalTextStyle, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + width: 4, + height: 4, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: theme.distanceArrivalTextStyle.color!, + ), + ), + ), + Text( + _dateFormat.format(arrivalTime), + style: theme.distanceArrivalTextStyle, + ), + ], + ) + ], + ), + Spacer(), + GestureDetector( + onTap: () { + if (_sheetController.size >= _maxExtent) { + _sheetController.animateTo( + _minExtent, + duration: Durations.short3, + curve: Curves.linear, + ); + } else { + _sheetController.animateTo( + _maxExtent, + duration: Durations.short3, + curve: Curves.linear, + ); + } + }, + child: Container( + width: theme.buttonSize, + height: theme.buttonSize, + decoration: BoxDecoration( + color: theme.buttonSurfaceColor, + borderRadius: BorderRadius.circular(theme.buttonBorderRadius), + ), + child: SvgPicture.asset( + 'packages/$pluginName/assets/icons/dgis_menu.svg', + fit: BoxFit.none, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + theme.iconColor, + BlendMode.srcIn, + ), + ), + ), + ) + ], + ); + }, + ), + ); + }, + ), + ), + ), + MeasureSize( + onChange: (size) { + setState(() { + _dashboardSize = size.height.round(); + _maxExtent = (size.height + _headerSize) / constraints.maxHeight; + }); + }, + child: ValueListenableBuilder( + valueListenable: _headerScaleSize, + builder: (context, headerScaleSize, child) { + return Opacity( + opacity: headerScaleSize, + child: Container( + color: theme.surfaceColor, + child: Column( + children: [ + Divider( + color: theme.buttonSurfaceColor, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ValueListenableBuilder( + valueListenable: _dashboardModel, + builder: (context, value, child) { + return GestureDetector( + onTap: _toggleSounds, + child: Container( + decoration: BoxDecoration( + color: theme.buttonSurfaceColor, + borderRadius: BorderRadius.circular( + theme.buttonBorderRadius, + ), + ), + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Настройки звука', + style: theme.menuButtonTextStyle, + ), + Text( + value.soundsEnabled ? 'Маневры включены' : 'Маневры выключены', + style: theme.menuButtonSubTextStyle, + ), + ], + ), + const Spacer(), + Container( + decoration: BoxDecoration( + color: value.soundsEnabled + ? theme.buttonPositiveSurfaceColor + : theme.buttonNegativeSurfaceColor, + shape: BoxShape.circle, + ), + padding: EdgeInsets.all(6), + child: SvgPicture.asset( + 'packages/$pluginName/assets/icons/dgis_sound.svg', + fit: BoxFit.none, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Color(0xffffffff), + BlendMode.srcIn, + ), + ), + ) + ], + ), + ), + ); + }), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: GestureDetector( + onTap: _showRoute, + child: Container( + decoration: BoxDecoration( + color: theme.buttonSurfaceColor, + borderRadius: BorderRadius.circular(theme.buttonBorderRadius), + ), + padding: const EdgeInsets.all(12), + child: Row( + children: [ + SvgPicture.asset( + 'packages/$pluginName/assets/icons/dgis_route.svg', + fit: BoxFit.none, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + theme.iconColor, + BlendMode.srcIn, + ), + ), + SizedBox( + width: 12, + ), + Text( + 'Просмотр Маршрута', + style: theme.menuButtonTextStyle, + ), + Spacer(), + SvgPicture.asset( + 'packages/$pluginName/assets/icons/dgis_chevron.svg', + fit: BoxFit.none, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + theme.iconColor, + BlendMode.srcIn, + ), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + onTap: widget.navigationManager.stop, + child: Container( + decoration: BoxDecoration( + color: theme.buttonNegativeSurfaceColor, + borderRadius: BorderRadius.circular(theme.buttonBorderRadius), + ), + padding: const EdgeInsets.all(14.0), + child: Center( + child: Text( + 'Завершить поездку', + style: theme.finishButtonTextStyle, + ), + ), + ), + ), + ), + const SizedBox( + height: 40, + ) + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + }, + ); + }, + ); + } +} + +class DashboardModel { + final int distance; + final Duration duration; + final bool soundsEnabled; + + DashboardModel({ + required this.distance, + required this.duration, + required this.soundsEnabled, + }); + + DashboardModel copyWith({ + int? distance, + Duration? duration, + bool? soundsEnabled, + }) { + return DashboardModel( + distance: distance ?? this.distance, + duration: duration ?? this.duration, + soundsEnabled: soundsEnabled ?? this.soundsEnabled, + ); + } +} + +class DashboardWidgetTheme extends MapWidgetTheme { + final TextStyle durationTextStyle; + final TextStyle distanceArrivalTextStyle; + + final Color surfaceColor; + final List shadows; + final double borderRadius; + + final Color buttonSurfaceColor; + final Color buttonNegativeSurfaceColor; + final Color buttonPositiveSurfaceColor; + final double buttonBorderRadius; + final double buttonSize; + final Color iconColor; + final double iconSize; + + final TextStyle menuButtonTextStyle; + final TextStyle menuButtonSubTextStyle; + final TextStyle finishButtonTextStyle; + + const DashboardWidgetTheme({ + required this.shadows, + required this.surfaceColor, + required this.borderRadius, + required this.durationTextStyle, + required this.distanceArrivalTextStyle, + required this.buttonSurfaceColor, + required this.buttonNegativeSurfaceColor, + required this.buttonPositiveSurfaceColor, + required this.buttonBorderRadius, + required this.buttonSize, + required this.iconColor, + required this.iconSize, + required this.menuButtonTextStyle, + required this.menuButtonSubTextStyle, + required this.finishButtonTextStyle, + }); + + /// Цветовая схема виджета для светлого режима по умолчанию. + static const defaultLight = DashboardWidgetTheme( + borderRadius: 16, + surfaceColor: Color(0xffffffff), + shadows: [ + BoxShadow( + color: Color(0x12000000), + blurRadius: 1, + ), + BoxShadow( + color: Color(0x0D000000), + offset: Offset(0, 2), + blurRadius: 4, + ), + ], + durationTextStyle: TextStyle( + color: Color(0xff141414), + fontWeight: FontWeight.w600, + fontSize: 18, + height: 1.22, + ), + distanceArrivalTextStyle: TextStyle( + color: Color(0xff141414), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 1.25, + ), + buttonSurfaceColor: Color(0x0F141414), + buttonNegativeSurfaceColor: Color(0xFFE81C21), + buttonPositiveSurfaceColor: Color(0xFF1DB93C), + buttonBorderRadius: 8, + buttonSize: 36, + iconColor: Color(0xFF3C3C3C), + iconSize: 24, + menuButtonTextStyle: TextStyle( + color: Color(0xff141414), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 1.25, + ), + menuButtonSubTextStyle: TextStyle( + color: Color(0xff898989), + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.3, + ), + finishButtonTextStyle: TextStyle( + color: Color(0xffffffff), + fontWeight: FontWeight.w600, + fontSize: 16, + height: 1.25, + ), + ); + + /// Цветовая схема виджета для темного режима по умолчанию. + static const defaultDark = DashboardWidgetTheme( + borderRadius: 16, + surfaceColor: Color(0xff121212), + shadows: [ + BoxShadow( + color: Color(0x14000000), + offset: Offset(0, 1), + blurRadius: 4, + ), + BoxShadow( + color: Color(0x0A000000), + spreadRadius: 0.5, + ), + ], + durationTextStyle: TextStyle( + color: Color(0xFFFFFFFF), + fontWeight: FontWeight.w600, + fontSize: 18, + height: 1.22, + ), + distanceArrivalTextStyle: TextStyle( + color: Color(0xFFFFFFFF), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 1.25, + ), + buttonSurfaceColor: Color(0x0FFFFFFF), + buttonNegativeSurfaceColor: Color(0xFFE81C21), + buttonPositiveSurfaceColor: Color(0xFF1DB93C), + buttonBorderRadius: 8, + buttonSize: 36, + iconColor: Color(0xFFB8B8B8), + iconSize: 24, + menuButtonTextStyle: TextStyle( + color: Color(0xffffffff), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 1.25, + ), + menuButtonSubTextStyle: TextStyle( + color: Color(0xff898989), + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.3, + ), + finishButtonTextStyle: TextStyle( + color: Color(0xffffffff), + fontWeight: FontWeight.w600, + fontSize: 16, + height: 1.25, + ), + ); + + @override + DashboardWidgetTheme copyWith({ + double? borderRadius, + Color? surfaceColor, + List? shadows, + TextStyle? durationTextStyle, + TextStyle? distanceArrivalTextStyle, + Color? buttonSurfaceColor, + Color? buttonNegativeSurfaceColor, + Color? buttonPositiveSurfaceColor, + double? buttonBorderRadius, + double? buttonSize, + Color? iconColor, + double? iconSize, + TextStyle? menuButtonTextStyle, + TextStyle? menuButtonSubTextStyle, + TextStyle? finishButtonTextStyle, + }) { + return DashboardWidgetTheme( + borderRadius: borderRadius ?? this.borderRadius, + surfaceColor: surfaceColor ?? this.surfaceColor, + shadows: shadows ?? this.shadows, + durationTextStyle: durationTextStyle ?? this.durationTextStyle, + distanceArrivalTextStyle: distanceArrivalTextStyle ?? this.distanceArrivalTextStyle, + buttonSurfaceColor: buttonSurfaceColor ?? this.buttonSurfaceColor, + buttonNegativeSurfaceColor: buttonNegativeSurfaceColor ?? this.buttonNegativeSurfaceColor, + buttonPositiveSurfaceColor: buttonPositiveSurfaceColor ?? this.buttonPositiveSurfaceColor, + buttonBorderRadius: buttonBorderRadius ?? this.buttonBorderRadius, + buttonSize: buttonSize ?? this.buttonSize, + iconColor: iconColor ?? this.iconColor, + iconSize: iconSize ?? this.iconSize, + menuButtonTextStyle: menuButtonTextStyle ?? this.menuButtonTextStyle, + menuButtonSubTextStyle: menuButtonSubTextStyle ?? this.menuButtonSubTextStyle, + finishButtonTextStyle: finishButtonTextStyle ?? this.finishButtonTextStyle, + ); + } +} + +/// TODO: Удалить после фикса SDK +extension ModelDistanceDuration on sdk.Model { + int? distance() { + final routeDistance = this.route.route.geometry.length.millimeters; + final currentDistance = this.routePosition?.distance.millimeters; + + if (currentDistance == null) { + return null; + } + + return routeDistance - currentDistance; + } + + Duration? duration() { + final routePosition = this.routePosition; + final endPosition = this.route.route.geometry.last?.point; + + if (routePosition == null || endPosition == null) { + return null; + } + + final duration = this.dynamicRouteInfo.traffic.durations.calculateDuration(routePosition, endPosition); + + return duration; + } +} diff --git a/lib/src/widgets/navigator/maneuver_widget.dart b/lib/src/widgets/navigator/maneuver_widget.dart new file mode 100644 index 0000000..187642c --- /dev/null +++ b/lib/src/widgets/navigator/maneuver_widget.dart @@ -0,0 +1,333 @@ +import 'dart:async'; + +import 'package:dgis_mobile_sdk_full/src/util/fromat_distance.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../generated/dart_bindings.dart' as sdk; +import '../../generated/optional.dart'; +import '../../util/plugin_name.dart'; +import '../map/base_map_control.dart'; +import '../map/map_widget_theme.dart'; +import '../map/themed_map_controlling_widget.dart'; + +class ManeuverWidget extends ThemedMapControllingWidget { + final sdk.NavigationManager navigationManager; + + const ManeuverWidget({ + super.key, + required this.navigationManager, + ManeuverWidgetTheme? light, + ManeuverWidgetTheme? dark, + }) : super( + light: light ?? ManeuverWidgetTheme.defaultLight, + dark: dark ?? ManeuverWidgetTheme.defaultDark, + ); + + @override + ThemedMapControllingWidgetState createState() => _ManeuverWidgetState(); +} + +class _ManeuverWidgetState extends ThemedMapControllingWidgetState { + late StreamSubscription _routeSubscription; + late StreamSubscription _routePositionSubscription; + late StreamSubscription _stateSubscription; + + sdk.InstructionRouteAttribute? _instructions; + + ValueNotifier _maneuverModel = ValueNotifier( + ManeuverModel( + instruction: null, + routePoint: null, + ), + ); + + @override + void onAttachedToMap(sdk.Map map) { + _routeSubscription = widget.navigationManager.uiModel.routeChannel.listen((route) { + _instructions = route.route.instructions; + + if (this._instructions != null && widget.navigationManager.uiModel.routePosition != null) { + _nextManeuverInfo(widget.navigationManager.uiModel.routePosition!, this._instructions!); + } + }); + _routePositionSubscription = widget.navigationManager.uiModel.routePositionChannel.listen((position) { + if (this._instructions != null && position != null) { + _nextManeuverInfo(position, this._instructions!); + } + }); + _stateSubscription = widget.navigationManager.uiModel.stateChannel.listen((state) { + if (this._instructions != null && widget.navigationManager.uiModel.routePosition != null) { + _nextManeuverInfo(widget.navigationManager.uiModel.routePosition!, this._instructions!); + } + }); + } + + void _nextManeuverInfo(sdk.RoutePoint position, sdk.InstructionRouteAttribute instructions) { + final nearBackward = instructions.findNearBackward(position); + if (nearBackward != null && + position.distance.millimeters <= + nearBackward.point.distance.millimeters + nearBackward.value.range.millimeters) { + _maneuverModel.value = _maneuverModel.value.copyWith( + instruction: Optional(nearBackward), + routePoint: Optional(position), + ); + return; + } + + final nearForward = instructions.findNearForward(position); + if (nearForward != null) { + _maneuverModel.value = _maneuverModel.value.copyWith( + instruction: Optional(nearForward), + routePoint: Optional(position), + ); + } + } + + @override + void onDetachedFromMap() { + _routeSubscription.cancel(); + _routePositionSubscription.cancel(); + _stateSubscription.cancel(); + } + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: BoxConstraints( + maxWidth: theme.maxWidth, + minWidth: theme.minWidth, + ), + child: Container( + decoration: BoxDecoration( + color: theme.controlTheme.surfaceColor, + boxShadow: theme.controlTheme.shadows, + borderRadius: BorderRadius.circular(theme.controlTheme.borderRadius), + ), + padding: EdgeInsets.all(8), + child: ValueListenableBuilder( + valueListenable: _maneuverModel, + builder: (context, vaue, child) { + final maneuverDistance = vaue.maneuverDistance(); + final maneuverIcon = vaue.maneuverIcon(); + final roadName = vaue.instruction?.value.roadName; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + maneuverIcon != null + ? SvgPicture.asset( + maneuverIcon, + fit: BoxFit.none, + width: theme.iconSize, + height: theme.iconSize, + ) + : SizedBox( + width: theme.iconSize, + height: theme.iconSize, + ), + if (maneuverDistance != null) + RichText( + text: TextSpan( + children: [ + TextSpan( + text: formatMeters(maneuverDistance).value, + style: theme.maneuverDistanceTextStyle, + ), + TextSpan( + text: formatMeters(maneuverDistance).unit, + style: theme.maneuverDistanceUnitTextStyle, + ), + ], + ), + ), + ], + ), + if (roadName != null && roadName.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Text( + roadName, + style: theme.roadNameTextStyle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ) + ], + ); + }, + ), + ), + ); + } +} + +class ManeuverModel { + final sdk.InstructionRouteEntry? instruction; + final sdk.RoutePoint? routePoint; + + ManeuverModel({ + required this.instruction, + required this.routePoint, + }); + + int? maneuverDistance() { + if (instruction == null || routePoint == null) { + return null; + } + + return instruction!.point.distance.millimeters - routePoint!.distance.millimeters; + } + + String? maneuverIcon() { + if (instruction == null) { + return null; + } + + final maneuver = sdk.getInstructionManeuver(instruction!.value.extraInstructionInfo); + + return switch (maneuver) { + sdk.InstructionManeuver.none => null, + sdk.InstructionManeuver.start => 'packages/$pluginName/assets/icons/maneuvers/dgis_start.svg', + sdk.InstructionManeuver.finish => 'packages/$pluginName/assets/icons/maneuvers/dgis_finish.svg', + sdk.InstructionManeuver.crossroadStraight => 'packages/$pluginName/assets/icons/maneuvers/dgis_start.svg', + sdk.InstructionManeuver.crossroadSlightlyLeft => + 'packages/$pluginName/assets/icons/maneuvers/dgis_crossroad_slightly_left.svg', + sdk.InstructionManeuver.crossroadLeft => 'packages/$pluginName/assets/icons/maneuvers/dgis_crossroad_left.svg', + sdk.InstructionManeuver.crossroadSharplyLeft => + 'packages/$pluginName/assets/icons/maneuvers/dgis_crossroad_sharply_left.svg', + sdk.InstructionManeuver.crossroadSlightlyRight => + 'packages/$pluginName/assets/icons/maneuvers/dgis_crossroad_slightly_right.svg', + sdk.InstructionManeuver.crossroadRight => 'packages/$pluginName/assets/icons/maneuvers/dgis_crossroad_right.svg', + sdk.InstructionManeuver.crossroadSharplyRight => + 'packages/$pluginName/assets/icons/maneuvers/dgis_crossroad_sharply_right.svg', + sdk.InstructionManeuver.crossroadKeepLeft => 'packages/$pluginName/assets/icons/maneuvers/dgis_left.svg', + sdk.InstructionManeuver.crossroadKeepRight => 'packages/$pluginName/assets/icons/maneuvers/dgis_right.svg', + sdk.InstructionManeuver.crossroadUTurn => 'packages/$pluginName/assets/icons/maneuvers/dgis_crossroad_uturn.svg', + sdk.InstructionManeuver.roundaboutForward => + 'packages/$pluginName/assets/icons/maneuvers/dgis_ringroad_forward.svg', + sdk.InstructionManeuver.roundaboutLeft45 => + 'packages/$pluginName/assets/icons/maneuvers/dgis_ringroad_left_45.svg', + sdk.InstructionManeuver.roundaboutLeft90 => + 'packages/$pluginName/assets/icons/maneuvers/dgis_ringroad_left_90.svg', + sdk.InstructionManeuver.roundaboutLeft135 => + 'packages/$pluginName/assets/icons/maneuvers/dgis_ringroad_left_135.svg', + sdk.InstructionManeuver.roundaboutRight45 => + 'packages/$pluginName/assets/icons/maneuvers/dgis_ringroad_right_45.svg', + sdk.InstructionManeuver.roundaboutRight90 => + 'packages/$pluginName/assets/icons/maneuvers/dgis_ringroad_right_90.svg', + sdk.InstructionManeuver.roundaboutRight135 => + 'packages/$pluginName/assets/icons/maneuvers/dgis_ringroad_right_135.svg', + sdk.InstructionManeuver.roundaboutBackward => + 'packages/$pluginName/assets/icons/maneuvers/dgis_ringroad_backward.svg', + sdk.InstructionManeuver.roundaboutExit => 'packages/$pluginName/assets/icons/maneuvers/dgis_ringroad_exit.svg', + sdk.InstructionManeuver.uTurn => 'packages/$pluginName/assets/icons/maneuvers/dgis_crossroad_uturn.svg', + sdk.InstructionManeuver.roadCrossing => null, + }; + } + + ManeuverModel copyWith({ + Optional? instruction, + Optional? routePoint, + }) { + return ManeuverModel( + instruction: instruction != null ? instruction.value : this.instruction, + routePoint: routePoint != null ? routePoint.value : this.routePoint, + ); + } +} + +class ManeuverWidgetTheme extends MapWidgetTheme { + final MapControlTheme controlTheme; + final TextStyle roadNameTextStyle; + final TextStyle maneuverDistanceTextStyle; + final TextStyle maneuverDistanceUnitTextStyle; + final double iconSize; + final double maxWidth; + final double minWidth; + + const ManeuverWidgetTheme({ + required this.controlTheme, + required this.roadNameTextStyle, + required this.maneuverDistanceTextStyle, + required this.maneuverDistanceUnitTextStyle, + required this.iconSize, + required this.maxWidth, + required this.minWidth, + }); + + /// Цветовая схема UI–элемента для светлого режима по умолчанию. + static const ManeuverWidgetTheme defaultLight = ManeuverWidgetTheme( + controlTheme: MapControlTheme.defaultLight, + roadNameTextStyle: TextStyle( + height: 1.25, + color: Color(0xff141414), + fontWeight: FontWeight.w500, + fontSize: 16, + ), + maneuverDistanceTextStyle: TextStyle( + height: 1.14, + color: Color(0xff141414), + fontWeight: FontWeight.w600, + fontSize: 28, + ), + maneuverDistanceUnitTextStyle: TextStyle( + height: 1.2, + color: Color(0xff141414), + fontWeight: FontWeight.w500, + fontSize: 20, + ), + iconSize: 36, + maxWidth: 189, + minWidth: 140, + ); + + /// Цветовая схема UI–элемента для темного режима по умолчанию. + static const ManeuverWidgetTheme defaultDark = ManeuverWidgetTheme( + controlTheme: MapControlTheme.defaultDark, + roadNameTextStyle: TextStyle( + height: 1.25, + color: Color(0xffffffff), + fontWeight: FontWeight.w500, + fontSize: 16, + ), + maneuverDistanceTextStyle: TextStyle( + height: 1.14, + color: Color(0xffffffff), + fontWeight: FontWeight.w500, + fontSize: 28, + ), + maneuverDistanceUnitTextStyle: TextStyle( + height: 1.2, + color: Color(0xffffffff), + fontWeight: FontWeight.w500, + fontSize: 20, + ), + iconSize: 36, + maxWidth: 189, + minWidth: 140, + ); + + @override + ManeuverWidgetTheme copyWith({ + MapControlTheme? controlTheme, + TextStyle? roadNameTextStyle, + TextStyle? maneuverDistanceTextStyle, + TextStyle? maneuverDistanceUnitTextStyle, + double? maxWidth, + double? minWidth, + double? iconSize, + }) { + return ManeuverWidgetTheme( + controlTheme: controlTheme ?? this.controlTheme, + roadNameTextStyle: roadNameTextStyle ?? this.roadNameTextStyle, + maneuverDistanceTextStyle: maneuverDistanceTextStyle ?? this.maneuverDistanceTextStyle, + maneuverDistanceUnitTextStyle: maneuverDistanceUnitTextStyle ?? this.maneuverDistanceUnitTextStyle, + maxWidth: maxWidth ?? this.maxWidth, + minWidth: minWidth ?? this.minWidth, + iconSize: iconSize ?? this.iconSize, + ); + } +} diff --git a/lib/src/widgets/navigator/speed_limit_widget.dart b/lib/src/widgets/navigator/speed_limit_widget.dart new file mode 100644 index 0000000..51cb56c --- /dev/null +++ b/lib/src/widgets/navigator/speed_limit_widget.dart @@ -0,0 +1,535 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../generated/dart_bindings.dart' as sdk; +import '../../generated/optional.dart'; +import '../../util/plugin_name.dart'; +import '../map/map_widget_theme.dart'; +import '../map/themed_map_controlling_widget.dart'; + +class SpeedLimitWidget extends ThemedMapControllingWidget { + final sdk.NavigationManager navigationManager; + + const SpeedLimitWidget({ + super.key, + required this.navigationManager, + SpeedLimitWidgetTheme? light, + SpeedLimitWidgetTheme? dark, + }) : super( + light: light ?? SpeedLimitWidgetTheme.defaultLight, + dark: dark ?? SpeedLimitWidgetTheme.defaultDark, + ); + + @override + ThemedMapControllingWidgetState createState() => _SpeedLimitWidgetState(); +} + +class _SpeedLimitWidgetState extends ThemedMapControllingWidgetState { + final ValueNotifier _speedLimitModel = ValueNotifier( + SpeedLimitModel( + location: null, + speedLimit: null, + exceeding: false, + cameraProgressInfo: null, + ), + ); + + late sdk.CameraNotifier _cameraNotifier; + sdk.FloatRouteLongAttribute? _speedLimits; + + late StreamSubscription _locationSubscription; + late StreamSubscription _routeSubscription; + late StreamSubscription _routePositionSubscription; + late StreamSubscription _exceedingMaxSpeedLimitSubscription; + late StreamSubscription _cameraProgressSubscription; + + @override + void onAttachedToMap(sdk.Map map) { + _locationSubscription = widget.navigationManager.uiModel.locationChannel.listen((location) { + _speedLimitModel.value = _speedLimitModel.value.copyWith( + location: Optional(location), + ); + }); + _routeSubscription = widget.navigationManager.uiModel.routeChannel.listen((route) { + _speedLimits = route.route.maxSpeedLimits; + }); + _routePositionSubscription = widget.navigationManager.uiModel.routePositionChannel.listen((position) { + if (position == null || _speedLimits == null) { + return; + } + + final entry = _speedLimits!.entry(position); + _speedLimitModel.value = _speedLimitModel.value.copyWith( + speedLimit: Optional( + entry?.value, + ), + ); + }); + _exceedingMaxSpeedLimitSubscription = + widget.navigationManager.uiModel.exceedingMaxSpeedLimitChannel.listen((exceeding) { + _speedLimitModel.value = _speedLimitModel.value.copyWith( + exceeding: exceeding, + ); + }); + _cameraNotifier = sdk.CameraNotifier(widget.navigationManager.uiModel); + _cameraProgressSubscription = _cameraNotifier.cameraProgressChannel.listen((cameraProgressInfo) { + _speedLimitModel.value = _speedLimitModel.value.copyWith( + cameraProgressInfo: Optional(cameraProgressInfo), + ); + }); + } + + @override + void onDetachedFromMap() { + _locationSubscription.cancel(); + _routeSubscription.cancel(); + _routePositionSubscription.cancel(); + _exceedingMaxSpeedLimitSubscription.cancel(); + _cameraProgressSubscription.cancel(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _speedLimitModel, + builder: (context, value, child) { + final cameraIcon = value.cameraIcon(); + + return Material( + color: Colors.transparent, + child: SizedBox( + width: theme.size, + height: theme.size, + child: Stack( + children: [ + /// speed + Positioned( + bottom: 0, + left: 0, + child: SizedBox( + width: theme.speedometerTheme.size, + height: theme.speedometerTheme.size, + child: OverflowBox( + alignment: Alignment.topCenter, + maxHeight: theme.speedometerTheme.size + theme.speedometerTheme.iconSize, + child: Stack( + children: [ + SizedBox( + width: theme.speedometerTheme.size, + height: theme.speedometerTheme.size, + child: DecoratedBox( + decoration: ShapeDecoration( + shape: CircleBorder( + side: value.cameraProgressInfo != null + ? BorderSide( + width: theme.cameraProgressTheme.thickness, + color: theme.cameraProgressTheme.progressColor, + strokeAlign: BorderSide.strokeAlignCenter, + ) + : BorderSide.none, + ), + color: theme.speedometerTheme.surfaceColor, + shadows: theme.speedometerTheme.shadows, + ), + child: Center( + child: Baseline( + baselineType: TextBaseline.alphabetic, + baseline: theme.speedometerTheme.textStyle.fontSize!, + child: Text( + '${(value.location?.groundSpeed?.value ?? 0).floor()}', + style: theme.speedometerTheme.textStyle, + ), + ), + ), + ), + ), + if (value.cameraProgressInfo != null && cameraIcon != null) + SizedBox( + width: theme.speedometerTheme.size, + height: theme.speedometerTheme.size, + child: CircularProgressIndicator( + color: value.exceeding + ? theme.cameraProgressTheme.progressExceededColor + : theme.cameraProgressTheme.progressColor, + value: value.cameraProgressInfo!.progress, + ), + ), + if (cameraIcon != null) + Align( + alignment: Alignment.bottomCenter, + child: SvgPicture.asset( + cameraIcon, + fit: BoxFit.none, + width: theme.speedometerTheme.iconSize, + height: theme.speedometerTheme.iconSize * 2, + ), + ), + ], + ), + ), + ), + ), + + Positioned( + top: 0, + right: 0, + child: SizedBox( + width: theme.speedLimitTheme.size, + height: theme.speedLimitTheme.size, + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: theme.speedLimitTheme.exceededSurfaceColor, + width: theme.speedLimitTheme.borderWidth, + ), + boxShadow: value.exceeding ? theme.speedLimitTheme.exceededShadows : null, + color: value.exceeding + ? theme.speedLimitTheme.exceededSurfaceColor + : theme.speedLimitTheme.surfaceColor, + ), + child: Center( + child: Text( + "${value.speedLimit != null ? (value.speedLimit! * 3.6).round() : '--'}", + style: theme.speedLimitTheme.textStyle, + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +class SpeedLimitModel { + final sdk.Location? location; + final double? speedLimit; + final bool exceeding; + final sdk.CameraProgressInfo? cameraProgressInfo; + + SpeedLimitModel({ + required this.location, + required this.speedLimit, + required this.exceeding, + required this.cameraProgressInfo, + }); + + SpeedLimitModel copyWith({ + Optional? location, + Optional? speedLimit, + Optional? cameraProgressInfo, + bool? exceeding, + }) { + return SpeedLimitModel( + location: location != null ? location.value : this.location, + speedLimit: speedLimit != null ? speedLimit.value : this.speedLimit, + exceeding: exceeding ?? this.exceeding, + cameraProgressInfo: cameraProgressInfo != null ? cameraProgressInfo.value : this.cameraProgressInfo, + ); + } + + String? cameraIcon() { + if (this.cameraProgressInfo == null) { + return null; + } + final purposes = this.cameraProgressInfo!.camera.purposes; + + if (purposes.contains(sdk.RouteCameraPurpose.noStoppingControl)) { + return 'packages/$pluginName/assets/icons/dgis_camera_stop.svg'; + } else if (purposes.contains(sdk.RouteCameraPurpose.speedControl) || + purposes.contains(sdk.RouteCameraPurpose.averageSpeedControl)) { + return switch (this.cameraProgressInfo!.camera.direction) { + sdk.RouteCameraDirection.against => 'packages/$pluginName/assets/icons/dgis_camera_back.svg', + sdk.RouteCameraDirection.along => 'packages/$pluginName/assets/icons/dgis_camera_front.svg', + sdk.RouteCameraDirection.both => 'packages/$pluginName/assets/icons/dgis_camera_both.svg', + }; + } + + return null; + } +} + +class CameraProgressTheme extends MapWidgetTheme { + final Color surfaceColor; + final Color progressColor; + final Color progressExceededColor; + final double thickness; + + const CameraProgressTheme({ + required this.surfaceColor, + required this.progressColor, + required this.progressExceededColor, + required this.thickness, + }); + + /// Цветовая схема виджета для светлого режима по умолчанию. + static const defaultLight = CameraProgressTheme( + surfaceColor: Color(0xFFB8B8B8), + progressColor: Color(0x80141414), + progressExceededColor: Color(0xFFE91C21), + thickness: 4, + ); + + /// Цветовая схема виджета для темного режима по умолчанию. + static const defaultDark = CameraProgressTheme( + surfaceColor: Color(0xff5A5A5A), + progressColor: Color(0xffC4C4C4), + progressExceededColor: Color(0xffE91C21), + thickness: 4, + ); + + @override + CameraProgressTheme copyWith({ + Color? surfaceColor, + Color? progressColor, + Color? progressExceededColor, + double? thickness, + }) { + return CameraProgressTheme( + surfaceColor: surfaceColor ?? this.surfaceColor, + progressColor: progressColor ?? this.progressColor, + progressExceededColor: progressExceededColor ?? this.progressExceededColor, + thickness: thickness ?? this.thickness, + ); + } +} + +class SpeedometerTheme extends MapWidgetTheme { + final double size; + final double iconSize; + + final Color surfaceColor; + final TextStyle textStyle; + final List shadows; + + const SpeedometerTheme({ + required this.size, + required this.iconSize, + required this.textStyle, + required this.shadows, + required this.surfaceColor, + }); + + /// Цветовая схема виджета для светлого режима по умолчанию. + static const defaultLight = SpeedometerTheme( + surfaceColor: Color(0xffffffff), + size: 64, + iconSize: 28, + textStyle: TextStyle( + height: 1.14, + color: Color(0xff141414), + fontWeight: FontWeight.w600, + fontSize: 28, + ), + shadows: [ + BoxShadow( + color: Color(0x12000000), + blurRadius: 1, + ), + BoxShadow( + color: Color(0x0D000000), + offset: Offset(0, 2), + blurRadius: 4, + ), + ], + ); + + /// Цветовая схема виджета для темного режима по умолчанию. + static const defaultDark = SpeedometerTheme( + surfaceColor: Color(0xff121212), + size: 64, + iconSize: 28, + textStyle: TextStyle( + height: 1.14, + color: Color(0xffffffff), + fontWeight: FontWeight.w600, + fontSize: 28, + ), + shadows: [ + BoxShadow( + color: Color(0x14000000), + offset: Offset(0, 1), + blurRadius: 4, + ), + BoxShadow( + color: Color(0x0A000000), + spreadRadius: 0.5, + ), + ], + ); + + @override + SpeedometerTheme copyWith({ + double? size, + double? iconSize, + Color? surfaceColor, + TextStyle? textStyle, + List? shadows, + }) { + return SpeedometerTheme( + surfaceColor: surfaceColor ?? this.surfaceColor, + size: size ?? this.size, + iconSize: iconSize ?? this.iconSize, + textStyle: textStyle ?? this.textStyle, + shadows: shadows ?? this.shadows, + ); + } +} + +class SpeedLimitTheme extends MapWidgetTheme { + final double size; + final TextStyle textStyle; + + final Color surfaceColor; + final Color exceededSurfaceColor; + final TextStyle exceededTextStyle; + final List exceededShadows; + + final double borderWidth; + + const SpeedLimitTheme({ + required this.size, + required this.borderWidth, + required this.surfaceColor, + required this.textStyle, + required this.exceededTextStyle, + required this.exceededSurfaceColor, + required this.exceededShadows, + }); + + /// Цветовая схема виджета для светлого режима по умолчанию. + static const defaultLight = SpeedLimitTheme( + size: 48, + borderWidth: 4, + surfaceColor: Color(0xffffffff), + textStyle: TextStyle( + height: 1.16, + color: Color(0xff141414), + fontWeight: FontWeight.w600, + fontSize: 24, + ), + exceededTextStyle: TextStyle( + height: 1.16, + color: Color(0xffffffff), + fontWeight: FontWeight.w600, + fontSize: 24, + ), + exceededSurfaceColor: Color(0xffE91C21), + exceededShadows: [ + BoxShadow( + color: Color(0xffE91C21), + blurRadius: 1, + ), + BoxShadow( + color: Color(0xffE91C21), + offset: Offset(0, 0), + blurRadius: 4, + ), + ], + ); + + /// Цветовая схема виджета для темного режима по умолчанию. + static const defaultDark = SpeedLimitTheme( + surfaceColor: Color(0xff121212), + exceededSurfaceColor: Color(0xffE91C21), + borderWidth: 4, + size: 48, + exceededTextStyle: TextStyle( + height: 1.16, + color: Color(0xffffffff), + fontWeight: FontWeight.w600, + fontSize: 24, + ), + textStyle: TextStyle( + height: 1.16, + color: Color(0xffffffff), + fontWeight: FontWeight.w600, + fontSize: 24, + ), + exceededShadows: [ + BoxShadow( + color: Color(0xffE91C21), + blurRadius: 1, + ), + BoxShadow( + color: Color(0xffE91C21), + offset: Offset(0, 0), + blurRadius: 4, + ), + ], + ); + + @override + SpeedLimitTheme copyWith({ + double? size, + double? borderWidth, + Color? surfaceColor, + TextStyle? textStyle, + TextStyle? exceededTextStyle, + Color? exceededSurfaceColor, + List? exceededShadows, + }) { + return SpeedLimitTheme( + size: size ?? this.size, + textStyle: textStyle ?? this.textStyle, + surfaceColor: surfaceColor ?? this.surfaceColor, + borderWidth: borderWidth ?? this.borderWidth, + exceededTextStyle: exceededTextStyle ?? this.exceededTextStyle, + exceededSurfaceColor: exceededSurfaceColor ?? this.exceededSurfaceColor, + exceededShadows: exceededShadows ?? this.exceededShadows, + ); + } +} + +class SpeedLimitWidgetTheme extends MapWidgetTheme { + final double size; + + final SpeedometerTheme speedometerTheme; + final SpeedLimitTheme speedLimitTheme; + final CameraProgressTheme cameraProgressTheme; + + const SpeedLimitWidgetTheme({ + required this.size, + required this.speedometerTheme, + required this.speedLimitTheme, + required this.cameraProgressTheme, + }); + + /// Цветовая схема виджета для светлого режима по умолчанию. + static const defaultLight = SpeedLimitWidgetTheme( + size: 94, + speedometerTheme: SpeedometerTheme.defaultLight, + speedLimitTheme: SpeedLimitTheme.defaultLight, + cameraProgressTheme: CameraProgressTheme.defaultLight, + ); + + /// Цветовая схема виджета для темного режима по умолчанию. + static const defaultDark = SpeedLimitWidgetTheme( + size: 94, + speedometerTheme: SpeedometerTheme.defaultDark, + speedLimitTheme: SpeedLimitTheme.defaultDark, + cameraProgressTheme: CameraProgressTheme.defaultDark, + ); + + @override + SpeedLimitWidgetTheme copyWith({ + double? widgetSize, + SpeedometerTheme? speedometerTheme, + SpeedLimitTheme? speedLimitTheme, + CameraProgressTheme? cameraProgressTheme, + }) { + return SpeedLimitWidgetTheme( + size: widgetSize ?? this.size, + speedometerTheme: speedometerTheme ?? this.speedometerTheme, + speedLimitTheme: speedLimitTheme ?? this.speedLimitTheme, + cameraProgressTheme: cameraProgressTheme ?? this.cameraProgressTheme, + ); + } +} diff --git a/lib/src/widgets/routing/route_card.dart b/lib/src/widgets/routing/route_card.dart new file mode 100644 index 0000000..9175bb3 --- /dev/null +++ b/lib/src/widgets/routing/route_card.dart @@ -0,0 +1,189 @@ +import 'package:dgis_mobile_sdk_full/src/util/format_duration.dart'; +import 'package:dgis_mobile_sdk_full/src/util/fromat_distance.dart'; +import 'package:flutter/cupertino.dart'; + +import '../../generated/dart_bindings.dart' as sdk; +import '../map/map_widget_theme.dart'; + +class RouteCard extends StatelessWidget { + const RouteCard({ + super.key, + required this.route, + required this.theme, + required this.onGoPressed, + }); + + final sdk.TrafficRoute route; + final RouteCardTheme theme; + + final ValueChanged onGoPressed; + + @override + Widget build(BuildContext context) { + final distance = formatMeters(route.distance() ?? 0); + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(theme.borderRadius), + color: theme.backgroundColor, + ), + padding: EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + formatDuration(route.duration() ?? Duration.zero), + style: theme.durationTextStyle, + ), + Text( + "${distance.value}${distance.unit}", + style: theme.distanceTextStyle, + ), + ], + ), + CupertinoButton( + minSize: 0, + padding: EdgeInsets.zero, + child: DefaultTextStyle.merge( + style: const TextStyle( + letterSpacing: 0, + ), + child: Container( + decoration: BoxDecoration( + color: theme.goButtonBackground, + borderRadius: BorderRadius.circular(theme.goButtonRadius), + ), + padding: EdgeInsets.symmetric( + vertical: 10, + horizontal: 14, + ), + child: Text( + 'В путь', + style: theme.goButtonTextStyle, + ), + ), + ), + onPressed: () => onGoPressed(route), + ) + ], + ), + ); + } +} + +class RouteCardTheme extends MapWidgetTheme { + const RouteCardTheme({ + required this.backgroundColor, + required this.borderRadius, + required this.durationTextStyle, + required this.distanceTextStyle, + required this.goButtonBackground, + required this.goButtonRadius, + required this.goButtonTextStyle, + }); + + final Color backgroundColor; + final double borderRadius; + + final TextStyle durationTextStyle; + final TextStyle distanceTextStyle; + + final Color goButtonBackground; + final double goButtonRadius; + final TextStyle goButtonTextStyle; + + static const defaultLight = RouteCardTheme( + backgroundColor: Color(0xFFFFFFFF), + borderRadius: 12, + durationTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + color: Color(0xFF141414), + fontSize: 19, + height: 1.2, + ), + distanceTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + color: Color(0xFF898989), + fontSize: 15, + height: 1.3, + ), + goButtonBackground: Color(0xFF1DB93C), + goButtonRadius: 8, + goButtonTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + color: Color(0xFFFFFFFF), + fontSize: 15, + height: 1.3, + ), + ); + static const defaultDark = RouteCardTheme( + backgroundColor: Color(0x0FFFFFFF), + borderRadius: 12, + durationTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + fontWeight: FontWeight.w500, + color: Color(0xFFFFFFFF), + fontSize: 19, + height: 1.2, + ), + distanceTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + fontWeight: FontWeight.w400, + color: Color(0xFF898989), + fontSize: 15, + height: 1.3, + ), + goButtonBackground: Color(0xFF1DB93C), + goButtonRadius: 8, + goButtonTextStyle: TextStyle( + fontWeight: FontWeight.w600, + leadingDistribution: TextLeadingDistribution.even, + color: Color(0xFFFFFFFF), + fontSize: 15, + height: 1.3, + ), + ); + + @override + RouteCardTheme copyWith({ + Color? backgroundColor, + double? borderRadius, + TextStyle? durationTextStyle, + TextStyle? distanceTextStyle, + Color? goButtonBackground, + double? goButtonRadius, + TextStyle? goButtonTextStyle, + }) { + return RouteCardTheme( + backgroundColor: backgroundColor ?? this.backgroundColor, + borderRadius: borderRadius ?? this.borderRadius, + durationTextStyle: durationTextStyle ?? this.durationTextStyle, + distanceTextStyle: distanceTextStyle ?? this.distanceTextStyle, + goButtonBackground: goButtonBackground ?? this.goButtonBackground, + goButtonRadius: goButtonRadius ?? this.goButtonRadius, + goButtonTextStyle: goButtonTextStyle ?? this.goButtonTextStyle, + ); + } +} + +extension RouteDistanceDuration on sdk.TrafficRoute { + int? distance() { + return route.geometry.length.millimeters; + } + + Duration? duration() { + final firstPoint = route.geometry.first; + final lastPoint = route.geometry.last; + + if (firstPoint == null || lastPoint == null) { + return null; + } + + final duration = traffic.durations.calculateDuration(firstPoint.point, lastPoint.point); + + return duration; + } +} diff --git a/lib/src/widgets/routing/routes_list_widget.dart b/lib/src/widgets/routing/routes_list_widget.dart new file mode 100644 index 0000000..3744a19 --- /dev/null +++ b/lib/src/widgets/routing/routes_list_widget.dart @@ -0,0 +1,441 @@ +import 'package:dgis_mobile_sdk_full/src/widgets/routing/route_card.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../generated/dart_bindings.dart' as sdk; +import '../../util/format_duration.dart'; +import '../../util/plugin_name.dart'; +import '../map/map_widget_theme.dart'; +import '../map/themed_map_controlling_widget.dart'; + +class RouteTypeTab { + final String icon; + final Duration duration; + + final sdk.RouteSearchOptions options; + + const RouteTypeTab({ + required this.icon, + required this.duration, + required this.options, + }); +} + +class RoutesListModel { + final List routes; + final List tabs; + + final String startLabel; + final String finishLabel; + + RoutesListModel({ + required this.routes, + required this.tabs, + required this.startLabel, + required this.finishLabel, + }); + + RoutesListModel copyWith({ + List? routes, + List? tabs, + String? startLabel, + String? finishLabel, + }) { + return RoutesListModel( + routes: routes ?? this.routes, + tabs: tabs ?? this.tabs, + startLabel: startLabel ?? this.startLabel, + finishLabel: finishLabel ?? this.finishLabel, + ); + } +} + +class RoutesListWidget extends ThemedMapControllingWidget { + final sdk.RouteSearchOptions selectedOptions; + + final RoutesListModel model; + final ValueChanged onTabChanged; + final VoidCallback onSwapPoints; + final Widget Function(sdk.TrafficRoute, RouteCardTheme) itemBuilder; + + const RoutesListWidget({ + super.key, + required this.model, + required this.selectedOptions, + required this.onTabChanged, + required this.itemBuilder, + required this.onSwapPoints, + RoutesListWidgetTheme? light, + RoutesListWidgetTheme? dark, + }) : super( + light: light ?? RoutesListWidgetTheme.defaultLight, + dark: dark ?? RoutesListWidgetTheme.defaultDark, + ); + + @override + ThemedMapControllingWidgetState createState() => _RoutesListWidgetState(); +} + +class _RoutesListWidgetState extends ThemedMapControllingWidgetState { + @override + Widget build(BuildContext context) { + return Container( + color: theme.backgroundColor, + child: SafeArea( + top: false, + child: Column( + children: [ + SizedBox( + height: 16, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 3), + child: Column( + children: [ + SizedBox( + height: 4, + ), + Padding( + padding: EdgeInsets.all(2), + child: SvgPicture.asset( + 'packages/$pluginName/assets/icons/dgis_bullet.svg', + colorFilter: ColorFilter.mode( + theme.startPointColor, + BlendMode.srcATop, + ), + ), + ), + Container( + width: 1, + height: 10, + decoration: BoxDecoration( + color: theme.routeLineColor, + borderRadius: BorderRadius.circular(99), + ), + ), + Padding( + padding: EdgeInsets.all(2), + child: SvgPicture.asset( + 'packages/$pluginName/assets/icons/dgis_bullet.svg', + colorFilter: ColorFilter.mode( + theme.finishPointColor, + BlendMode.srcATop, + ), + ), + ), + ], + ), + ), + SizedBox( + width: 8, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 5, bottom: 1), + child: Text( + widget.model.startLabel, + style: theme.startLabelTextStyle, + ), + ), + Padding( + padding: EdgeInsets.only(top: 5, bottom: 1), + child: Text( + widget.model.finishLabel, + style: theme.finishLabelTextStyle, + ), + ), + ], + ), + ), + CupertinoButton( + minSize: 0, + padding: EdgeInsets.zero, + onPressed: widget.onSwapPoints, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: theme.headerButtonBackground, + ), + padding: EdgeInsets.all(8), + child: SvgPicture.asset( + 'packages/$pluginName/assets/icons/dgis_swap_points.svg', + colorFilter: ColorFilter.mode( + theme.headerButtonColor, + BlendMode.srcATop, + ), + ), + ), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.all(16), + child: ListView.separated( + itemBuilder: (context, index) => widget.itemBuilder( + widget.model.routes[index], + theme.cardTheme, + ), + separatorBuilder: (context, index) => SizedBox( + height: 16, + ), + itemCount: widget.model.routes.length, + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric( + vertical: 8, + ), + child: SizedBox( + height: 36, + child: ListView( + padding: EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + children: widget.model.tabs + .map((tab) => _RouteTypeTab( + tab: tab, + theme: theme, + isActive: tab.options == widget.selectedOptions, + onTabChanged: widget.onTabChanged, + )) + .toList(), + ), + ), + ) + ], + ), + ), + ); + } + + @override + void onAttachedToMap(sdk.Map map) {} + + @override + void onDetachedFromMap() {} +} + +class _RouteTypeTab extends StatelessWidget { + const _RouteTypeTab({ + required this.tab, + required this.theme, + required this.onTabChanged, + required this.isActive, + }); + + final RouteTypeTab tab; + final bool isActive; + final ValueChanged onTabChanged; + final RoutesListWidgetTheme theme; + + @override + Widget build(BuildContext context) { + return CupertinoButton( + minSize: 0, + padding: EdgeInsets.zero, + child: DefaultTextStyle.merge( + style: const TextStyle( + letterSpacing: 0, + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: isActive ? theme.activeTabBackground : Colors.transparent, + ), + padding: EdgeInsets.symmetric( + vertical: 6, + horizontal: 10, + ), + child: Row( + children: [ + SvgPicture.asset( + tab.icon, + colorFilter: ColorFilter.mode( + isActive ? theme.activeTabIconColor : theme.inactiveTabIconColor, + BlendMode.srcATop, + ), + ), + if (tab.duration != Duration.zero) + SizedBox( + width: 6, + ), + Text( + formatDuration(tab.duration), + style: isActive ? theme.activeTabTextStyle : theme.inactiveTabTextStyle, + ) + ], + ), + ), + ), + onPressed: () => onTabChanged(tab.options), + ); + } +} + +class RoutesListWidgetTheme extends MapWidgetTheme { + const RoutesListWidgetTheme({ + required this.backgroundColor, + required this.cardTheme, + required this.inactiveTabIconColor, + required this.inactiveTabTextStyle, + required this.activeTabIconColor, + required this.activeTabBackground, + required this.activeTabTextStyle, + required this.headerButtonBackground, + required this.headerButtonColor, + required this.startLabelTextStyle, + required this.startPointColor, + required this.finishLabelTextStyle, + required this.finishPointColor, + required this.routeLineColor, + }); + + final Color backgroundColor; + final RouteCardTheme cardTheme; + + final Color inactiveTabIconColor; + final TextStyle inactiveTabTextStyle; + + final Color activeTabIconColor; + final Color activeTabBackground; + final TextStyle activeTabTextStyle; + + final Color headerButtonBackground; + final Color headerButtonColor; + + final TextStyle startLabelTextStyle; + final Color startPointColor; + + final TextStyle finishLabelTextStyle; + final Color finishPointColor; + + final Color routeLineColor; + + static const defaultLight = RoutesListWidgetTheme( + backgroundColor: Color(0xFFF1F1F1), + cardTheme: RouteCardTheme.defaultLight, + inactiveTabIconColor: Color(0xFF898989), + inactiveTabTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + fontWeight: FontWeight.w500, + color: Color(0xFF898989), + fontSize: 14, + height: 1.3, + ), + activeTabIconColor: Color(0xFF141414), + activeTabBackground: Color(0xFFFFFFFF), + activeTabTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + fontWeight: FontWeight.w500, + color: Color(0xFF141414), + fontSize: 14, + height: 1.3, + ), + headerButtonBackground: Color(0x0F141414), + headerButtonColor: Color(0xFF5A5A5A), + startLabelTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + fontWeight: FontWeight.w500, + color: Color(0xFF898989), + fontSize: 15, + height: 1.3, + ), + startPointColor: Color(0xFF1BA136), + finishLabelTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + fontWeight: FontWeight.w500, + color: Color(0xFF141414), + fontSize: 15, + height: 1.3, + ), + finishPointColor: Color(0xFF0059D6), + routeLineColor: Color(0xFF5A5A5A), + ); + static const defaultDark = RoutesListWidgetTheme( + backgroundColor: Color(0xFF141414), + cardTheme: RouteCardTheme.defaultDark, + inactiveTabIconColor: Color(0xFF898989), + inactiveTabTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + fontWeight: FontWeight.w500, + color: Color(0xFF898989), + fontSize: 14, + height: 1.3, + ), + activeTabIconColor: Color(0xFFFFFFFF), + activeTabBackground: Color(0x2BFFFFFF), + activeTabTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + fontWeight: FontWeight.w500, + color: Color(0xFFFFFFFF), + fontSize: 14, + height: 1.3, + ), + headerButtonBackground: Color(0x0FFFFFFF), + headerButtonColor: Color(0xFFB8B8B8), + startLabelTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + fontWeight: FontWeight.w500, + color: Color(0xFF898989), + fontSize: 15, + height: 1.3, + ), + startPointColor: Color(0xFF26C947), + finishLabelTextStyle: TextStyle( + leadingDistribution: TextLeadingDistribution.even, + fontWeight: FontWeight.w500, + color: Color(0xFFFFFFFF), + fontSize: 15, + height: 1.3, + ), + finishPointColor: Color(0xFF3588FD), + routeLineColor: Color(0xFFB8B8B8), + ); + + @override + RoutesListWidgetTheme copyWith({ + Color? backgroundColor, + RouteCardTheme? cardTheme, + Color? inactiveTabIconColor, + TextStyle? inactiveTabTextStyle, + Color? activeTabIconColor, + Color? activeTabBackground, + TextStyle? activeTabTextStyle, + Color? headerButtonBackground, + Color? headerButtonColor, + TextStyle? startLabelTextStyle, + Color? startPointColor, + TextStyle? finishLabelTextStyle, + Color? finishPointColor, + Color? routeLineColor, + }) { + return RoutesListWidgetTheme( + backgroundColor: backgroundColor ?? this.backgroundColor, + cardTheme: cardTheme ?? this.cardTheme, + inactiveTabIconColor: inactiveTabIconColor ?? this.inactiveTabIconColor, + inactiveTabTextStyle: inactiveTabTextStyle ?? this.inactiveTabTextStyle, + activeTabIconColor: activeTabIconColor ?? this.activeTabIconColor, + activeTabBackground: activeTabBackground ?? this.activeTabBackground, + activeTabTextStyle: activeTabTextStyle ?? this.activeTabTextStyle, + headerButtonBackground: headerButtonBackground ?? this.headerButtonBackground, + headerButtonColor: headerButtonColor ?? this.headerButtonColor, + startLabelTextStyle: startLabelTextStyle ?? this.startLabelTextStyle, + startPointColor: startPointColor ?? this.startPointColor, + finishLabelTextStyle: finishLabelTextStyle ?? this.finishLabelTextStyle, + finishPointColor: finishPointColor ?? this.finishPointColor, + routeLineColor: routeLineColor ?? this.routeLineColor, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index e7e8119..cf2f295 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,8 +5,8 @@ repository: https://github.com/2gis/dgis_mobile_sdk_full documentation: https://docs.2gis.com/en/flutter/sdk/overview environment: - sdk: '>=3.4.1 <4.0.0' - flutter: '>=3.22.0' + sdk: ">=3.4.1 <4.0.0" + flutter: ">=3.22.0" dependencies: async: ^2.11.0 @@ -14,6 +14,7 @@ dependencies: flutter: sdk: flutter flutter_svg: ^2.0.10+1 + intl: ^0.19.0 meta: ^1.12.0 url_launcher: ^6.3.0 @@ -24,6 +25,7 @@ dev_dependencies: flutter: assets: - assets/icons/ + - assets/icons/maneuvers/ plugin: platforms: android: