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: