From adc5c737b4213b321ac9aa1673f089d06021e998 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 15 Aug 2023 14:47:05 +0100 Subject: [PATCH 01/14] Reworked the way in which layers are added to the map --- example/lib/pages/custom_crs/custom_crs.dart | 29 ++-- example/lib/pages/epsg3413_crs.dart | 48 +++--- example/lib/pages/home.dart | 124 ++++++--------- example/lib/pages/map_inside_listview.dart | 16 +- example/lib/pages/plugin_scalebar.dart | 22 +-- example/lib/pages/plugin_zoombuttons.dart | 16 +- example/lib/pages/wms_tile_layer.dart | 20 ++- .../lib/pages/zoombuttons_plugin_option.dart | 5 +- lib/flutter_map.dart | 1 + lib/src/layer/attribution_layer/rich.dart | 53 +++---- lib/src/layer/attribution_layer/simple.dart | 73 ++++++--- lib/src/layer/general/overlay_layer.dart | 148 +++++++++++++++++ .../layer/general/translucent_pointer.dart | 130 +++++++++++++++ lib/src/layer/tile_layer/tile_layer.dart | 5 + lib/src/map/options.dart | 13 ++ lib/src/map/widget.dart | 149 +++++++++++++++--- 16 files changed, 617 insertions(+), 235 deletions(-) create mode 100644 lib/src/layer/general/overlay_layer.dart create mode 100644 lib/src/layer/general/translucent_pointer.dart diff --git a/example/lib/pages/custom_crs/custom_crs.dart b/example/lib/pages/custom_crs/custom_crs.dart index 1cb82806d..a05e011f4 100644 --- a/example/lib/pages/custom_crs/custom_crs.dart +++ b/example/lib/pages/custom_crs/custom_crs.dart @@ -140,7 +140,17 @@ class _CustomCrsPageState extends State { point = proj4.Point(x: p.latitude, y: p.longitude); }), ), - nonRotatedChildren: [ + children: [ + TileLayer( + wmsOptions: WMSTileLayerOptions( + crs: epsg3413CRS, + transparent: true, + format: 'image/jpeg', + baseUrl: + 'https://www.gebco.net/data_and_products/gebco_web_services/north_polar_view_wms/mapserv?', + layers: const ['gebco_north_polar_view'], + ), + ), RichAttributionWidget( popupInitialDisplayDuration: const Duration(seconds: 5), attributions: [ @@ -155,23 +165,6 @@ class _CustomCrsPageState extends State { ], ), ], - children: [ - Opacity( - opacity: 1, - child: TileLayer( - backgroundColor: Colors.transparent, - wmsOptions: WMSTileLayerOptions( - // Set the WMS layer's CRS - crs: epsg3413CRS, - transparent: true, - format: 'image/jpeg', - baseUrl: - 'https://www.gebco.net/data_and_products/gebco_web_services/north_polar_view_wms/mapserv?', - layers: const ['gebco_north_polar_view'], - ), - ), - ), - ], ), ), ], diff --git a/example/lib/pages/epsg3413_crs.dart b/example/lib/pages/epsg3413_crs.dart index c836e3f5a..5aad2f0a5 100644 --- a/example/lib/pages/epsg3413_crs.dart +++ b/example/lib/pages/epsg3413_crs.dart @@ -136,34 +136,15 @@ class _EPSG3413PageState extends State { initialZoom: 3, maxZoom: maxZoom, ), - nonRotatedChildren: [ - RichAttributionWidget( - popupInitialDisplayDuration: const Duration(seconds: 5), - attributions: [ - TextSourceAttribution( - 'Imagery reproduced from the GEBCO_2022 Grid, GEBCO Compilation Group (2022) GEBCO 2022 Grid (doi:10.5285/e0f0bb80-ab44-2739-e053-6c86abc0289c)', - onTap: () => launchUrl( - Uri.parse( - 'https://www.gebco.net/data_and_products/gebco_web_services/web_map_service/#polar', - ), - ), - ), - ], - ), - ], children: [ - Opacity( - opacity: 1, - child: TileLayer( - backgroundColor: Colors.transparent, - wmsOptions: WMSTileLayerOptions( - crs: epsg3413CRS, - transparent: true, - format: 'image/jpeg', - baseUrl: - 'https://www.gebco.net/data_and_products/gebco_web_services/north_polar_view_wms/mapserv?', - layers: const ['gebco_north_polar_view'], - ), + TileLayer( + wmsOptions: WMSTileLayerOptions( + crs: epsg3413CRS, + transparent: true, + format: 'image/jpeg', + baseUrl: + 'https://www.gebco.net/data_and_products/gebco_web_services/north_polar_view_wms/mapserv?', + layers: const ['gebco_north_polar_view'], ), ), OverlayImageLayer( @@ -180,6 +161,19 @@ class _EPSG3413PageState extends State { ], ), CircleLayer(circles: circles), + RichAttributionWidget( + popupInitialDisplayDuration: const Duration(seconds: 5), + attributions: [ + TextSourceAttribution( + 'Imagery reproduced from the GEBCO_2022 Grid, GEBCO Compilation Group (2022) GEBCO 2022 Grid (doi:10.5285/e0f0bb80-ab44-2739-e053-6c86abc0289c)', + onTap: () => launchUrl( + Uri.parse( + 'https://www.gebco.net/data_and_products/gebco_web_services/web_map_service/#polar', + ), + ), + ), + ], + ), ], ), ), diff --git a/example/lib/pages/home.dart b/example/lib/pages/home.dart index 6015db39a..df620d9f3 100644 --- a/example/lib/pages/home.dart +++ b/example/lib/pages/home.dart @@ -99,88 +99,66 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Home')), + //appBar: AppBar(title: const Text('Home')), drawer: buildDrawer(context, HomePage.route), - body: Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - const Padding( - padding: EdgeInsets.only(top: 8, bottom: 8), - child: Text('This is a map that is showing (51.5, -0.9).'), + body: Stack( + children: [ + FlutterMap.simple( + options: MapOptions( + initialCenter: const LatLng(51.5, -0.09), + initialZoom: 5, + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds( + const LatLng(-90, -180), + const LatLng(90, 180), + ), + ), ), - Flexible( - child: FlutterMap( - options: MapOptions( - initialCenter: const LatLng(51.5, -0.09), - initialZoom: 5, - cameraConstraint: CameraConstraint.contain( - bounds: LatLngBounds( - const LatLng(-90, -180), - const LatLng(90, 180), - ), + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + attribution: RichAttributionWidget( + popupInitialDisplayDuration: const Duration(seconds: 5), + animationConfig: const ScaleRAWA(), + showFlutterMapAttribution: false, + attributions: [ + TextSourceAttribution( + 'OpenStreetMap contributors', + onTap: () => launchUrl( + Uri.parse('https://openstreetmap.org/copyright'), ), ), - nonRotatedChildren: [ - RichAttributionWidget( - popupInitialDisplayDuration: const Duration(seconds: 5), - animationConfig: const ScaleRAWA(), - attributions: [ - TextSourceAttribution( - 'OpenStreetMap contributors', - onTap: () => launchUrl( - Uri.parse('https://openstreetmap.org/copyright'), - ), - ), - const TextSourceAttribution( - 'This attribution is the same throughout this app, except where otherwise specified', - prependCopyright: false, - ), - ], - ), - ], + const TextSourceAttribution( + 'This attribution is the same throughout this app, except where otherwise specified', + prependCopyright: false, + ), + ], + ), + ), + PositionedDirectional( + start: 16, + top: 16, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(999), + ), + padding: const EdgeInsets.all(8), + child: Row( children: [ - TileLayer( - urlTemplate: - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), - MarkerLayer( - markers: [ - Marker( - width: 80, - height: 80, - point: const LatLng(51.5, -0.09), - builder: (ctx) => const FlutterLogo( - textColor: Colors.blue, - key: ObjectKey(Colors.blue), - ), - ), - Marker( - width: 80, - height: 80, - point: const LatLng(53.3498, -6.2603), - builder: (ctx) => const FlutterLogo( - textColor: Colors.green, - key: ObjectKey(Colors.green), - ), - ), - Marker( - width: 80, - height: 80, - point: const LatLng(48.8566, 2.3522), - builder: (ctx) => const FlutterLogo( - textColor: Colors.purple, - key: ObjectKey(Colors.purple), - ), - ), - ], + Builder( + builder: (context) => IconButton( + onPressed: () => Scaffold.of(context).openDrawer(), + icon: const Icon(Icons.menu), + ), ), + const SizedBox(width: 8), + Image.asset('assets/ProjectIcon.png', height: 32, width: 32), + const SizedBox(width: 8), ], ), ), - ], - ), + ) + ], ), ); } diff --git a/example/lib/pages/map_inside_listview.dart b/example/lib/pages/map_inside_listview.dart index edc4df362..bbb7f2a74 100644 --- a/example/lib/pages/map_inside_listview.dart +++ b/example/lib/pages/map_inside_listview.dart @@ -26,21 +26,19 @@ class MapInsideListViewPage extends StatelessWidget { initialCenter: LatLng(51.5, -0.09), initialZoom: 5, ), - nonRotatedChildren: const [ - FlutterMapZoomButtons( - minZoom: 4, - maxZoom: 19, - mini: true, - padding: 10, - alignment: Alignment.bottomLeft, - ), - ], children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), + const FlutterMapZoomButtons( + minZoom: 4, + maxZoom: 19, + mini: true, + padding: 10, + alignment: Alignment.bottomLeft, + ), ], ), ), diff --git a/example/lib/pages/plugin_scalebar.dart b/example/lib/pages/plugin_scalebar.dart index f0fc28596..8f815de32 100644 --- a/example/lib/pages/plugin_scalebar.dart +++ b/example/lib/pages/plugin_scalebar.dart @@ -24,23 +24,23 @@ class PluginScaleBar extends StatelessWidget { initialCenter: LatLng(51.5, -0.09), initialZoom: 5, ), - nonRotatedChildren: [ - ScaleLayerWidget( - options: ScaleLayerPluginOption( - lineColor: Colors.blue, - lineWidth: 2, - textStyle: - const TextStyle(color: Colors.blue, fontSize: 12), - padding: const EdgeInsets.all(10), - ), - ), - ], children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), + OverlayLayer( + child: ScaleLayerWidget( + options: ScaleLayerPluginOption( + lineColor: Colors.blue, + lineWidth: 2, + textStyle: + const TextStyle(color: Colors.blue, fontSize: 12), + padding: const EdgeInsets.all(10), + ), + ), + ), ], ), ), diff --git a/example/lib/pages/plugin_zoombuttons.dart b/example/lib/pages/plugin_zoombuttons.dart index ca24acb3c..0bbc90836 100644 --- a/example/lib/pages/plugin_zoombuttons.dart +++ b/example/lib/pages/plugin_zoombuttons.dart @@ -24,8 +24,13 @@ class PluginZoomButtons extends StatelessWidget { initialCenter: LatLng(51.5, -0.09), initialZoom: 5, ), - nonRotatedChildren: const [ - FlutterMapZoomButtons( + children: [ + TileLayer( + urlTemplate: + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ), + const FlutterMapZoomButtons( minZoom: 4, maxZoom: 19, mini: true, @@ -33,13 +38,6 @@ class PluginZoomButtons extends StatelessWidget { alignment: Alignment.bottomRight, ), ], - children: [ - TileLayer( - urlTemplate: - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ) - ], ), ), ], diff --git a/example/lib/pages/wms_tile_layer.dart b/example/lib/pages/wms_tile_layer.dart index a6b0610da..f85ffd301 100644 --- a/example/lib/pages/wms_tile_layer.dart +++ b/example/lib/pages/wms_tile_layer.dart @@ -28,7 +28,15 @@ class WMSLayerPage extends StatelessWidget { initialCenter: LatLng(42.58, 12.43), initialZoom: 6, ), - nonRotatedChildren: [ + children: [ + TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://{s}.s2maps-tiles.eu/wms/?', + layers: const ['s2cloudless-2021_3857'], + ), + subdomains: const ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ), RichAttributionWidget( popupInitialDisplayDuration: const Duration(seconds: 5), attributions: [ @@ -50,16 +58,6 @@ class WMSLayerPage extends StatelessWidget { ], ), ], - children: [ - TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: 'https://{s}.s2maps-tiles.eu/wms/?', - layers: const ['s2cloudless-2021_3857'], - ), - subdomains: const ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), - ], ), ), ], diff --git a/example/lib/pages/zoombuttons_plugin_option.dart b/example/lib/pages/zoombuttons_plugin_option.dart index fff8d099f..9e438e479 100644 --- a/example/lib/pages/zoombuttons_plugin_option.dart +++ b/example/lib/pages/zoombuttons_plugin_option.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/plugin_api.dart'; -class FlutterMapZoomButtons extends StatelessWidget { +class FlutterMapZoomButtons extends StatelessWidget + with OverlayLayerStatelessMixin { final double minZoom; final double maxZoom; final bool mini; @@ -33,6 +34,8 @@ class FlutterMapZoomButtons extends StatelessWidget { @override Widget build(BuildContext context) { + super.build(context); + final map = MapCamera.of(context); return Align( alignment: alignment, diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index a37757612..fbedc6a23 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -10,6 +10,7 @@ export 'package:flutter_map/src/layer/attribution_layer/rich.dart'; export 'package:flutter_map/src/layer/attribution_layer/simple.dart'; export 'package:flutter_map/src/layer/attribution_layer/source.dart'; export 'package:flutter_map/src/layer/circle_layer.dart'; +export 'package:flutter_map/src/layer/general/translucent_pointer.dart'; export 'package:flutter_map/src/layer/marker_layer.dart'; export 'package:flutter_map/src/layer/overlay_image_layer.dart'; export 'package:flutter_map/src/layer/polygon_layer.dart'; diff --git a/lib/src/layer/attribution_layer/rich.dart b/lib/src/layer/attribution_layer/rich.dart index 11c0e0bec..0dd09e68b 100644 --- a/lib/src/layer/attribution_layer/rich.dart +++ b/lib/src/layer/attribution_layer/rich.dart @@ -26,6 +26,7 @@ enum AttributionAlignment { final Alignment real; } +/// {@template rich_attribution_widget} /// A prebuilt dynamic attribution layer that supports both logos and text /// through [SourceAttribution]s /// @@ -50,10 +51,22 @@ enum AttributionAlignment { /// property for more information. By default, a simple fade/opacity animation /// is provided by [FadeRAWA]. [ScaleRAWA] is also available. /// +/// This layer is an overlay layer, so [OverlayLayer] should not be used. +/// /// Read the documentation on the individual properties for more information and /// customizability. +/// +/// See also: +/// +/// * [SimpleAttributionWidget], which is a simple, classicly styled, text-only +/// attribution layer +/// {@endtemplate} @immutable -class RichAttributionWidget extends StatefulWidget { +class RichAttributionWidget extends StatefulWidget + with + AttributionWidget, + OverlayLayerStatefulMixin< + OverlayLayerStateMixin> { /// List of attributions to display /// /// [TextSourceAttribution]s are shown in a popup box (toggled by a tap/click @@ -106,32 +119,7 @@ class RichAttributionWidget extends StatefulWidget { /// [LogoSourceAttribution]. final Duration popupInitialDisplayDuration; - /// A prebuilt dynamic attribution layer that supports both logos and text - /// through [SourceAttribution]s - /// - /// [TextSourceAttribution]s are shown in a popup box that can be visible or - /// invisible. Its state is toggled by a tri-state [openButton]/[closeButton] : - /// 1. Not hovered, not opened: faded button, invisible box - /// 2. Hovered, not opened: full opacity button, invisible box - /// 3. Opened: full opacity button, visible box - /// - /// The hover state on mobile devices is unspecified, but the behaviour is - /// usually inconsequential on mobile devices anyway, due to the fingertip - /// covering the entire button. - /// - /// [LogoSourceAttribution]s are shown adjacent to the open/close button, to - /// comply with some stricter tile server requirements (such as Mapbox). These - /// are usually supplemented with a [TextSourceAttribution]. - /// - /// The popup box also closes automatically on any interaction with the map. - /// - /// Animations are built in by default, and configured/handled through - /// [RichAttributionWidgetAnimation] - see that class and the [animationConfig] - /// property for more information. By default, a simple fade/opacity animation - /// is provided by [FadeRAWA]. [ScaleRAWA] is also available. - /// - /// Read the documentation on the individual properties for more information - /// and customizability. + /// {@macro rich_attribution_widget} const RichAttributionWidget({ super.key, required this.attributions, @@ -147,10 +135,11 @@ class RichAttributionWidget extends StatefulWidget { }); @override - State createState() => RichAttributionWidgetState(); + OverlayLayerStateMixin createState() => _RichAttributionWidgetState(); } -class RichAttributionWidgetState extends State { +class _RichAttributionWidgetState extends State + with OverlayLayerStateMixin { StreamSubscription? mapEventSubscription; final persistentAttributionKey = GlobalKey(); @@ -167,9 +156,7 @@ class RichAttributionWidgetState extends State { Future.delayed( widget.popupInitialDisplayDuration, () { - if (mounted) { - setState(() => popupExpanded = false); - } + if (mounted) setState(() => popupExpanded = false); }, ); } @@ -199,6 +186,8 @@ class RichAttributionWidgetState extends State { @override Widget build(BuildContext context) { + super.build(context); + final persistentAttributionItems = [ ...List.from( widget.attributions.whereType(), diff --git a/lib/src/layer/attribution_layer/simple.dart b/lib/src/layer/attribution_layer/simple.dart index 67ddfff60..551e296f9 100644 --- a/lib/src/layer/attribution_layer/simple.dart +++ b/lib/src/layer/attribution_layer/simple.dart @@ -1,12 +1,28 @@ import 'package:flutter/material.dart'; +import 'package:flutter_map/src/layer/attribution_layer/rich.dart'; +import 'package:flutter_map/src/map/widget.dart'; -/// A simple, classic style, attribution widget, to be placed in -/// [FlutterMap.nonRotatedChildren] +/// Layer widget intended to attribute a source +/// +/// Applied to [RichAttributionWidget] & [SimpleAttributionWidget]. +/// +/// Has no effect, other than as a label to group the provided layers together. +mixin AttributionWidget on Widget {} + +/// A simple, classic style, attribution layer /// /// Displayed as a padded translucent [backgroundColor] box with the following /// text: 'flutter_map | © [source]', where [source] is wrapped with [onTap]. +/// +/// This layer is an overlay layer, so [OverlayLayer] should not be used. +/// +/// See also: +/// +/// * [RichAttributionWidget], which is dynamic, supports more customization, +/// and has a more complex appearance. @immutable -class SimpleAttributionWidget extends StatelessWidget { +class SimpleAttributionWidget extends StatelessWidget + with AttributionWidget, OverlayLayerStatelessMixin { /// Attribution text, such as 'OpenStreetMap contributors' final Text source; @@ -19,11 +35,12 @@ class SimpleAttributionWidget extends StatelessWidget { /// Anchor the widget in a position of the map final Alignment alignment; - /// A simple, classic style, attribution widget, to be placed in - /// [FlutterMap.nonRotatedChildren] + /// A simple, classic style, attribution widget /// /// Displayed as a padded translucent white box with the following text: /// 'flutter_map | © [source]'. + /// + /// This layer is an overlay layer, so [OverlayLayer] should not be used. const SimpleAttributionWidget({ super.key, required this.source, @@ -33,28 +50,32 @@ class SimpleAttributionWidget extends StatelessWidget { }); @override - Widget build(BuildContext context) => Align( - alignment: alignment, - child: ColoredBox( - color: backgroundColor ?? Theme.of(context).colorScheme.background, - child: GestureDetector( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(3), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('flutter_map | © '), - MouseRegion( - cursor: onTap == null - ? MouseCursor.defer - : SystemMouseCursors.click, - child: source, - ), - ], - ), + Widget build(BuildContext context) { + super.build(context); + + return Align( + alignment: alignment, + child: ColoredBox( + color: backgroundColor ?? Theme.of(context).colorScheme.background, + child: GestureDetector( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(3), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('flutter_map | © '), + MouseRegion( + cursor: onTap == null + ? MouseCursor.defer + : SystemMouseCursors.click, + child: source, + ), + ], ), ), ), - ); + ), + ); + } } diff --git a/lib/src/layer/general/overlay_layer.dart b/lib/src/layer/general/overlay_layer.dart new file mode 100644 index 000000000..b071d3307 --- /dev/null +++ b/lib/src/layer/general/overlay_layer.dart @@ -0,0 +1,148 @@ +part of '../../map/widget.dart'; + +/// Provide an internal detection point for the overlay layer mixins +/// +/// Although any other widget could be used as the detection point, this is +/// provided as close as possible to the mixin-ed widgets to reduce the number of +/// iterations `context.visitAncestorElements` requires to ascertain whether +/// the mixin is being used correctly. +class _OverlayLayerDetectorAncestor extends StatelessWidget { + const _OverlayLayerDetectorAncestor({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) => child; + + static Widget _buildDetector(BuildContext context) { + context.visitAncestorElements((e) { + if (e.widget is _OverlayLayerDetectorAncestor) return false; + + throw FlutterError( + 'The widget with `OverlayLayer*Mixin` (or `OverlayLayer`) must only be ' + 'used as a top level widget in `FlutterMap.children`\n' + 'Failure to do so will mean that the layer behaves as a normal layer.\n' + 'To resolve this:\n' + " * if you're using a provided layer beneath this widget, check if it " + 'already includes the appropriate mixin, in which case, remove this ' + 'widget\n' + " * if you're using a custom widget beneath this widget, ensure it is a " + 'top level widget in `FlutterMap.children`, and swap widgets if ' + 'necessary\n', + ); + }); + + // The user shouldn't build the output of this method + return Builder( + builder: (_) => throw FlutterError( + 'Widgets that mixin `OverlayLayer*Mixin` must call ' + '`super.build(context)`, but must also ignore the return value', + ), + ); + } +} + +/// Apply to a [StatelessWidget] to transform it into an overlay widget that is +/// anchored and does not move with the map +/// +/// The widget mixing this in must always be a top level widget in +/// [FlutterMap.children], ie. it must not be a child of another widget. Failure +/// to do this will throw an error, as the behaviour will not be correct. +/// +/// Always call `super.build(context)` from within the widget's `build` method. +/// Ignore its result, and build children as normal. +/// +/// See also: +/// +/// * [OverlayLayer], which mixes this onto a standard child widget +/// * [OverlayLayerStateMixin], which is the equivalent for [StatefulWidget]s +mixin OverlayLayerStatelessMixin on StatelessWidget { + @override + @mustCallSuper + Widget build(BuildContext context) => + _OverlayLayerDetectorAncestor._buildDetector(context); +} + +/// Apply to a [State] to transform it into an overlay widget that is anchored +/// and does not move with the map +/// +/// Must be paired with an [OverlayLayerStatefulMixin] on the [StatefulWidget]. +/// +/// The widget mixing this in must always be a top level widget in +/// [FlutterMap.children], ie. it must not be a child of another widget. Failure +/// to do this will throw an error, as the behaviour will not be correct. +/// +/// Always call `super.build(context)` from within the widget's `build` method. +/// Ignore its result, and build children as normal. +/// +/// See also: +/// +/// * [OverlayLayer], which mixes [OverlayLayerStatelessMixin] onto a +/// standard child widget +/// * [OverlayLayerStatelessMixin], which is the equivalent for +/// [StatelessWidget]s +mixin OverlayLayerStateMixin< + T extends OverlayLayerStatefulMixin>> + on State { + @override + @mustCallSuper + Widget build(BuildContext context) => + _OverlayLayerDetectorAncestor._buildDetector(context); +} + +/// Apply to a [StatefulWidget] to transform it into an overlay widget that is +/// anchored and does not move with the map +/// +/// Must be paired with an [OverlayLayerStateMixin] on the [State]. +/// +/// The widget mixing this in must always be a top level widget in +/// [FlutterMap.children], ie. it must not be a child of another widget. Failure +/// to do this will throw an error, as the behaviour will not be correct. +/// +/// Always call `super.build(context)` from within the widget's `build` method. +/// Ignore its result, and build children as normal. +/// +/// See also: +/// +/// * [OverlayLayer], which mixes [OverlayLayerStatelessMixin] onto a +/// standard child widget +/// * [OverlayLayerStatelessMixin], which is the equivalent for +/// [StatelessWidget]s +mixin OverlayLayerStatefulMixin>> + on StatefulWidget { + @override + OverlayLayerStateMixin createState(); +} + +/// {@template overlay_layer} +/// Transforms the [child] widget into an overlay layer that is anchored and +/// does not move with the map +/// +/// This widget must always be a top level widget in [FlutterMap.children], ie. +/// it must not be a child of another widget. Failure to do this will throw an +/// error, as the behaviour will not be correct. +/// +/// Some layers include the appropriate mixins, if they are not intended to be +/// used in a non-overlay scenario, such as the [AttributionWidget]s. If this is +/// the case, those layers should document this behaviour, as applying an +/// additional [OverlayLayer] transformer will cause an erroneous result. +/// +/// If you have control over the [child], prefer mixing in +/// [OverlayLayerStatelessMixin] or [OverlayLayerStatefulMixin] / +/// [OverlayLayerStateMixin] yourself, to avoid an extra widget in the tree. +/// {@endtemplate} +class OverlayLayer extends StatelessWidget with OverlayLayerStatelessMixin { + /// {@macro overlay_layer} + const OverlayLayer({ + super.key, + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + super.build(context); + return child; + } +} diff --git a/lib/src/layer/general/translucent_pointer.dart b/lib/src/layer/general/translucent_pointer.dart new file mode 100644 index 000000000..91819f2d7 --- /dev/null +++ b/lib/src/layer/general/translucent_pointer.dart @@ -0,0 +1,130 @@ +// Migrated from https://github.com/spkersten/flutter_transparent_pointer, with +// some changes + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +/// This widget is invisible for its parent during hit testing, but still +/// allows its subtree to receive pointer events. +/// +/// +/// In this example, a drag can be started anywhere in the widget, including on +/// top of the text button, even though the button is visually in front of the +/// background gesture detector. At the same time, the button is tappable. +/// +/// ```dart +/// class MyWidget extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return Stack( +/// children: [ +/// GestureDetector( +/// behavior: HitTestBehavior.opaque, +/// onVerticalDragStart: (_) => print("Background drag started"), +/// ), +/// Positioned( +/// top: 60, +/// left: 60, +/// height: 60, +/// width: 60, +/// child: TransparentPointer( +/// child: TextButton( +/// child: Text("Tap me"), +/// onPressed: () => print("You tapped me"), +/// ), +/// ), +/// ), +/// ], +/// ); +/// } +/// } +/// ``` +/// +/// See also: +/// +/// * [IgnorePointer], which is also invisible for its parent during hit +/// testing, but does not allow its subtree to receive pointer events. +/// * [AbsorbPointer], which is visible during hit testing, but prevents its +/// subtree from receiving pointer event. The opposite of this widget. +class TranslucentPointer extends SingleChildRenderObjectWidget { + /// Creates a widget that is invisible for its parent during hit testing, but + /// still allows its subtree to receive pointer events. + const TranslucentPointer({ + super.key, + this.translucent = true, + super.child, + }); + + /// Whether this widget is invisible to its parent during hit testing. + /// + /// Regardless of whether this render object is invisible to its parent during + /// hit testing, it will still consume space during layout and be visible + /// during painting. + final bool translucent; + + @override + RenderTranslucentPointer createRenderObject(BuildContext context) => + RenderTranslucentPointer(translucent: translucent); + + @override + void updateRenderObject( + BuildContext context, + RenderTranslucentPointer renderObject, + ) => + renderObject.translucent = translucent; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('translucent', translucent)); + } +} + +/// A render object that is invisible to its parent during hit testing. +/// +/// When [translucent] is true, this render object allows its subtree to receive +/// pointer events, whilst also not terminating hit testing at itself. It still +/// consumes space during layout and paints its child as usual. It just prevents +/// its children from being the termination of located events, because its render +/// object returns true from [hitTest]. +/// +/// See also: +/// +/// * [RenderIgnorePointer], removing the subtree from considering entirely for +/// the purposes of hit testing. +/// * [RenderAbsorbPointer], which takes the pointer events but prevents any +/// nodes in the subtree from seeing them. +class RenderTranslucentPointer extends RenderProxyBox { + /// Creates a render object that is invisible to its parent during hit testing. + /// + /// The [translucent] argument must not be null. + RenderTranslucentPointer({ + RenderBox? child, + bool translucent = true, + }) : _translucent = translucent, + super(child); + + /// Whether this widget is invisible to its parent during hit testing. + /// + /// Regardless of whether this render object is invisible to its parent during + /// hit testing, it will still consume space during layout and be visible + /// during painting. + bool get translucent => _translucent; + bool _translucent; + set translucent(bool value) { + if (value == _translucent) return; + _translucent = value; + } + + @override + bool hitTest(BoxHitTestResult result, {required Offset position}) { + final hit = super.hitTest(result, position: position); + return !translucent && hit; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('translucent', translucent)); + } +} diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 799048d58..c37e7238d 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -238,6 +238,11 @@ class TileLayer extends StatefulWidget { this.subdomains = const [], this.keepBuffer = 2, this.panBuffer = 0, + @Deprecated( + 'Prefer `MapOptions.backgroundColor`. ' + 'This property has been removed simplify interaction when using multiple `TileLayer`s. ' + 'This property is deprecated since v6.', + ) this.backgroundColor, this.errorImage, final TileProvider? tileProvider, diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index af5b48336..37846d6c4 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -110,6 +110,18 @@ class MapOptions { /// widget from rebuilding. final bool keepAlive; + /// Whether to apply pointer translucency to all layers automatically + /// + /// This means that layers are invisible to its parent when hit testing, but + /// still allows its subtree to receive pointer events. + /// + /// Note that layers that are visually obscured behind another layer will + /// recieve events, if this is enabled. + /// + /// If this is `false` (defaults to `true`), then `TranslucentPointer` may be + /// used on individual layers. + final bool applyPointerTranslucencyToLayers; + final InteractionOptions? _interactionOptions; const MapOptions({ @@ -236,6 +248,7 @@ class MapOptions { ) this.maxBounds, this.keepAlive = false, + this.applyPointerTranslucencyToLayers = true, }) : _interactionOptions = interactionOptions, _interactiveFlags = interactiveFlags, _debugMultiFingerGestureWinner = debugMultiFingerGestureWinner, diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index 2e3fed1ce..b728e1588 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -1,8 +1,15 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:math'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/layer/attribution_layer/rich.dart'; +import 'package:flutter_map/src/layer/attribution_layer/simple.dart'; +import 'package:flutter_map/src/layer/general/translucent_pointer.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/map/internal_controller.dart'; @@ -10,6 +17,8 @@ import 'package:flutter_map/src/map/map_controller.dart'; import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:flutter_map/src/map/options.dart'; +part '../layer/general/overlay_layer.dart'; + /// Renders an interactive geographical map as a widget /// /// See the online documentation for more information about set-up, @@ -24,14 +33,52 @@ class FlutterMap extends StatefulWidget { super.key, required this.options, this.children = const [], + @Deprecated( + 'Prefer `children`. ' + 'This property has been removed to simplify the way layers are used. ' + 'This property is deprecated since v6.', + ) this.nonRotatedChildren = const [], this.mapController, }); + /// Renders a simple geographical map as a widget + /// + /// Has limited customization options, and lacks the ability to add feature + /// layers. Prefer [FlutterMap]'s standard constructor if these are required. + /// + /// Provide a [RichAttributionWidget] or [SimpleAttributionWidget] to the + /// [attribution] argument. + /// + /// See the online documentation for more information about set-up, + /// configuration, and usage. + FlutterMap.simple({ + super.key, + required this.options, + required String urlTemplate, + required String userAgentPackageName, + required AttributionWidget attribution, + }) : children = [ + TileLayer( + urlTemplate: urlTemplate, + userAgentPackageName: userAgentPackageName, + ), + attribution, + ], + mapController = null, + nonRotatedChildren = []; + /// Layers/widgets to be painted onto the map, in a [Stack]-like fashion final List children; - /// Same as [children], except these are unnaffected by map rotation + /// Same as [children], except these are overlaid onto the map + /// + /// See [OverlayLayer] for information. + @Deprecated( + 'Prefer `children`. ' + 'This property has been removed to simplify the way layers are used. ' + 'This property is deprecated since v6.', + ) final List nonRotatedChildren; /// Configure this map @@ -101,23 +148,13 @@ class FlutterMapStateContainer extends State { options: options, camera: camera, child: ClipRect( - child: Stack( - children: [ - Positioned.fill( - child: ColoredBox(color: options.backgroundColor), - ), - OverflowBox( - minWidth: camera.size.x, - maxWidth: camera.size.x, - minHeight: camera.size.y, - maxHeight: camera.size.y, - child: Transform.rotate( - angle: camera.rotationRad, - child: Stack(children: widget.children), - ), - ), - ...widget.nonRotatedChildren, - ], + child: ColoredBox( + color: options.backgroundColor, + child: _LayersStack( + camera: camera, + options: options, + children: widget.children..addAll(widget.nonRotatedChildren), + ), ), ), ), @@ -198,3 +235,79 @@ class FlutterMapStateContainer extends State { BuildContext context, BoxConstraints constraints) => constraints.maxWidth != 0 || MediaQuery.sizeOf(context) != Size.zero; } + +class _LayersStack extends StatefulWidget { + const _LayersStack({ + required this.camera, + required this.options, + required this.children, + }); + + final MapCamera camera; + final MapOptions options; + final List children; + + @override + State<_LayersStack> createState() => _LayersStackState(); +} + +class _LayersStackState extends State<_LayersStack> { + List children = []; + + Iterable _prepareChildren() sync* { + final stackChildren = []; + + Widget prepareRotateStack() { + final box = OverflowBox( + minWidth: widget.camera.size.x, + maxWidth: widget.camera.size.x, + minHeight: widget.camera.size.y, + maxHeight: widget.camera.size.y, + child: Transform.rotate( + angle: widget.camera.rotationRad, + child: Stack(children: List.from(stackChildren)), + ), + ); + stackChildren.clear(); + return box; + } + + for (final Widget child in widget.children) { + if (child is OverlayLayerStatefulMixin || + child is OverlayLayerStatelessMixin) { + if (stackChildren.isNotEmpty) yield prepareRotateStack(); + final overlayChild = _OverlayLayerDetectorAncestor(child: child); + yield widget.options.applyPointerTranslucencyToLayers + ? TranslucentPointer(child: overlayChild) + : overlayChild; + } else { + stackChildren.add( + widget.options.applyPointerTranslucencyToLayers + ? TranslucentPointer(child: child) + : child, + ); + } + } + if (stackChildren.isNotEmpty) yield prepareRotateStack(); + } + + @override + void initState() { + super.initState(); + children = _prepareChildren().toList(); + } + + @override + void didUpdateWidget(covariant _LayersStack oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.children != oldWidget.children || + widget.camera != oldWidget.camera || + widget.options.applyPointerTranslucencyToLayers != + oldWidget.options.applyPointerTranslucencyToLayers) { + children = _prepareChildren().toList(); + } + } + + @override + Widget build(BuildContext context) => Stack(children: children); +} From eced180a4f30f506fcd1899e5c65494df665333b Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 15 Aug 2023 22:03:46 +0100 Subject: [PATCH 02/14] Improved documentation Improved internal file structure --- lib/flutter_map.dart | 2 +- .../flutter_map_interactive_viewer.dart | 2 +- .../impl.dart} | 4 +- .../internal.dart} | 2 +- .../map/{ => controller}/map_controller.dart | 2 +- lib/src/map/inherited_model.dart | 2 +- lib/src/map/layers_stack.dart | 77 +++++++++ lib/src/map/widget.dart | 149 ++++++------------ test/flutter_map_controller_test.dart | 2 +- test/test_utils/test_app.dart | 2 +- 10 files changed, 138 insertions(+), 106 deletions(-) rename lib/src/map/{map_controller_impl.dart => controller/impl.dart} (98%) rename lib/src/map/{internal_controller.dart => controller/internal.dart} (99%) rename lib/src/map/{ => controller}/map_controller.dart (99%) create mode 100644 lib/src/map/layers_stack.dart diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index fbedc6a23..3c5a24822 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -30,7 +30,7 @@ export 'package:flutter_map/src/layer/tile_layer/tile_update_transformer.dart'; export 'package:flutter_map/src/map/camera/camera.dart'; export 'package:flutter_map/src/map/camera/camera_constraint.dart'; export 'package:flutter_map/src/map/camera/camera_fit.dart'; -export 'package:flutter_map/src/map/map_controller.dart'; +export 'package:flutter_map/src/map/controller/map_controller.dart'; export 'package:flutter_map/src/map/options.dart'; export 'package:flutter_map/src/map/widget.dart'; export 'package:flutter_map/src/misc/center_zoom.dart'; diff --git a/lib/src/gestures/flutter_map_interactive_viewer.dart b/lib/src/gestures/flutter_map_interactive_viewer.dart index da1ef6d44..4371fa526 100644 --- a/lib/src/gestures/flutter_map_interactive_viewer.dart +++ b/lib/src/gestures/flutter_map_interactive_viewer.dart @@ -9,7 +9,7 @@ import 'package:flutter_map/src/gestures/latlng_tween.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; -import 'package:flutter_map/src/map/internal_controller.dart'; +import 'package:flutter_map/src/map/controller/internal.dart'; import 'package:flutter_map/src/map/options.dart'; import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; diff --git a/lib/src/map/map_controller_impl.dart b/lib/src/map/controller/impl.dart similarity index 98% rename from lib/src/map/map_controller_impl.dart rename to lib/src/map/controller/impl.dart index 5f6501943..e54085a1d 100644 --- a/lib/src/map/map_controller_impl.dart +++ b/lib/src/map/controller/impl.dart @@ -6,8 +6,8 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/map/camera/camera_fit.dart'; -import 'package:flutter_map/src/map/internal_controller.dart'; -import 'package:flutter_map/src/map/map_controller.dart'; +import 'package:flutter_map/src/map/controller/internal.dart'; +import 'package:flutter_map/src/map/controller/map_controller.dart'; import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; diff --git a/lib/src/map/internal_controller.dart b/lib/src/map/controller/internal.dart similarity index 99% rename from lib/src/map/internal_controller.dart rename to lib/src/map/controller/internal.dart index e97e54503..9689423da 100644 --- a/lib/src/map/internal_controller.dart +++ b/lib/src/map/controller/internal.dart @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; -import 'package:flutter_map/src/map/map_controller_impl.dart'; +import 'package:flutter_map/src/map/controller/impl.dart'; import 'package:latlong2/latlong.dart'; /// This controller is for internal use. All updates to the state should be done diff --git a/lib/src/map/map_controller.dart b/lib/src/map/controller/map_controller.dart similarity index 99% rename from lib/src/map/map_controller.dart rename to lib/src/map/controller/map_controller.dart index c5a6523a9..80a8f5c5d 100644 --- a/lib/src/map/map_controller.dart +++ b/lib/src/map/controller/map_controller.dart @@ -6,8 +6,8 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/map/camera/camera_fit.dart'; +import 'package:flutter_map/src/map/controller/impl.dart'; import 'package:flutter_map/src/map/inherited_model.dart'; -import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; diff --git a/lib/src/map/inherited_model.dart b/lib/src/map/inherited_model.dart index 60b1e9a6a..66faf0589 100644 --- a/lib/src/map/inherited_model.dart +++ b/lib/src/map/inherited_model.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; -import 'package:flutter_map/src/map/map_controller.dart'; +import 'package:flutter_map/src/map/controller/map_controller.dart'; import 'package:flutter_map/src/map/options.dart'; /// Allows descendents of [FlutterMap] to access the [MapCamera], [MapOptions] diff --git a/lib/src/map/layers_stack.dart b/lib/src/map/layers_stack.dart new file mode 100644 index 000000000..2a61ac274 --- /dev/null +++ b/lib/src/map/layers_stack.dart @@ -0,0 +1,77 @@ +part of 'widget.dart'; + +class _LayersStack extends StatefulWidget { + const _LayersStack({ + required this.camera, + required this.options, + required this.children, + }); + + final MapCamera camera; + final MapOptions options; + final List children; + + @override + State<_LayersStack> createState() => _LayersStackState(); +} + +class _LayersStackState extends State<_LayersStack> { + List children = []; + + Iterable _prepareChildren() sync* { + final stackChildren = []; + + Widget prepareRotateStack() { + final box = OverflowBox( + minWidth: widget.camera.size.x, + maxWidth: widget.camera.size.x, + minHeight: widget.camera.size.y, + maxHeight: widget.camera.size.y, + child: Transform.rotate( + angle: widget.camera.rotationRad, + child: Stack(children: List.of(stackChildren)), + ), + ); + stackChildren.clear(); + return box; + } + + for (final Widget child in widget.children) { + if (child is OverlayLayerStatefulMixin || + child is OverlayLayerStatelessMixin) { + if (stackChildren.isNotEmpty) yield prepareRotateStack(); + final overlayChild = _OverlayLayerDetectorAncestor(child: child); + yield widget.options.applyPointerTranslucencyToLayers + ? TranslucentPointer(child: overlayChild) + : overlayChild; + } else { + stackChildren.add( + widget.options.applyPointerTranslucencyToLayers + ? TranslucentPointer(child: child) + : child, + ); + } + } + if (stackChildren.isNotEmpty) yield prepareRotateStack(); + } + + @override + void initState() { + super.initState(); + children = _prepareChildren().toList(); + } + + @override + void didUpdateWidget(covariant _LayersStack oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.children != oldWidget.children || + widget.camera != oldWidget.camera || + widget.options.applyPointerTranslucencyToLayers != + oldWidget.options.applyPointerTranslucencyToLayers) { + children = _prepareChildren().toList(); + } + } + + @override + Widget build(BuildContext context) => Stack(children: children); +} diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index b728e1588..e904b9e48 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -11,87 +11,118 @@ import 'package:flutter_map/src/layer/general/translucent_pointer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/map/camera/camera_fit.dart'; +import 'package:flutter_map/src/map/controller/impl.dart'; +import 'package:flutter_map/src/map/controller/internal.dart'; +import 'package:flutter_map/src/map/controller/map_controller.dart'; import 'package:flutter_map/src/map/inherited_model.dart'; -import 'package:flutter_map/src/map/internal_controller.dart'; -import 'package:flutter_map/src/map/map_controller.dart'; -import 'package:flutter_map/src/map/map_controller_impl.dart'; import 'package:flutter_map/src/map/options.dart'; part '../layer/general/overlay_layer.dart'; +part 'layers_stack.dart'; -/// Renders an interactive geographical map as a widget +/// An interactive geographical map /// /// See the online documentation for more information about set-up, /// configuration, and usage. @immutable class FlutterMap extends StatefulWidget { - /// Renders an interactive geographical map as a widget + /// Creates an interactive geographical map /// /// See the online documentation for more information about set-up, /// configuration, and usage. const FlutterMap({ super.key, + this.mapController, required this.options, - this.children = const [], + required this.children, @Deprecated( 'Prefer `children`. ' 'This property has been removed to simplify the way layers are used. ' 'This property is deprecated since v6.', ) this.nonRotatedChildren = const [], - this.mapController, }); - /// Renders a simple geographical map as a widget + /// Creates an interactive geographical map /// - /// Has limited customization options, and lacks the ability to add feature - /// layers. Prefer [FlutterMap]'s standard constructor if these are required. - /// - /// Provide a [RichAttributionWidget] or [SimpleAttributionWidget] to the - /// [attribution] argument. + /// This constructor is a shortcut intended for only the most simple of maps. + /// It does not support customization of the underlying [TileLayer], and lacks + /// the ability to add feature layers (except for an attribution layer) or + /// attach a [MapController]. Use the standard constructor if these are + /// required. /// /// See the online documentation for more information about set-up, /// configuration, and usage. + /// + /// --- + /// + /// Provide a standard slippy map URL template to the [urlTemplate] argument, + /// with `{x}`, `{y}`, and `{z}` placeholders. Subdomain support is not + /// supported through this simple constructor. + /// + /// Provide the application's correct package name, such as 'com.example.app', + /// to the [userAgentPackageName] argument, to allow the tile server to + /// identify your application. For more information, see + /// [TileLayer.tileProvider]'s documentation. + /// + /// It is recommended to provide a [RichAttributionWidget] or + /// [SimpleAttributionWidget] to the [attribution] argument. For more + /// information, see their documentation. FlutterMap.simple({ super.key, required this.options, required String urlTemplate, required String userAgentPackageName, - required AttributionWidget attribution, + AttributionWidget? attribution, }) : children = [ TileLayer( urlTemplate: urlTemplate, userAgentPackageName: userAgentPackageName, ), - attribution, + if (attribution != null) attribution, ], mapController = null, nonRotatedChildren = []; - /// Layers/widgets to be painted onto the map, in a [Stack]-like fashion + /// Layer widgets to be placed onto the map in a [Stack]-like fashion + /// + /// See the online documentation for more information. final List children; - /// Same as [children], except these are overlaid onto the map + /// This member has been deprecated as of v6. Use [children] instead, using + /// [OverlayLayer]s where necessary. This will simplify the way layers are + /// inserted into the map, and allow for greater flexibility of layer + /// positioning. + /// + /// --- + /// + /// Same as [children], except these are overlaid onto the map in an anchored + /// position /// /// See [OverlayLayer] for information. @Deprecated( 'Prefer `children`. ' - 'This property has been removed to simplify the way layers are used. ' + 'This property has been removed to simplify the way layers are inserted ' + 'into the map, and allow for greater flexibility of layer positioning. ' 'This property is deprecated since v6.', ) final List nonRotatedChildren; - /// Configure this map + /// Configure this map's permanent rules and initial state + /// + /// See the online documentation for more information. final MapOptions options; /// Programmatically interact with this map + /// + /// See the online documentation for more information. final MapController? mapController; @override - State createState() => FlutterMapStateContainer(); + State createState() => _FlutterMapStateContainer(); } -class FlutterMapStateContainer extends State { +class _FlutterMapStateContainer extends State { bool _initialCameraFitApplied = false; late final FlutterMapInternalController _flutterMapInternalController; @@ -235,79 +266,3 @@ class FlutterMapStateContainer extends State { BuildContext context, BoxConstraints constraints) => constraints.maxWidth != 0 || MediaQuery.sizeOf(context) != Size.zero; } - -class _LayersStack extends StatefulWidget { - const _LayersStack({ - required this.camera, - required this.options, - required this.children, - }); - - final MapCamera camera; - final MapOptions options; - final List children; - - @override - State<_LayersStack> createState() => _LayersStackState(); -} - -class _LayersStackState extends State<_LayersStack> { - List children = []; - - Iterable _prepareChildren() sync* { - final stackChildren = []; - - Widget prepareRotateStack() { - final box = OverflowBox( - minWidth: widget.camera.size.x, - maxWidth: widget.camera.size.x, - minHeight: widget.camera.size.y, - maxHeight: widget.camera.size.y, - child: Transform.rotate( - angle: widget.camera.rotationRad, - child: Stack(children: List.from(stackChildren)), - ), - ); - stackChildren.clear(); - return box; - } - - for (final Widget child in widget.children) { - if (child is OverlayLayerStatefulMixin || - child is OverlayLayerStatelessMixin) { - if (stackChildren.isNotEmpty) yield prepareRotateStack(); - final overlayChild = _OverlayLayerDetectorAncestor(child: child); - yield widget.options.applyPointerTranslucencyToLayers - ? TranslucentPointer(child: overlayChild) - : overlayChild; - } else { - stackChildren.add( - widget.options.applyPointerTranslucencyToLayers - ? TranslucentPointer(child: child) - : child, - ); - } - } - if (stackChildren.isNotEmpty) yield prepareRotateStack(); - } - - @override - void initState() { - super.initState(); - children = _prepareChildren().toList(); - } - - @override - void didUpdateWidget(covariant _LayersStack oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.children != oldWidget.children || - widget.camera != oldWidget.camera || - widget.options.applyPointerTranslucencyToLayers != - oldWidget.options.applyPointerTranslucencyToLayers) { - children = _prepareChildren().toList(); - } - } - - @override - Widget build(BuildContext context) => Stack(children: children); -} diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 3d7c5783b..d25f23433 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/map/camera/camera_fit.dart'; -import 'package:flutter_map/src/map/map_controller.dart'; +import 'package:flutter_map/src/map/controller/map_controller.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; diff --git a/test/test_utils/test_app.dart b/test/test_utils/test_app.dart index 203d42645..acfc2bed3 100644 --- a/test/test_utils/test_app.dart +++ b/test/test_utils/test_app.dart @@ -4,7 +4,7 @@ import 'package:flutter_map/src/layer/marker_layer.dart'; import 'package:flutter_map/src/layer/polygon_layer.dart'; import 'package:flutter_map/src/layer/polyline_layer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; -import 'package:flutter_map/src/map/map_controller.dart'; +import 'package:flutter_map/src/map/controller/map_controller.dart'; import 'package:flutter_map/src/map/options.dart'; import 'package:flutter_map/src/map/widget.dart'; import 'package:latlong2/latlong.dart'; From dd8befc595e9edab55c3f632bdf3a44b9f678cbf Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 18 Aug 2023 12:30:56 +0100 Subject: [PATCH 03/14] Added `FlutterMap.overlaidAnchoredChildren` to improve DX Improved documentation --- example/lib/pages/plugin_scalebar.dart | 3 +- .../lib/pages/zoombuttons_plugin_option.dart | 2 +- lib/src/layer/attribution_layer/rich.dart | 11 +- lib/src/layer/attribution_layer/simple.dart | 8 +- lib/src/layer/general/anchored_layer.dart | 160 ++++++++++++++++++ lib/src/layer/general/overlay_layer.dart | 148 ---------------- lib/src/layer/overlay_image_layer.dart | 2 +- lib/src/map/layers_stack.dart | 6 +- lib/src/map/widget.dart | 62 +++---- 9 files changed, 210 insertions(+), 192 deletions(-) create mode 100644 lib/src/layer/general/anchored_layer.dart delete mode 100644 lib/src/layer/general/overlay_layer.dart diff --git a/example/lib/pages/plugin_scalebar.dart b/example/lib/pages/plugin_scalebar.dart index 8f815de32..bc5001747 100644 --- a/example/lib/pages/plugin_scalebar.dart +++ b/example/lib/pages/plugin_scalebar.dart @@ -30,7 +30,8 @@ class PluginScaleBar extends StatelessWidget { 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - OverlayLayer( + // This usage is only for demonstration, prefer a mixin + AnchoredLayerTransformer( child: ScaleLayerWidget( options: ScaleLayerPluginOption( lineColor: Colors.blue, diff --git a/example/lib/pages/zoombuttons_plugin_option.dart b/example/lib/pages/zoombuttons_plugin_option.dart index 9e438e479..32edfeee3 100644 --- a/example/lib/pages/zoombuttons_plugin_option.dart +++ b/example/lib/pages/zoombuttons_plugin_option.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/plugin_api.dart'; class FlutterMapZoomButtons extends StatelessWidget - with OverlayLayerStatelessMixin { + with AnchoredLayerStatelessMixin { final double minZoom; final double maxZoom; final bool mini; diff --git a/lib/src/layer/attribution_layer/rich.dart b/lib/src/layer/attribution_layer/rich.dart index 0dd09e68b..6e5675c59 100644 --- a/lib/src/layer/attribution_layer/rich.dart +++ b/lib/src/layer/attribution_layer/rich.dart @@ -51,7 +51,8 @@ enum AttributionAlignment { /// property for more information. By default, a simple fade/opacity animation /// is provided by [FadeRAWA]. [ScaleRAWA] is also available. /// -/// This layer is an overlay layer, so [OverlayLayer] should not be used. +/// This layer is an anchored layer, so an additional [AnchoredLayer] should not +/// be used. /// /// Read the documentation on the individual properties for more information and /// customizability. @@ -65,8 +66,8 @@ enum AttributionAlignment { class RichAttributionWidget extends StatefulWidget with AttributionWidget, - OverlayLayerStatefulMixin< - OverlayLayerStateMixin> { + AnchoredLayerStatefulMixin< + AnchoredLayerStateMixin> { /// List of attributions to display /// /// [TextSourceAttribution]s are shown in a popup box (toggled by a tap/click @@ -135,11 +136,11 @@ class RichAttributionWidget extends StatefulWidget }); @override - OverlayLayerStateMixin createState() => _RichAttributionWidgetState(); + AnchoredLayerStateMixin createState() => _RichAttributionWidgetState(); } class _RichAttributionWidgetState extends State - with OverlayLayerStateMixin { + with AnchoredLayerStateMixin { StreamSubscription? mapEventSubscription; final persistentAttributionKey = GlobalKey(); diff --git a/lib/src/layer/attribution_layer/simple.dart b/lib/src/layer/attribution_layer/simple.dart index 551e296f9..a0140ac18 100644 --- a/lib/src/layer/attribution_layer/simple.dart +++ b/lib/src/layer/attribution_layer/simple.dart @@ -14,7 +14,8 @@ mixin AttributionWidget on Widget {} /// Displayed as a padded translucent [backgroundColor] box with the following /// text: 'flutter_map | © [source]', where [source] is wrapped with [onTap]. /// -/// This layer is an overlay layer, so [OverlayLayer] should not be used. +/// This layer is an anchored layer, so an additional [AnchoredLayer] should not +/// be used. /// /// See also: /// @@ -22,7 +23,7 @@ mixin AttributionWidget on Widget {} /// and has a more complex appearance. @immutable class SimpleAttributionWidget extends StatelessWidget - with AttributionWidget, OverlayLayerStatelessMixin { + with AttributionWidget, AnchoredLayerStatelessMixin { /// Attribution text, such as 'OpenStreetMap contributors' final Text source; @@ -40,7 +41,8 @@ class SimpleAttributionWidget extends StatelessWidget /// Displayed as a padded translucent white box with the following text: /// 'flutter_map | © [source]'. /// - /// This layer is an overlay layer, so [OverlayLayer] should not be used. + /// This layer is an anchored layer, so an additional [AnchoredLayer] should + /// not be used. const SimpleAttributionWidget({ super.key, required this.source, diff --git a/lib/src/layer/general/anchored_layer.dart b/lib/src/layer/general/anchored_layer.dart new file mode 100644 index 000000000..7b84bf16e --- /dev/null +++ b/lib/src/layer/general/anchored_layer.dart @@ -0,0 +1,160 @@ +part of '../../map/widget.dart'; + +/// Provide an internal detection point for the [AnchoredLayer]s +/// +/// Although any other widget could be used as the detection point, this is +/// provided as close as possible to the mixin-ed widgets to allow only one +/// `context.visitAncestorElements` iteration to determine whether usage is +/// correct. +class _AnchoredLayerDetectorAncestor extends StatelessWidget { + const _AnchoredLayerDetectorAncestor({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) => child; + + static Widget _buildDetector(BuildContext context) { + context.visitAncestorElements((e) { + if (e.widget is _AnchoredLayerDetectorAncestor) return false; + + throw FlutterError( + 'The `AnchoredLayer` was used incorrectly. Read the documenation on ' + '`AnchoredLayer` for more information.', + ); + }); + + // The user shouldn't build the output of this method + return Builder( + builder: (_) => throw FlutterError( + 'Widgets that mixin `AnchoredLayer*Mixin` must call ' + '`super.build(context)`, but must also ignore the return value', + ), + ); + } +} + +/// Apply to a [StatelessWidget] to transform it into an [AnchoredLayer] +/// +/// {@macro anchored_layer_call_super} +/// +/// {@macro anchored_layer_more_info} +/// +/// --- +/// +/// {@macro anchored_layer_warning} +mixin AnchoredLayerStatelessMixin on StatelessWidget implements AnchoredLayer { + @override + @mustCallSuper + Widget build(BuildContext context) => + _AnchoredLayerDetectorAncestor._buildDetector(context); +} + +/// Apply to a [State] to transform its corresponding [StatefulWidget] into an +/// [AnchoredLayer] +/// +/// Must be paired with an [AnchoredLayerStatefulMixin] on the [State]. See +/// [RichAttributionWidget] for an example of this. +/// +/// {@macro anchored_layer_call_super} +/// +/// {@macro anchored_layer_more_info} +/// +/// --- +/// +/// {@macro anchored_layer_warning} +mixin AnchoredLayerStateMixin< + T extends AnchoredLayerStatefulMixin>> + on State { + @override + @mustCallSuper + Widget build(BuildContext context) => + _AnchoredLayerDetectorAncestor._buildDetector(context); +} + +/// Apply to a [StatefulWidget] to transform it into an [AnchoredLayer] +/// +/// Must be paired with an [AnchoredLayerStateMixin] on the [State], and this +/// object's [createState] method must return an instance of +/// [AnchoredLayerStateMixin]. See [RichAttributionWidget] for an example of +/// this. +/// +/// {@template anchored_layer_call_super} +/// Always call `super.build(context)` from within the widget's `build` method, +/// but ignore its result and build children as normal. +/// {@endtemplate} +/// +/// {@macro anchored_layer_more_info} +/// +/// --- +/// +/// {@macro anchored_layer_warning} +mixin AnchoredLayerStatefulMixin>> + on StatefulWidget implements AnchoredLayer { + @override + AnchoredLayerStateMixin createState(); +} + +/// Transforms the [child] widget into an [AnchoredLayer] +/// +/// Uses a [AnchoredLayerStatelessMixin] internally. +/// +/// {@template anchored_layer_more_info} +/// See [AnchoredLayer] for more information about other methods to create an +/// anchored layer. +/// {@endtemplate} +/// +/// --- +/// +/// {@macro anchored_layer_warning} +class AnchoredLayerTransformer extends StatelessWidget + with AnchoredLayerStatelessMixin + implements AnchoredLayer { + /// Transforms the [child] widget into an [AnchoredLayer] + /// + /// Uses a [AnchoredLayerStatelessMixin] internally. + /// + /// --- + /// + /// {@macro anchored_layer_warning} + const AnchoredLayerTransformer({ + super.key, + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + super.build(context); + return child; + } +} + +/// A layer that is anchored to the map, that does not move with the other layers +/// +/// There are multiple ways to add an anchored layer to the map, in order of +/// preference: +/// +/// 1. Apply [AnchoredLayerStatelessMixin] to a [StatelessWidget] +/// 2. Apply [AnchoredLayerStatefulMixin] to a [StatefulWidget], and +/// [AnchoredLayerStateMixin] to its corresponding [State] +/// 3. Wrap the normal widget with an [AnchoredLayerTransformer] +/// +/// {@template anchored_layer_warning} +/// Anchored layers must be on the top-level of [FlutterMap.children] and +/// [FlutterMap.overlaidAnchoredChildren]. They must also not be multiplied as an +/// ancestor or child. Failure to do this will throw an error, as the anchored +/// effect will not be correctly applied. +/// +/// * If you have control over the widget, do not use [AnchoredLayerTransformer] +/// in its `build` method. Prefer using the appropriate mixin, or wrap the +/// transformer around every instance of the widget. +/// * If the widget already uses a mixin, do not use [AnchoredLayerTransformer] +/// in addition. These widgets are designed to be used as an anchored layer only, +/// and need no additional setup. These widgets should contain a notice in the +/// documentation. +/// {@endtemplate} +sealed class AnchoredLayer extends Widget { + const AnchoredLayer({super.key}); +} diff --git a/lib/src/layer/general/overlay_layer.dart b/lib/src/layer/general/overlay_layer.dart deleted file mode 100644 index b071d3307..000000000 --- a/lib/src/layer/general/overlay_layer.dart +++ /dev/null @@ -1,148 +0,0 @@ -part of '../../map/widget.dart'; - -/// Provide an internal detection point for the overlay layer mixins -/// -/// Although any other widget could be used as the detection point, this is -/// provided as close as possible to the mixin-ed widgets to reduce the number of -/// iterations `context.visitAncestorElements` requires to ascertain whether -/// the mixin is being used correctly. -class _OverlayLayerDetectorAncestor extends StatelessWidget { - const _OverlayLayerDetectorAncestor({required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) => child; - - static Widget _buildDetector(BuildContext context) { - context.visitAncestorElements((e) { - if (e.widget is _OverlayLayerDetectorAncestor) return false; - - throw FlutterError( - 'The widget with `OverlayLayer*Mixin` (or `OverlayLayer`) must only be ' - 'used as a top level widget in `FlutterMap.children`\n' - 'Failure to do so will mean that the layer behaves as a normal layer.\n' - 'To resolve this:\n' - " * if you're using a provided layer beneath this widget, check if it " - 'already includes the appropriate mixin, in which case, remove this ' - 'widget\n' - " * if you're using a custom widget beneath this widget, ensure it is a " - 'top level widget in `FlutterMap.children`, and swap widgets if ' - 'necessary\n', - ); - }); - - // The user shouldn't build the output of this method - return Builder( - builder: (_) => throw FlutterError( - 'Widgets that mixin `OverlayLayer*Mixin` must call ' - '`super.build(context)`, but must also ignore the return value', - ), - ); - } -} - -/// Apply to a [StatelessWidget] to transform it into an overlay widget that is -/// anchored and does not move with the map -/// -/// The widget mixing this in must always be a top level widget in -/// [FlutterMap.children], ie. it must not be a child of another widget. Failure -/// to do this will throw an error, as the behaviour will not be correct. -/// -/// Always call `super.build(context)` from within the widget's `build` method. -/// Ignore its result, and build children as normal. -/// -/// See also: -/// -/// * [OverlayLayer], which mixes this onto a standard child widget -/// * [OverlayLayerStateMixin], which is the equivalent for [StatefulWidget]s -mixin OverlayLayerStatelessMixin on StatelessWidget { - @override - @mustCallSuper - Widget build(BuildContext context) => - _OverlayLayerDetectorAncestor._buildDetector(context); -} - -/// Apply to a [State] to transform it into an overlay widget that is anchored -/// and does not move with the map -/// -/// Must be paired with an [OverlayLayerStatefulMixin] on the [StatefulWidget]. -/// -/// The widget mixing this in must always be a top level widget in -/// [FlutterMap.children], ie. it must not be a child of another widget. Failure -/// to do this will throw an error, as the behaviour will not be correct. -/// -/// Always call `super.build(context)` from within the widget's `build` method. -/// Ignore its result, and build children as normal. -/// -/// See also: -/// -/// * [OverlayLayer], which mixes [OverlayLayerStatelessMixin] onto a -/// standard child widget -/// * [OverlayLayerStatelessMixin], which is the equivalent for -/// [StatelessWidget]s -mixin OverlayLayerStateMixin< - T extends OverlayLayerStatefulMixin>> - on State { - @override - @mustCallSuper - Widget build(BuildContext context) => - _OverlayLayerDetectorAncestor._buildDetector(context); -} - -/// Apply to a [StatefulWidget] to transform it into an overlay widget that is -/// anchored and does not move with the map -/// -/// Must be paired with an [OverlayLayerStateMixin] on the [State]. -/// -/// The widget mixing this in must always be a top level widget in -/// [FlutterMap.children], ie. it must not be a child of another widget. Failure -/// to do this will throw an error, as the behaviour will not be correct. -/// -/// Always call `super.build(context)` from within the widget's `build` method. -/// Ignore its result, and build children as normal. -/// -/// See also: -/// -/// * [OverlayLayer], which mixes [OverlayLayerStatelessMixin] onto a -/// standard child widget -/// * [OverlayLayerStatelessMixin], which is the equivalent for -/// [StatelessWidget]s -mixin OverlayLayerStatefulMixin>> - on StatefulWidget { - @override - OverlayLayerStateMixin createState(); -} - -/// {@template overlay_layer} -/// Transforms the [child] widget into an overlay layer that is anchored and -/// does not move with the map -/// -/// This widget must always be a top level widget in [FlutterMap.children], ie. -/// it must not be a child of another widget. Failure to do this will throw an -/// error, as the behaviour will not be correct. -/// -/// Some layers include the appropriate mixins, if they are not intended to be -/// used in a non-overlay scenario, such as the [AttributionWidget]s. If this is -/// the case, those layers should document this behaviour, as applying an -/// additional [OverlayLayer] transformer will cause an erroneous result. -/// -/// If you have control over the [child], prefer mixing in -/// [OverlayLayerStatelessMixin] or [OverlayLayerStatefulMixin] / -/// [OverlayLayerStateMixin] yourself, to avoid an extra widget in the tree. -/// {@endtemplate} -class OverlayLayer extends StatelessWidget with OverlayLayerStatelessMixin { - /// {@macro overlay_layer} - const OverlayLayer({ - super.key, - required this.child, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - super.build(context); - return child; - } -} diff --git a/lib/src/layer/overlay_image_layer.dart b/lib/src/layer/overlay_image_layer.dart index 68d53c0df..2bc7abd6e 100644 --- a/lib/src/layer/overlay_image_layer.dart +++ b/lib/src/layer/overlay_image_layer.dart @@ -150,7 +150,7 @@ class OverlayImageLayer extends StatelessWidget { return ClipRect( child: Stack( children: [ - for (var overlayImage in overlayImages) + for (final overlayImage in overlayImages) overlayImage.buildPositionedForOverlay(map), ], ), diff --git a/lib/src/map/layers_stack.dart b/lib/src/map/layers_stack.dart index 2a61ac274..8448e0282 100644 --- a/lib/src/map/layers_stack.dart +++ b/lib/src/map/layers_stack.dart @@ -37,10 +37,10 @@ class _LayersStackState extends State<_LayersStack> { } for (final Widget child in widget.children) { - if (child is OverlayLayerStatefulMixin || - child is OverlayLayerStatelessMixin) { + if (child is AnchoredLayerStatefulMixin || + child is AnchoredLayerStatelessMixin) { if (stackChildren.isNotEmpty) yield prepareRotateStack(); - final overlayChild = _OverlayLayerDetectorAncestor(child: child); + final overlayChild = _AnchoredLayerDetectorAncestor(child: child); yield widget.options.applyPointerTranslucencyToLayers ? TranslucentPointer(child: overlayChild) : overlayChild; diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index e904b9e48..b5862e2b9 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -1,5 +1,3 @@ -// ignore_for_file: deprecated_member_use_from_same_package - import 'dart:math'; import 'package:flutter/widgets.dart'; @@ -8,6 +6,7 @@ import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/layer/attribution_layer/rich.dart'; import 'package:flutter_map/src/layer/attribution_layer/simple.dart'; import 'package:flutter_map/src/layer/general/translucent_pointer.dart'; +import 'package:flutter_map/src/layer/overlay_image_layer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/map/camera/camera_fit.dart'; @@ -17,7 +16,7 @@ import 'package:flutter_map/src/map/controller/map_controller.dart'; import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/map/options.dart'; -part '../layer/general/overlay_layer.dart'; +part '../layer/general/anchored_layer.dart'; part 'layers_stack.dart'; /// An interactive geographical map @@ -28,19 +27,14 @@ part 'layers_stack.dart'; class FlutterMap extends StatefulWidget { /// Creates an interactive geographical map /// - /// See the online documentation for more information about set-up, - /// configuration, and usage. + /// See the properties and online documentation for more information about + /// set-up, configuration, and usage. const FlutterMap({ super.key, this.mapController, required this.options, required this.children, - @Deprecated( - 'Prefer `children`. ' - 'This property has been removed to simplify the way layers are used. ' - 'This property is deprecated since v6.', - ) - this.nonRotatedChildren = const [], + this.overlaidAnchoredChildren = const [], }); /// Creates an interactive geographical map @@ -51,8 +45,8 @@ class FlutterMap extends StatefulWidget { /// attach a [MapController]. Use the standard constructor if these are /// required. /// - /// See the online documentation for more information about set-up, - /// configuration, and usage. + /// See the properties and online documentation for more information about + /// set-up, configuration, and usage. /// /// --- /// @@ -82,31 +76,38 @@ class FlutterMap extends StatefulWidget { if (attribution != null) attribution, ], mapController = null, - nonRotatedChildren = []; + overlaidAnchoredChildren = []; /// Layer widgets to be placed onto the map in a [Stack]-like fashion /// + /// These may be any widgets, be that prebuilt layers, [AnchoredLayer]s, or + /// custom widgets. + /// /// See the online documentation for more information. + /// + /// --- + /// + /// {@macro anchored_layer_warning} final List children; - /// This member has been deprecated as of v6. Use [children] instead, using - /// [OverlayLayer]s where necessary. This will simplify the way layers are - /// inserted into the map, and allow for greater flexibility of layer - /// positioning. + /// Same as [children], except these are [AnchoredLayer]s only /// - /// --- + /// These may be any widgets, be that prebuilt layers or custom widgets, but + /// they must also be [AnchoredLayer]s by way of mixin or being wrapped in + /// [AnchoredLayerTransformer]. + /// + /// These are overlaid above all normal [children] layers in the order of + /// specification. To use an [AnchoredLayer] in a non-overlaid position + /// instead, insert it directly into [children]. + /// + /// See [AnchoredLayer] and online documentation for more information. /// - /// Same as [children], except these are overlaid onto the map in an anchored - /// position + /// Not to be confused with [OverlayImageLayer]. + /// + /// --- /// - /// See [OverlayLayer] for information. - @Deprecated( - 'Prefer `children`. ' - 'This property has been removed to simplify the way layers are inserted ' - 'into the map, and allow for greater flexibility of layer positioning. ' - 'This property is deprecated since v6.', - ) - final List nonRotatedChildren; + /// {@macro anchored_layer_warning} + final List overlaidAnchoredChildren; /// Configure this map's permanent rules and initial state /// @@ -184,7 +185,8 @@ class _FlutterMapStateContainer extends State { child: _LayersStack( camera: camera, options: options, - children: widget.children..addAll(widget.nonRotatedChildren), + children: widget.children + ..addAll(widget.overlaidAnchoredChildren), ), ), ), From 2a8a32152f93a09706eda93853309f0f230851d5 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 18 Aug 2023 15:16:25 +0100 Subject: [PATCH 04/14] Improved documentation Improved error message for incorrect `AnchoredLayer` usage --- example/lib/pages/wms_tile_layer.dart | 29 ++- lib/src/layer/general/anchored_layer.dart | 183 ++++++++++-------- .../layer/general/translucent_pointer.dart | 51 +---- lib/src/map/options.dart | 8 +- lib/src/map/widget.dart | 10 +- 5 files changed, 141 insertions(+), 140 deletions(-) diff --git a/example/lib/pages/wms_tile_layer.dart b/example/lib/pages/wms_tile_layer.dart index f85ffd301..6b0fbcd6c 100644 --- a/example/lib/pages/wms_tile_layer.dart +++ b/example/lib/pages/wms_tile_layer.dart @@ -28,36 +28,35 @@ class WMSLayerPage extends StatelessWidget { initialCenter: LatLng(42.58, 12.43), initialZoom: 6, ), - children: [ - TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: 'https://{s}.s2maps-tiles.eu/wms/?', - layers: const ['s2cloudless-2021_3857'], - ), - subdomains: const ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), + overlaidAnchoredChildren: [ RichAttributionWidget( popupInitialDisplayDuration: const Duration(seconds: 5), attributions: [ TextSourceAttribution( 'Sentinel-2 cloudless - https://s2maps.eu by EOX IT Services GmbH', - onTap: () => launchUrl( - Uri.parse('https://s2maps.eu '), - ), + onTap: () => launchUrl(Uri.parse('https://s2maps.eu')), ), const TextSourceAttribution( 'Modified Copernicus Sentinel data 2021', ), TextSourceAttribution( 'Rendering: EOX::Maps', - onTap: () => launchUrl( - Uri.parse('https://maps.eox.at/'), - ), + onTap: () => + launchUrl(Uri.parse('https://maps.eox.at/')), ), ], ), ], + children: [ + TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://{s}.s2maps-tiles.eu/wms/?', + layers: const ['s2cloudless-2021_3857'], + ), + subdomains: const ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ), + ], ), ), ], diff --git a/lib/src/layer/general/anchored_layer.dart b/lib/src/layer/general/anchored_layer.dart index 7b84bf16e..e5fcb302f 100644 --- a/lib/src/layer/general/anchored_layer.dart +++ b/lib/src/layer/general/anchored_layer.dart @@ -1,11 +1,59 @@ part of '../../map/widget.dart'; +/// A layer that is anchored to the map, that does not move with the other layers +/// +/// There are multiple ways to add an anchored layer to the map, in order of +/// preference: +/// +/// 1. Apply [AnchoredLayerStatelessMixin] to a [StatelessWidget] +/// 2. Apply [AnchoredLayerStatefulMixin] to a [StatefulWidget], and +/// [AnchoredLayerStateMixin] to its corresponding [State] +/// 3. Wrap the widget with an [AnchoredLayerTransformer] +/// +/// This layer may be used in both [FlutterMap.children] and +/// [FlutterMap.overlaidAnchoredChildren]. See documentation on those properties +/// for more information. +/// +/// {@template anchored_layer_warning} +/// [AnchoredLayer]s must be on the top-level of [FlutterMap.children] or +/// [FlutterMap.overlaidAnchoredChildren]. They must also not be multiplied as an +/// ancestor or child: use only one. Failure to do this will throw an error, as +/// the anchored effect will not be correctly applied. See below for common +/// mistakes and their resolutions: +/// +/// * If you have control over the widget, do not use [AnchoredLayerTransformer] +/// in its `build` method. Prefer using the appropriate mixin, or wrap the +/// transformer around every instance of the widget. +/// * If the widget already uses a mixin, do not use [AnchoredLayerTransformer] +/// in addition. These widgets are designed to be used as an anchored layer only, +/// and need no additional setup. These widgets should contain a notice in the +/// documentation. +/// {@endtemplate} +sealed class AnchoredLayer extends Widget { + const AnchoredLayer._(); +} + /// Provide an internal detection point for the [AnchoredLayer]s /// /// Although any other widget could be used as the detection point, this is /// provided as close as possible to the mixin-ed widgets to allow only one /// `context.visitAncestorElements` iteration to determine whether usage is /// correct. +/// +/// --- +/// +/// Explanation of how [AnchoredLayer]s work internally: +/// +/// 1. A [Widget] is converted to an [AnchoredLayer] by means of the mixins, or +/// the [AnchoredLayerTransformer] (which uses the mixins behind the scenes). +/// 2. The mixin means the affected [Widget] must call the mixin's own `build` +/// method, which calls [_detectAncestor]. +/// 3. [_detectAncestor] performs a single lookup to the direct ancestor, to +/// check whether it is an [_AnchoredLayerDetectorAncestor], throwing if it +/// isn't, because it will not have the next step applied +/// 4. When laying out the layers, [_LayersStack] avoids applying rotation to +/// any widget that has mixed-in one of the mixins, whilst the layer itself +/// doesn't follow the map movement - resulting in an 'anchored' layer. class _AnchoredLayerDetectorAncestor extends StatelessWidget { const _AnchoredLayerDetectorAncestor({required this.child}); @@ -14,14 +62,25 @@ class _AnchoredLayerDetectorAncestor extends StatelessWidget { @override Widget build(BuildContext context) => child; - static Widget _buildDetector(BuildContext context) { + static Widget _detectAncestor(BuildContext context) { context.visitAncestorElements((e) { if (e.widget is _AnchoredLayerDetectorAncestor) return false; - throw FlutterError( - 'The `AnchoredLayer` was used incorrectly. Read the documenation on ' - '`AnchoredLayer` for more information.', - ); + throw FlutterError.fromParts([ + ErrorSummary( + '`AnchoredLayer` (such as `AnchoredLayerTransformer` or one of the ' + 'mixins) was used incorrectly.', + ), + ErrorDescription( + 'Instead of `_AnchoredLayerDetectorAncestor`, the direct ancestor was ' + 'of type `${e.widget.runtimeType}`.', + ), + ErrorHint( + 'Ensure that there is only one `AnchoredLayer`, and that it is a ' + 'top-level widget of `children` or `overlaidAnchoredChildren`. Read ' + 'the documentation on `AnchoredLayer` for more information.', + ) + ]); }); // The user shouldn't build the output of this method @@ -34,27 +93,43 @@ class _AnchoredLayerDetectorAncestor extends StatelessWidget { } } -/// Apply to a [StatelessWidget] to transform it into an [AnchoredLayer] +/// Transforms the [child] widget into an [AnchoredLayer] /// -/// {@macro anchored_layer_call_super} +/// Uses a [AnchoredLayerStatelessMixin] internally. /// -/// {@macro anchored_layer_more_info} +/// {@template anchored_layer_more_info} +/// See [AnchoredLayer] for more information about other methods to create an +/// anchored layer. +/// {@endtemplate} /// /// --- /// /// {@macro anchored_layer_warning} -mixin AnchoredLayerStatelessMixin on StatelessWidget implements AnchoredLayer { +class AnchoredLayerTransformer extends StatelessWidget + with AnchoredLayerStatelessMixin + implements AnchoredLayer { + /// Transforms the [child] widget into an [AnchoredLayer] + /// + /// Uses a [AnchoredLayerStatelessMixin] internally. + /// + /// --- + /// + /// {@macro anchored_layer_warning} + const AnchoredLayerTransformer({ + super.key, + required this.child, + }); + + final Widget child; + @override - @mustCallSuper - Widget build(BuildContext context) => - _AnchoredLayerDetectorAncestor._buildDetector(context); + Widget build(BuildContext context) { + super.build(context); + return child; + } } -/// Apply to a [State] to transform its corresponding [StatefulWidget] into an -/// [AnchoredLayer] -/// -/// Must be paired with an [AnchoredLayerStatefulMixin] on the [State]. See -/// [RichAttributionWidget] for an example of this. +/// Apply to a [StatelessWidget] to transform it into an [AnchoredLayer] /// /// {@macro anchored_layer_call_super} /// @@ -63,13 +138,11 @@ mixin AnchoredLayerStatelessMixin on StatelessWidget implements AnchoredLayer { /// --- /// /// {@macro anchored_layer_warning} -mixin AnchoredLayerStateMixin< - T extends AnchoredLayerStatefulMixin>> - on State { +mixin AnchoredLayerStatelessMixin on StatelessWidget implements AnchoredLayer { @override @mustCallSuper Widget build(BuildContext context) => - _AnchoredLayerDetectorAncestor._buildDetector(context); + _AnchoredLayerDetectorAncestor._detectAncestor(context); } /// Apply to a [StatefulWidget] to transform it into an [AnchoredLayer] @@ -95,66 +168,24 @@ mixin AnchoredLayerStatefulMixin>> AnchoredLayerStateMixin createState(); } -/// Transforms the [child] widget into an [AnchoredLayer] +/// Apply to a [State] to transform its corresponding [StatefulWidget] into an +/// [AnchoredLayer] /// -/// Uses a [AnchoredLayerStatelessMixin] internally. +/// Must be paired with an [AnchoredLayerStatefulMixin] on the [State]. See +/// [RichAttributionWidget] for an example of this. /// -/// {@template anchored_layer_more_info} -/// See [AnchoredLayer] for more information about other methods to create an -/// anchored layer. -/// {@endtemplate} +/// {@macro anchored_layer_call_super} +/// +/// {@macro anchored_layer_more_info} /// /// --- /// /// {@macro anchored_layer_warning} -class AnchoredLayerTransformer extends StatelessWidget - with AnchoredLayerStatelessMixin - implements AnchoredLayer { - /// Transforms the [child] widget into an [AnchoredLayer] - /// - /// Uses a [AnchoredLayerStatelessMixin] internally. - /// - /// --- - /// - /// {@macro anchored_layer_warning} - const AnchoredLayerTransformer({ - super.key, - required this.child, - }); - - final Widget child; - +mixin AnchoredLayerStateMixin< + T extends AnchoredLayerStatefulMixin>> + on State { @override - Widget build(BuildContext context) { - super.build(context); - return child; - } -} - -/// A layer that is anchored to the map, that does not move with the other layers -/// -/// There are multiple ways to add an anchored layer to the map, in order of -/// preference: -/// -/// 1. Apply [AnchoredLayerStatelessMixin] to a [StatelessWidget] -/// 2. Apply [AnchoredLayerStatefulMixin] to a [StatefulWidget], and -/// [AnchoredLayerStateMixin] to its corresponding [State] -/// 3. Wrap the normal widget with an [AnchoredLayerTransformer] -/// -/// {@template anchored_layer_warning} -/// Anchored layers must be on the top-level of [FlutterMap.children] and -/// [FlutterMap.overlaidAnchoredChildren]. They must also not be multiplied as an -/// ancestor or child. Failure to do this will throw an error, as the anchored -/// effect will not be correctly applied. -/// -/// * If you have control over the widget, do not use [AnchoredLayerTransformer] -/// in its `build` method. Prefer using the appropriate mixin, or wrap the -/// transformer around every instance of the widget. -/// * If the widget already uses a mixin, do not use [AnchoredLayerTransformer] -/// in addition. These widgets are designed to be used as an anchored layer only, -/// and need no additional setup. These widgets should contain a notice in the -/// documentation. -/// {@endtemplate} -sealed class AnchoredLayer extends Widget { - const AnchoredLayer({super.key}); + @mustCallSuper + Widget build(BuildContext context) => + _AnchoredLayerDetectorAncestor._detectAncestor(context); } diff --git a/lib/src/layer/general/translucent_pointer.dart b/lib/src/layer/general/translucent_pointer.dart index 91819f2d7..e11cbc3cf 100644 --- a/lib/src/layer/general/translucent_pointer.dart +++ b/lib/src/layer/general/translucent_pointer.dart @@ -1,44 +1,11 @@ // Migrated from https://github.com/spkersten/flutter_transparent_pointer, with -// some changes +// some API & documentation changes import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -/// This widget is invisible for its parent during hit testing, but still -/// allows its subtree to receive pointer events. -/// -/// -/// In this example, a drag can be started anywhere in the widget, including on -/// top of the text button, even though the button is visually in front of the -/// background gesture detector. At the same time, the button is tappable. -/// -/// ```dart -/// class MyWidget extends StatelessWidget { -/// @override -/// Widget build(BuildContext context) { -/// return Stack( -/// children: [ -/// GestureDetector( -/// behavior: HitTestBehavior.opaque, -/// onVerticalDragStart: (_) => print("Background drag started"), -/// ), -/// Positioned( -/// top: 60, -/// left: 60, -/// height: 60, -/// width: 60, -/// child: TransparentPointer( -/// child: TextButton( -/// child: Text("Tap me"), -/// onPressed: () => print("You tapped me"), -/// ), -/// ), -/// ), -/// ], -/// ); -/// } -/// } -/// ``` +/// A widget that is invisible for its parent during hit testing, but still +/// allows its subtree to receive pointer events /// /// See also: /// @@ -48,7 +15,7 @@ import 'package:flutter/widgets.dart'; /// subtree from receiving pointer event. The opposite of this widget. class TranslucentPointer extends SingleChildRenderObjectWidget { /// Creates a widget that is invisible for its parent during hit testing, but - /// still allows its subtree to receive pointer events. + /// still allows its subtree to receive pointer events const TranslucentPointer({ super.key, this.translucent = true, @@ -95,9 +62,7 @@ class TranslucentPointer extends SingleChildRenderObjectWidget { /// * [RenderAbsorbPointer], which takes the pointer events but prevents any /// nodes in the subtree from seeing them. class RenderTranslucentPointer extends RenderProxyBox { - /// Creates a render object that is invisible to its parent during hit testing. - /// - /// The [translucent] argument must not be null. + /// Creates a render object that is invisible to its parent during hit testing RenderTranslucentPointer({ RenderBox? child, bool translucent = true, @@ -117,10 +82,8 @@ class RenderTranslucentPointer extends RenderProxyBox { } @override - bool hitTest(BoxHitTestResult result, {required Offset position}) { - final hit = super.hitTest(result, position: position); - return !translucent && hit; - } + bool hitTest(BoxHitTestResult result, {required Offset position}) => + !translucent && super.hitTest(result, position: position); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index 37846d6c4..b82bdaa3a 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -6,9 +6,11 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; +import 'package:flutter_map/src/layer/general/translucent_pointer.dart'; import 'package:flutter_map/src/map/camera/camera_constraint.dart'; import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/map/inherited_model.dart'; +import 'package:flutter_map/src/map/widget.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/position.dart'; import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart'; @@ -118,8 +120,10 @@ class MapOptions { /// Note that layers that are visually obscured behind another layer will /// recieve events, if this is enabled. /// - /// If this is `false` (defaults to `true`), then `TranslucentPointer` may be - /// used on individual layers. + /// Also note that this applies to (overlaid) [AnchoredLayer]s as well. + /// + /// If this is `false` (defaults to `true`), then [TranslucentPointer] may be + /// applied to individual layers. final bool applyPointerTranslucencyToLayers; final InteractionOptions? _interactionOptions; diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index b5862e2b9..81c4b5513 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -83,10 +83,14 @@ class FlutterMap extends StatefulWidget { /// These may be any widgets, be that prebuilt layers, [AnchoredLayer]s, or /// custom widgets. /// - /// See the online documentation for more information. - /// /// --- /// + /// Note that using [AnchoredLayer]s at the end of this list is equivalent to + /// using them in [overlaidAnchoredChildren] instead. + /// + /// When inserting [AnchoredLayer]s inbetween children (ie. not at the end), + /// there is likely to be a very small performance penalty. + /// /// {@macro anchored_layer_warning} final List children; @@ -100,7 +104,7 @@ class FlutterMap extends StatefulWidget { /// specification. To use an [AnchoredLayer] in a non-overlaid position /// instead, insert it directly into [children]. /// - /// See [AnchoredLayer] and online documentation for more information. + /// See [AnchoredLayer] for more information. /// /// Not to be confused with [OverlayImageLayer]. /// From 3f600963c970c57e17485fade66e79faac6dacb5 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 18 Aug 2023 15:33:21 +0100 Subject: [PATCH 05/14] Made `AnchoredLayerTransformer` private, in favour of a redirecting constructor on `AnchoredLayer` --- example/lib/pages/plugin_scalebar.dart | 2 +- lib/src/layer/general/anchored_layer.dart | 64 ++++++++++------------- lib/src/map/widget.dart | 4 +- 3 files changed, 31 insertions(+), 39 deletions(-) diff --git a/example/lib/pages/plugin_scalebar.dart b/example/lib/pages/plugin_scalebar.dart index bc5001747..e3cd4fa04 100644 --- a/example/lib/pages/plugin_scalebar.dart +++ b/example/lib/pages/plugin_scalebar.dart @@ -31,7 +31,7 @@ class PluginScaleBar extends StatelessWidget { userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), // This usage is only for demonstration, prefer a mixin - AnchoredLayerTransformer( + AnchoredLayer( child: ScaleLayerWidget( options: ScaleLayerPluginOption( lineColor: Colors.blue, diff --git a/lib/src/layer/general/anchored_layer.dart b/lib/src/layer/general/anchored_layer.dart index e5fcb302f..7b0673092 100644 --- a/lib/src/layer/general/anchored_layer.dart +++ b/lib/src/layer/general/anchored_layer.dart @@ -2,13 +2,14 @@ part of '../../map/widget.dart'; /// A layer that is anchored to the map, that does not move with the other layers /// -/// There are multiple ways to add an anchored layer to the map, in order of -/// preference: +/// There are multiple ways to transform a widget into an anchored layer. In +/// order of preference: /// /// 1. Apply [AnchoredLayerStatelessMixin] to a [StatelessWidget] /// 2. Apply [AnchoredLayerStatefulMixin] to a [StatefulWidget], and /// [AnchoredLayerStateMixin] to its corresponding [State] -/// 3. Wrap the widget with an [AnchoredLayerTransformer] +/// 3. Wrap the widget with an [AnchoredLayer] (which uses an extra widget that +/// mixes in [AnchoredLayerStatelessMixin]) /// /// This layer may be used in both [FlutterMap.children] and /// [FlutterMap.overlaidAnchoredChildren]. See documentation on those properties @@ -21,16 +22,27 @@ part of '../../map/widget.dart'; /// the anchored effect will not be correctly applied. See below for common /// mistakes and their resolutions: /// -/// * If you have control over the widget, do not use [AnchoredLayerTransformer] +/// - If you have control over the widget, do not use [AnchoredLayer] /// in its `build` method. Prefer using the appropriate mixin, or wrap the /// transformer around every instance of the widget. -/// * If the widget already uses a mixin, do not use [AnchoredLayerTransformer] -/// in addition. These widgets are designed to be used as an anchored layer only, +/// - If the widget already uses a mixin, do not use [AnchoredLayer] in +/// addition. These widgets are designed to be used as an anchored layer only, /// and need no additional setup. These widgets should contain a notice in the /// documentation. /// {@endtemplate} +@immutable sealed class AnchoredLayer extends Widget { - const AnchoredLayer._(); + /// Transforms the [child] widget into an [AnchoredLayer] + /// + /// Uses an extra widget that mixes in [AnchoredLayerStatelessMixin] + /// internally. Therefore, prefer another transformation method where possible: + /// see documentation on [AnchoredLayer] for more information. + /// + /// --- + /// + /// {@macro anchored_layer_warning} + const factory AnchoredLayer({Key? key, required Widget child}) = + _AnchoredLayerTransformer; } /// Provide an internal detection point for the [AnchoredLayer]s @@ -44,8 +56,7 @@ sealed class AnchoredLayer extends Widget { /// /// Explanation of how [AnchoredLayer]s work internally: /// -/// 1. A [Widget] is converted to an [AnchoredLayer] by means of the mixins, or -/// the [AnchoredLayerTransformer] (which uses the mixins behind the scenes). +/// 1. A [Widget] is transformed into an [AnchoredLayer] by any means. /// 2. The mixin means the affected [Widget] must call the mixin's own `build` /// method, which calls [_detectAncestor]. /// 3. [_detectAncestor] performs a single lookup to the direct ancestor, to @@ -68,8 +79,7 @@ class _AnchoredLayerDetectorAncestor extends StatelessWidget { throw FlutterError.fromParts([ ErrorSummary( - '`AnchoredLayer` (such as `AnchoredLayerTransformer` or one of the ' - 'mixins) was used incorrectly.', + '`AnchoredLayer` (or one of the mixins) was used incorrectly.', ), ErrorDescription( 'Instead of `_AnchoredLayerDetectorAncestor`, the direct ancestor was ' @@ -93,32 +103,11 @@ class _AnchoredLayerDetectorAncestor extends StatelessWidget { } } -/// Transforms the [child] widget into an [AnchoredLayer] -/// -/// Uses a [AnchoredLayerStatelessMixin] internally. -/// -/// {@template anchored_layer_more_info} -/// See [AnchoredLayer] for more information about other methods to create an -/// anchored layer. -/// {@endtemplate} -/// -/// --- -/// -/// {@macro anchored_layer_warning} -class AnchoredLayerTransformer extends StatelessWidget +class _AnchoredLayerTransformer extends StatelessWidget with AnchoredLayerStatelessMixin implements AnchoredLayer { - /// Transforms the [child] widget into an [AnchoredLayer] - /// - /// Uses a [AnchoredLayerStatelessMixin] internally. - /// - /// --- - /// - /// {@macro anchored_layer_warning} - const AnchoredLayerTransformer({ - super.key, - required this.child, - }); + /// Reached externally by the constructor on [AnchoredLayer] + const _AnchoredLayerTransformer({super.key, required this.child}); final Widget child; @@ -133,7 +122,10 @@ class AnchoredLayerTransformer extends StatelessWidget /// /// {@macro anchored_layer_call_super} /// -/// {@macro anchored_layer_more_info} +/// {@template anchored_layer_more_info} +/// See [AnchoredLayer] for more information about other methods to create an +/// anchored layer. +/// {@endtemplate} /// /// --- /// diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index 81c4b5513..ea4cbd561 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -97,8 +97,8 @@ class FlutterMap extends StatefulWidget { /// Same as [children], except these are [AnchoredLayer]s only /// /// These may be any widgets, be that prebuilt layers or custom widgets, but - /// they must also be [AnchoredLayer]s by way of mixin or being wrapped in - /// [AnchoredLayerTransformer]. + /// they must also be [AnchoredLayer]s by way of mixin or being wrapped in an + /// [AnchoredLayer]. /// /// These are overlaid above all normal [children] layers in the order of /// specification. To use an [AnchoredLayer] in a non-overlaid position From 2695b460404e03a0a5bccf90bc9ae70c5f2350cb Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 18 Aug 2023 15:48:36 +0100 Subject: [PATCH 06/14] Fixed issue causing tests to fail --- lib/src/layer/general/translucent_pointer.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/general/translucent_pointer.dart b/lib/src/layer/general/translucent_pointer.dart index e11cbc3cf..9d5de5a5f 100644 --- a/lib/src/layer/general/translucent_pointer.dart +++ b/lib/src/layer/general/translucent_pointer.dart @@ -82,8 +82,10 @@ class RenderTranslucentPointer extends RenderProxyBox { } @override - bool hitTest(BoxHitTestResult result, {required Offset position}) => - !translucent && super.hitTest(result, position: position); + bool hitTest(BoxHitTestResult result, {required Offset position}) { + final hit = super.hitTest(result, position: position); + return !translucent && hit; + } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { From 12c5eb9b10ced2257da4a82c9ec19aaee972aa16 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 18 Aug 2023 16:40:43 +0100 Subject: [PATCH 07/14] Removed unnecessary comparisons Improved internal documentation --- lib/src/map/layers_stack.dart | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/src/map/layers_stack.dart b/lib/src/map/layers_stack.dart index 8448e0282..159da726c 100644 --- a/lib/src/map/layers_stack.dart +++ b/lib/src/map/layers_stack.dart @@ -1,5 +1,7 @@ part of 'widget.dart'; +/// Batches all 'normal' layers into as few Stacks as possible whilst not moving +/// any 'anchored' layers, and applies necessary transformations to the Stacks class _LayersStack extends StatefulWidget { const _LayersStack({ required this.camera, @@ -16,8 +18,6 @@ class _LayersStack extends StatefulWidget { } class _LayersStackState extends State<_LayersStack> { - List children = []; - Iterable _prepareChildren() sync* { final stackChildren = []; @@ -37,28 +37,30 @@ class _LayersStackState extends State<_LayersStack> { } for (final Widget child in widget.children) { - if (child is AnchoredLayerStatefulMixin || - child is AnchoredLayerStatelessMixin) { - if (stackChildren.isNotEmpty) yield prepareRotateStack(); - final overlayChild = _AnchoredLayerDetectorAncestor(child: child); - yield widget.options.applyPointerTranslucencyToLayers - ? TranslucentPointer(child: overlayChild) - : overlayChild; - } else { + if (child is! AnchoredLayer) { stackChildren.add( widget.options.applyPointerTranslucencyToLayers ? TranslucentPointer(child: child) : child, ); + continue; } + + if (stackChildren.isNotEmpty) yield prepareRotateStack(); + final overlayChild = _AnchoredLayerDetectorAncestor(child: child); + yield widget.options.applyPointerTranslucencyToLayers + ? TranslucentPointer(child: overlayChild) + : overlayChild; } if (stackChildren.isNotEmpty) yield prepareRotateStack(); } + List _outputChildren = []; + @override void initState() { super.initState(); - children = _prepareChildren().toList(); + _outputChildren = _prepareChildren().toList(); } @override @@ -68,10 +70,10 @@ class _LayersStackState extends State<_LayersStack> { widget.camera != oldWidget.camera || widget.options.applyPointerTranslucencyToLayers != oldWidget.options.applyPointerTranslucencyToLayers) { - children = _prepareChildren().toList(); + _outputChildren = _prepareChildren().toList(); } } @override - Widget build(BuildContext context) => Stack(children: children); + Widget build(BuildContext context) => Stack(children: _outputChildren); } From 2bde03c67d2a97c8e888d978b2e85f69628dcc45 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 18 Aug 2023 16:41:33 +0100 Subject: [PATCH 08/14] Minor typing improvements --- lib/src/layer/general/anchored_layer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/layer/general/anchored_layer.dart b/lib/src/layer/general/anchored_layer.dart index 7b0673092..0eaf00d25 100644 --- a/lib/src/layer/general/anchored_layer.dart +++ b/lib/src/layer/general/anchored_layer.dart @@ -68,7 +68,7 @@ sealed class AnchoredLayer extends Widget { class _AnchoredLayerDetectorAncestor extends StatelessWidget { const _AnchoredLayerDetectorAncestor({required this.child}); - final Widget child; + final AnchoredLayer child; @override Widget build(BuildContext context) => child; From e752af78eeb3ccb52c8bb4c790b1be03515d1547 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 21 Aug 2023 11:18:47 +0100 Subject: [PATCH 09/14] `AttributionWidget` cleanup and sealing --- lib/flutter_map.dart | 5 +---- .../{ => rich}/animation.dart | 7 +++--- .../attribution_layer/{ => rich}/source.dart | 22 ++++++------------- .../{rich.dart => rich/widget.dart} | 12 ++++------ lib/src/layer/attribution_layer/shared.dart | 21 ++++++++++++++++++ lib/src/layer/attribution_layer/simple.dart | 14 +++--------- lib/src/layer/general/anchored_layer.dart | 1 - .../layer/general/translucent_pointer.dart | 7 ++++-- lib/src/map/widget.dart | 3 +-- 9 files changed, 45 insertions(+), 47 deletions(-) rename lib/src/layer/attribution_layer/{ => rich}/animation.dart (96%) rename lib/src/layer/attribution_layer/{ => rich}/source.dart (83%) rename lib/src/layer/attribution_layer/{rich.dart => rich/widget.dart} (98%) create mode 100644 lib/src/layer/attribution_layer/shared.dart diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 3c5a24822..b7ae9ee9b 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -5,10 +5,7 @@ export 'package:flutter_map/src/geo/latlng_bounds.dart'; export 'package:flutter_map/src/gestures/interactive_flag.dart'; export 'package:flutter_map/src/gestures/map_events.dart'; export 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; -export 'package:flutter_map/src/layer/attribution_layer/animation.dart'; -export 'package:flutter_map/src/layer/attribution_layer/rich.dart'; -export 'package:flutter_map/src/layer/attribution_layer/simple.dart'; -export 'package:flutter_map/src/layer/attribution_layer/source.dart'; +export 'package:flutter_map/src/layer/attribution_layer/shared.dart'; export 'package:flutter_map/src/layer/circle_layer.dart'; export 'package:flutter_map/src/layer/general/translucent_pointer.dart'; export 'package:flutter_map/src/layer/marker_layer.dart'; diff --git a/lib/src/layer/attribution_layer/animation.dart b/lib/src/layer/attribution_layer/rich/animation.dart similarity index 96% rename from lib/src/layer/attribution_layer/animation.dart rename to lib/src/layer/attribution_layer/rich/animation.dart index 6ee4d5fb7..56dcd5348 100644 --- a/lib/src/layer/attribution_layer/animation.dart +++ b/lib/src/layer/attribution_layer/rich/animation.dart @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/src/layer/attribution_layer/rich.dart'; +part of '../shared.dart'; -/// Animation provider base for a [RichAttributionWidget] +/// Animation provider interface for a [RichAttributionWidget] /// /// The popup box's animation is handled/built by [popupAnimationBuilder] for /// full flexibility. @@ -14,7 +13,7 @@ import 'package:flutter_map/src/layer/attribution_layer/rich.dart'; /// [RichAttributionWidgetAnimation], or the prebuilt [FadeRAWA] and /// [ScaleRAWA] animations can be used with limited customization. @immutable -abstract class RichAttributionWidgetAnimation { +abstract interface class RichAttributionWidgetAnimation { /// The duration of the animation used when toggling the state of the /// open/close button /// diff --git a/lib/src/layer/attribution_layer/source.dart b/lib/src/layer/attribution_layer/rich/source.dart similarity index 83% rename from lib/src/layer/attribution_layer/source.dart rename to lib/src/layer/attribution_layer/rich/source.dart index 9b9efd5d7..65092e5ee 100644 --- a/lib/src/layer/attribution_layer/source.dart +++ b/lib/src/layer/attribution_layer/rich/source.dart @@ -1,22 +1,14 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_map/src/layer/attribution_layer/rich.dart'; +part of '../shared.dart'; -/// Base class for attributions that render themselves as widgets -/// -/// Only used by [RichAttributionWidget]. -/// -/// Extended/implemented by [TextSourceAttribution] & [LogoSourceAttribution]. +/// Base class for attributions that render themselves as widgets in a +/// [RichAttributionWidget] /// -/// Avoid manual implementation - unknown subtypes will not be displayed. +/// Extended by [TextSourceAttribution] & [LogoSourceAttribution]. @immutable -abstract class SourceAttribution extends StatelessWidget { - final VoidCallback? _onTap; +sealed class SourceAttribution extends StatelessWidget { + const SourceAttribution._({super.key, VoidCallback? onTap}) : _onTap = onTap; - const SourceAttribution._({ - super.key, - VoidCallback? onTap, - }) : _onTap = onTap; + final VoidCallback? _onTap; Widget _render(BuildContext context); diff --git a/lib/src/layer/attribution_layer/rich.dart b/lib/src/layer/attribution_layer/rich/widget.dart similarity index 98% rename from lib/src/layer/attribution_layer/rich.dart rename to lib/src/layer/attribution_layer/rich/widget.dart index 6e5675c59..16474dbad 100644 --- a/lib/src/layer/attribution_layer/rich.dart +++ b/lib/src/layer/attribution_layer/rich/widget.dart @@ -1,8 +1,4 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; -import 'package:meta/meta.dart'; +part of '../shared.dart'; /// Position to anchor [RichAttributionWidget] to relative to the [FlutterMap] /// @@ -22,7 +18,6 @@ enum AttributionAlignment { const AttributionAlignment(this.real); /// Reflects the standard [Alignment] - @internal final Alignment real; } @@ -65,9 +60,10 @@ enum AttributionAlignment { @immutable class RichAttributionWidget extends StatefulWidget with - AttributionWidget, AnchoredLayerStatefulMixin< - AnchoredLayerStateMixin> { + AnchoredLayerStateMixin> + implements + AttributionWidget { /// List of attributions to display /// /// [TextSourceAttribution]s are shown in a popup box (toggled by a tap/click diff --git a/lib/src/layer/attribution_layer/shared.dart b/lib/src/layer/attribution_layer/shared.dart new file mode 100644 index 000000000..1a4952ac4 --- /dev/null +++ b/lib/src/layer/attribution_layer/shared.dart @@ -0,0 +1,21 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:meta/meta.dart'; + +part 'rich/animation.dart'; +part 'rich/source.dart'; +part 'rich/widget.dart'; +part 'simple.dart'; + +/// Layer widget intended to attribute a source +/// +/// Implemented by [RichAttributionWidget] & [SimpleAttributionWidget]. +/// +/// Has no effect other than as a label to group the provided layers together +/// for the [FlutterMap.simple] constructor. +@immutable +sealed class AttributionWidget extends Widget { + const AttributionWidget._(); +} diff --git a/lib/src/layer/attribution_layer/simple.dart b/lib/src/layer/attribution_layer/simple.dart index a0140ac18..b230315b2 100644 --- a/lib/src/layer/attribution_layer/simple.dart +++ b/lib/src/layer/attribution_layer/simple.dart @@ -1,13 +1,4 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/src/layer/attribution_layer/rich.dart'; -import 'package:flutter_map/src/map/widget.dart'; - -/// Layer widget intended to attribute a source -/// -/// Applied to [RichAttributionWidget] & [SimpleAttributionWidget]. -/// -/// Has no effect, other than as a label to group the provided layers together. -mixin AttributionWidget on Widget {} +part of 'shared.dart'; /// A simple, classic style, attribution layer /// @@ -23,7 +14,8 @@ mixin AttributionWidget on Widget {} /// and has a more complex appearance. @immutable class SimpleAttributionWidget extends StatelessWidget - with AttributionWidget, AnchoredLayerStatelessMixin { + with AnchoredLayerStatelessMixin + implements AttributionWidget { /// Attribution text, such as 'OpenStreetMap contributors' final Text source; diff --git a/lib/src/layer/general/anchored_layer.dart b/lib/src/layer/general/anchored_layer.dart index 0eaf00d25..d85db36a0 100644 --- a/lib/src/layer/general/anchored_layer.dart +++ b/lib/src/layer/general/anchored_layer.dart @@ -30,7 +30,6 @@ part of '../../map/widget.dart'; /// and need no additional setup. These widgets should contain a notice in the /// documentation. /// {@endtemplate} -@immutable sealed class AnchoredLayer extends Widget { /// Transforms the [child] widget into an [AnchoredLayer] /// diff --git a/lib/src/layer/general/translucent_pointer.dart b/lib/src/layer/general/translucent_pointer.dart index 9d5de5a5f..d8cbaf370 100644 --- a/lib/src/layer/general/translucent_pointer.dart +++ b/lib/src/layer/general/translucent_pointer.dart @@ -1,5 +1,8 @@ -// Migrated from https://github.com/spkersten/flutter_transparent_pointer, with -// some API & documentation changes +//////////////////////////////////////////////////////////////// +/// Based on the work by Sander Kersten /// +/// Migrated, and now maintained here for flexibility /// +/// https://github.com/spkersten/flutter_transparent_pointer /// +//////////////////////////////////////////////////////////////// import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index ea4cbd561..f9623899a 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -3,8 +3,7 @@ import 'dart:math'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; -import 'package:flutter_map/src/layer/attribution_layer/rich.dart'; -import 'package:flutter_map/src/layer/attribution_layer/simple.dart'; +import 'package:flutter_map/src/layer/attribution_layer/shared.dart'; import 'package:flutter_map/src/layer/general/translucent_pointer.dart'; import 'package:flutter_map/src/layer/overlay_image_layer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; From 48c0ec61083c4c055fc627b645f70aacbd50d0c3 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 21 Aug 2023 12:31:52 +0100 Subject: [PATCH 10/14] Invert approach to mobile/non-mobile layers Deprecate `nonRotatedChildren` --- example/lib/pages/overlay_image.dart | 77 ++++---- example/lib/pages/plugin_scalebar.dart | 17 +- example/lib/pages/wms_tile_layer.dart | 20 +- .../lib/pages/zoombuttons_plugin_option.dart | 14 +- lib/flutter_map.dart | 3 +- .../layer/attribution_layer/rich/widget.dart | 13 +- lib/src/layer/attribution_layer/simple.dart | 47 ++--- lib/src/layer/circle_layer.dart | 56 +++--- lib/src/layer/general/anchored_layer.dart | 182 ------------------ .../general/mobile_layer_transformer.dart | 26 +++ lib/src/layer/marker_layer.dart | 6 +- lib/src/layer/overlay_image_layer.dart | 166 ++++++++-------- lib/src/layer/{ => polygon_layer}/label.dart | 3 +- .../{ => polygon_layer}/polygon_layer.dart | 15 +- lib/src/layer/polyline_layer.dart | 27 +-- lib/src/layer/tile_layer/tile_layer.dart | 63 +++--- lib/src/map/layers_stack.dart | 79 -------- lib/src/map/widget.dart | 91 ++++----- test/layer/polygon_layer_test.dart | 2 +- test/test_utils/test_app.dart | 2 +- 20 files changed, 348 insertions(+), 561 deletions(-) delete mode 100644 lib/src/layer/general/anchored_layer.dart create mode 100644 lib/src/layer/general/mobile_layer_transformer.dart rename lib/src/layer/{ => polygon_layer}/label.dart (93%) rename lib/src/layer/{ => polygon_layer}/polygon_layer.dart (96%) delete mode 100644 lib/src/map/layers_stack.dart diff --git a/example/lib/pages/overlay_image.dart b/example/lib/pages/overlay_image.dart index f3310238f..9a59083c6 100644 --- a/example/lib/pages/overlay_image.dart +++ b/example/lib/pages/overlay_image.dart @@ -8,30 +8,28 @@ class OverlayImagePage extends StatelessWidget { const OverlayImagePage({Key? key}) : super(key: key); + static final _overlayImages = [ + OverlayImage( + bounds: LatLngBounds( + const LatLng(51.5, -0.09), + const LatLng(48.8566, 2.3522), + ), + opacity: 0.8, + imageProvider: const NetworkImage( + 'https://images.pexels.com/photos/231009/pexels-photo-231009.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=300&w=600'), + ), + const RotatedOverlayImage( + topLeftCorner: LatLng(53.377, -2.999), + bottomLeftCorner: LatLng(52.503, -1.868), + bottomRightCorner: LatLng(53.475, 0.275), + opacity: 0.8, + imageProvider: NetworkImage( + 'https://images.pexels.com/photos/231009/pexels-photo-231009.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=300&w=600'), + ), + ]; + @override Widget build(BuildContext context) { - const topLeftCorner = LatLng(53.377, -2.999); - const bottomRightCorner = LatLng(53.475, 0.275); - const bottomLeftCorner = LatLng(52.503, -1.868); - - final overlayImages = [ - OverlayImage( - bounds: LatLngBounds( - const LatLng(51.5, -0.09), - const LatLng(48.8566, 2.3522), - ), - opacity: 0.8, - imageProvider: const NetworkImage( - 'https://images.pexels.com/photos/231009/pexels-photo-231009.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=300&w=600')), - RotatedOverlayImage( - topLeftCorner: topLeftCorner, - bottomLeftCorner: bottomLeftCorner, - bottomRightCorner: bottomRightCorner, - opacity: 0.8, - imageProvider: const NetworkImage( - 'https://images.pexels.com/photos/231009/pexels-photo-231009.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=300&w=600')), - ]; - return Scaffold( appBar: AppBar(title: const Text('Overlay Image')), drawer: buildDrawer(context, route), @@ -55,21 +53,26 @@ class OverlayImagePage extends StatelessWidget { 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - OverlayImageLayer(overlayImages: overlayImages), - MarkerLayer(markers: [ - Marker( - point: topLeftCorner, - builder: (context) => const _Circle( - color: Colors.redAccent, label: "TL")), - Marker( - point: bottomLeftCorner, - builder: (context) => const _Circle( - color: Colors.redAccent, label: "BL")), - Marker( - point: bottomRightCorner, - builder: (context) => const _Circle( - color: Colors.redAccent, label: "BR")), - ]) + OverlayImageLayer(overlayImages: _overlayImages), + MarkerLayer( + markers: [ + Marker( + point: const LatLng(53.377, -2.999), + builder: (context) => + const _Circle(color: Colors.redAccent, label: "TL"), + ), + Marker( + point: const LatLng(52.503, -1.868), + builder: (context) => + const _Circle(color: Colors.redAccent, label: "BL"), + ), + Marker( + point: const LatLng(53.475, 0.275), + builder: (context) => + const _Circle(color: Colors.redAccent, label: "BR"), + ), + ], + ), ], ), ), diff --git a/example/lib/pages/plugin_scalebar.dart b/example/lib/pages/plugin_scalebar.dart index e3cd4fa04..106c3e896 100644 --- a/example/lib/pages/plugin_scalebar.dart +++ b/example/lib/pages/plugin_scalebar.dart @@ -30,16 +30,13 @@ class PluginScaleBar extends StatelessWidget { 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - // This usage is only for demonstration, prefer a mixin - AnchoredLayer( - child: ScaleLayerWidget( - options: ScaleLayerPluginOption( - lineColor: Colors.blue, - lineWidth: 2, - textStyle: - const TextStyle(color: Colors.blue, fontSize: 12), - padding: const EdgeInsets.all(10), - ), + ScaleLayerWidget( + options: ScaleLayerPluginOption( + lineColor: Colors.blue, + lineWidth: 2, + textStyle: + const TextStyle(color: Colors.blue, fontSize: 12), + padding: const EdgeInsets.all(10), ), ), ], diff --git a/example/lib/pages/wms_tile_layer.dart b/example/lib/pages/wms_tile_layer.dart index 6b0fbcd6c..d3bbdf5c6 100644 --- a/example/lib/pages/wms_tile_layer.dart +++ b/example/lib/pages/wms_tile_layer.dart @@ -28,7 +28,15 @@ class WMSLayerPage extends StatelessWidget { initialCenter: LatLng(42.58, 12.43), initialZoom: 6, ), - overlaidAnchoredChildren: [ + children: [ + TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://{s}.s2maps-tiles.eu/wms/?', + layers: const ['s2cloudless-2021_3857'], + ), + subdomains: const ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ), RichAttributionWidget( popupInitialDisplayDuration: const Duration(seconds: 5), attributions: [ @@ -47,16 +55,6 @@ class WMSLayerPage extends StatelessWidget { ], ), ], - children: [ - TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: 'https://{s}.s2maps-tiles.eu/wms/?', - layers: const ['s2cloudless-2021_3857'], - ), - subdomains: const ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), - ], ), ), ], diff --git a/example/lib/pages/zoombuttons_plugin_option.dart b/example/lib/pages/zoombuttons_plugin_option.dart index 32edfeee3..c3ce34990 100644 --- a/example/lib/pages/zoombuttons_plugin_option.dart +++ b/example/lib/pages/zoombuttons_plugin_option.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/plugin_api.dart'; -class FlutterMapZoomButtons extends StatelessWidget - with AnchoredLayerStatelessMixin { +class FlutterMapZoomButtons extends StatelessWidget { final double minZoom; final double maxZoom; final bool mini; @@ -34,9 +33,8 @@ class FlutterMapZoomButtons extends StatelessWidget @override Widget build(BuildContext context) { - super.build(context); + final camera = MapCamera.of(context); - final map = MapCamera.of(context); return Align( alignment: alignment, child: Column( @@ -51,9 +49,9 @@ class FlutterMapZoomButtons extends StatelessWidget backgroundColor: zoomInColor ?? Theme.of(context).primaryColor, onPressed: () { final paddedMapCamera = CameraFit.bounds( - bounds: map.visibleBounds, + bounds: camera.visibleBounds, padding: _fitBoundsPadding, - ).fit(map); + ).fit(camera); var zoom = paddedMapCamera.zoom + 1; if (zoom > maxZoom) { zoom = maxZoom; @@ -72,9 +70,9 @@ class FlutterMapZoomButtons extends StatelessWidget backgroundColor: zoomOutColor ?? Theme.of(context).primaryColor, onPressed: () { final paddedMapCamera = CameraFit.bounds( - bounds: map.visibleBounds, + bounds: camera.visibleBounds, padding: _fitBoundsPadding, - ).fit(map); + ).fit(camera); var zoom = paddedMapCamera.zoom - 1; if (zoom < minZoom) { zoom = minZoom; diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index b7ae9ee9b..180c3534a 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -7,10 +7,11 @@ export 'package:flutter_map/src/gestures/map_events.dart'; export 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; export 'package:flutter_map/src/layer/attribution_layer/shared.dart'; export 'package:flutter_map/src/layer/circle_layer.dart'; +export 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; export 'package:flutter_map/src/layer/general/translucent_pointer.dart'; export 'package:flutter_map/src/layer/marker_layer.dart'; export 'package:flutter_map/src/layer/overlay_image_layer.dart'; -export 'package:flutter_map/src/layer/polygon_layer.dart'; +export 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; export 'package:flutter_map/src/layer/polyline_layer.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_builder.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; diff --git a/lib/src/layer/attribution_layer/rich/widget.dart b/lib/src/layer/attribution_layer/rich/widget.dart index 16474dbad..637d256fe 100644 --- a/lib/src/layer/attribution_layer/rich/widget.dart +++ b/lib/src/layer/attribution_layer/rich/widget.dart @@ -59,11 +59,7 @@ enum AttributionAlignment { /// {@endtemplate} @immutable class RichAttributionWidget extends StatefulWidget - with - AnchoredLayerStatefulMixin< - AnchoredLayerStateMixin> - implements - AttributionWidget { + implements AttributionWidget { /// List of attributions to display /// /// [TextSourceAttribution]s are shown in a popup box (toggled by a tap/click @@ -132,11 +128,10 @@ class RichAttributionWidget extends StatefulWidget }); @override - AnchoredLayerStateMixin createState() => _RichAttributionWidgetState(); + State createState() => _RichAttributionWidgetState(); } -class _RichAttributionWidgetState extends State - with AnchoredLayerStateMixin { +class _RichAttributionWidgetState extends State { StreamSubscription? mapEventSubscription; final persistentAttributionKey = GlobalKey(); @@ -183,8 +178,6 @@ class _RichAttributionWidgetState extends State @override Widget build(BuildContext context) { - super.build(context); - final persistentAttributionItems = [ ...List.from( widget.attributions.whereType(), diff --git a/lib/src/layer/attribution_layer/simple.dart b/lib/src/layer/attribution_layer/simple.dart index b230315b2..804a29931 100644 --- a/lib/src/layer/attribution_layer/simple.dart +++ b/lib/src/layer/attribution_layer/simple.dart @@ -14,7 +14,6 @@ part of 'shared.dart'; /// and has a more complex appearance. @immutable class SimpleAttributionWidget extends StatelessWidget - with AnchoredLayerStatelessMixin implements AttributionWidget { /// Attribution text, such as 'OpenStreetMap contributors' final Text source; @@ -44,32 +43,28 @@ class SimpleAttributionWidget extends StatelessWidget }); @override - Widget build(BuildContext context) { - super.build(context); - - return Align( - alignment: alignment, - child: ColoredBox( - color: backgroundColor ?? Theme.of(context).colorScheme.background, - child: GestureDetector( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(3), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('flutter_map | © '), - MouseRegion( - cursor: onTap == null - ? MouseCursor.defer - : SystemMouseCursors.click, - child: source, - ), - ], + Widget build(BuildContext context) => Align( + alignment: alignment, + child: ColoredBox( + color: backgroundColor ?? Theme.of(context).colorScheme.background, + child: GestureDetector( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(3), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('flutter_map | © '), + MouseRegion( + cursor: onTap == null + ? MouseCursor.defer + : SystemMouseCursors.click, + child: source, + ), + ], + ), ), ), ), - ), - ); - } + ); } diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index f4ea50ad6..e1eabc4f0 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:latlong2/latlong.dart' hide Path; @@ -26,37 +27,40 @@ class CircleMarker { @immutable class CircleLayer extends StatelessWidget { + static const _distance = Distance(); + final List circles; - const CircleLayer({super.key, this.circles = const []}); + const CircleLayer({super.key, required this.circles}); @override Widget build(BuildContext context) { - const distance = Distance(); - return LayoutBuilder( - builder: (context, bc) { - final size = Size(bc.maxWidth, bc.maxHeight); - final map = MapCamera.of(context); - final circleWidgets = circles.map((circle) { - final offset = map.getOffsetFromOrigin(circle.point); - double? realRadius; - if (circle.useRadiusInMeter) { - final r = distance.offset(circle.point, circle.radius, 180); - final delta = offset - map.getOffsetFromOrigin(r); - realRadius = delta.distance; - } - return CustomPaint( - key: circle.key, - painter: CirclePainter( - circle, - offset: offset, - radius: realRadius ?? 0, - ), - size: size, - ); - }).toList(growable: false); - return Stack(children: circleWidgets); - }, + return MobileLayerTransformer( + child: LayoutBuilder( + builder: (context, bc) { + final size = Size(bc.maxWidth, bc.maxHeight); + final map = MapCamera.of(context); + final circleWidgets = circles.map((circle) { + final offset = map.getOffsetFromOrigin(circle.point); + double? realRadius; + if (circle.useRadiusInMeter) { + final r = _distance.offset(circle.point, circle.radius, 180); + final delta = offset - map.getOffsetFromOrigin(r); + realRadius = delta.distance; + } + return CustomPaint( + key: circle.key, + painter: CirclePainter( + circle, + offset: offset, + radius: realRadius ?? 0, + ), + size: size, + ); + }).toList(growable: false); + return Stack(children: circleWidgets); + }, + ), ); } } diff --git a/lib/src/layer/general/anchored_layer.dart b/lib/src/layer/general/anchored_layer.dart deleted file mode 100644 index d85db36a0..000000000 --- a/lib/src/layer/general/anchored_layer.dart +++ /dev/null @@ -1,182 +0,0 @@ -part of '../../map/widget.dart'; - -/// A layer that is anchored to the map, that does not move with the other layers -/// -/// There are multiple ways to transform a widget into an anchored layer. In -/// order of preference: -/// -/// 1. Apply [AnchoredLayerStatelessMixin] to a [StatelessWidget] -/// 2. Apply [AnchoredLayerStatefulMixin] to a [StatefulWidget], and -/// [AnchoredLayerStateMixin] to its corresponding [State] -/// 3. Wrap the widget with an [AnchoredLayer] (which uses an extra widget that -/// mixes in [AnchoredLayerStatelessMixin]) -/// -/// This layer may be used in both [FlutterMap.children] and -/// [FlutterMap.overlaidAnchoredChildren]. See documentation on those properties -/// for more information. -/// -/// {@template anchored_layer_warning} -/// [AnchoredLayer]s must be on the top-level of [FlutterMap.children] or -/// [FlutterMap.overlaidAnchoredChildren]. They must also not be multiplied as an -/// ancestor or child: use only one. Failure to do this will throw an error, as -/// the anchored effect will not be correctly applied. See below for common -/// mistakes and their resolutions: -/// -/// - If you have control over the widget, do not use [AnchoredLayer] -/// in its `build` method. Prefer using the appropriate mixin, or wrap the -/// transformer around every instance of the widget. -/// - If the widget already uses a mixin, do not use [AnchoredLayer] in -/// addition. These widgets are designed to be used as an anchored layer only, -/// and need no additional setup. These widgets should contain a notice in the -/// documentation. -/// {@endtemplate} -sealed class AnchoredLayer extends Widget { - /// Transforms the [child] widget into an [AnchoredLayer] - /// - /// Uses an extra widget that mixes in [AnchoredLayerStatelessMixin] - /// internally. Therefore, prefer another transformation method where possible: - /// see documentation on [AnchoredLayer] for more information. - /// - /// --- - /// - /// {@macro anchored_layer_warning} - const factory AnchoredLayer({Key? key, required Widget child}) = - _AnchoredLayerTransformer; -} - -/// Provide an internal detection point for the [AnchoredLayer]s -/// -/// Although any other widget could be used as the detection point, this is -/// provided as close as possible to the mixin-ed widgets to allow only one -/// `context.visitAncestorElements` iteration to determine whether usage is -/// correct. -/// -/// --- -/// -/// Explanation of how [AnchoredLayer]s work internally: -/// -/// 1. A [Widget] is transformed into an [AnchoredLayer] by any means. -/// 2. The mixin means the affected [Widget] must call the mixin's own `build` -/// method, which calls [_detectAncestor]. -/// 3. [_detectAncestor] performs a single lookup to the direct ancestor, to -/// check whether it is an [_AnchoredLayerDetectorAncestor], throwing if it -/// isn't, because it will not have the next step applied -/// 4. When laying out the layers, [_LayersStack] avoids applying rotation to -/// any widget that has mixed-in one of the mixins, whilst the layer itself -/// doesn't follow the map movement - resulting in an 'anchored' layer. -class _AnchoredLayerDetectorAncestor extends StatelessWidget { - const _AnchoredLayerDetectorAncestor({required this.child}); - - final AnchoredLayer child; - - @override - Widget build(BuildContext context) => child; - - static Widget _detectAncestor(BuildContext context) { - context.visitAncestorElements((e) { - if (e.widget is _AnchoredLayerDetectorAncestor) return false; - - throw FlutterError.fromParts([ - ErrorSummary( - '`AnchoredLayer` (or one of the mixins) was used incorrectly.', - ), - ErrorDescription( - 'Instead of `_AnchoredLayerDetectorAncestor`, the direct ancestor was ' - 'of type `${e.widget.runtimeType}`.', - ), - ErrorHint( - 'Ensure that there is only one `AnchoredLayer`, and that it is a ' - 'top-level widget of `children` or `overlaidAnchoredChildren`. Read ' - 'the documentation on `AnchoredLayer` for more information.', - ) - ]); - }); - - // The user shouldn't build the output of this method - return Builder( - builder: (_) => throw FlutterError( - 'Widgets that mixin `AnchoredLayer*Mixin` must call ' - '`super.build(context)`, but must also ignore the return value', - ), - ); - } -} - -class _AnchoredLayerTransformer extends StatelessWidget - with AnchoredLayerStatelessMixin - implements AnchoredLayer { - /// Reached externally by the constructor on [AnchoredLayer] - const _AnchoredLayerTransformer({super.key, required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - super.build(context); - return child; - } -} - -/// Apply to a [StatelessWidget] to transform it into an [AnchoredLayer] -/// -/// {@macro anchored_layer_call_super} -/// -/// {@template anchored_layer_more_info} -/// See [AnchoredLayer] for more information about other methods to create an -/// anchored layer. -/// {@endtemplate} -/// -/// --- -/// -/// {@macro anchored_layer_warning} -mixin AnchoredLayerStatelessMixin on StatelessWidget implements AnchoredLayer { - @override - @mustCallSuper - Widget build(BuildContext context) => - _AnchoredLayerDetectorAncestor._detectAncestor(context); -} - -/// Apply to a [StatefulWidget] to transform it into an [AnchoredLayer] -/// -/// Must be paired with an [AnchoredLayerStateMixin] on the [State], and this -/// object's [createState] method must return an instance of -/// [AnchoredLayerStateMixin]. See [RichAttributionWidget] for an example of -/// this. -/// -/// {@template anchored_layer_call_super} -/// Always call `super.build(context)` from within the widget's `build` method, -/// but ignore its result and build children as normal. -/// {@endtemplate} -/// -/// {@macro anchored_layer_more_info} -/// -/// --- -/// -/// {@macro anchored_layer_warning} -mixin AnchoredLayerStatefulMixin>> - on StatefulWidget implements AnchoredLayer { - @override - AnchoredLayerStateMixin createState(); -} - -/// Apply to a [State] to transform its corresponding [StatefulWidget] into an -/// [AnchoredLayer] -/// -/// Must be paired with an [AnchoredLayerStatefulMixin] on the [State]. See -/// [RichAttributionWidget] for an example of this. -/// -/// {@macro anchored_layer_call_super} -/// -/// {@macro anchored_layer_more_info} -/// -/// --- -/// -/// {@macro anchored_layer_warning} -mixin AnchoredLayerStateMixin< - T extends AnchoredLayerStatefulMixin>> - on State { - @override - @mustCallSuper - Widget build(BuildContext context) => - _AnchoredLayerDetectorAncestor._detectAncestor(context); -} diff --git a/lib/src/layer/general/mobile_layer_transformer.dart b/lib/src/layer/general/mobile_layer_transformer.dart new file mode 100644 index 000000000..d56184239 --- /dev/null +++ b/lib/src/layer/general/mobile_layer_transformer.dart @@ -0,0 +1,26 @@ +import 'package:flutter/widgets.dart'; + +import 'package:flutter_map/src/map/camera/camera.dart'; + +/// Transforms a [child] widget tree into a layer that can move and rotate based +/// on the [MapCamera] +class MobileLayerTransformer extends StatelessWidget { + /// Transforms a [child] widget tree into a layer that can move and rotate based + /// on the [MapCamera] + const MobileLayerTransformer({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final camera = MapCamera.of(context); + + return OverflowBox( + minWidth: camera.size.x, + maxWidth: camera.size.x, + minHeight: camera.size.y, + maxHeight: camera.size.y, + child: Transform.rotate(angle: camera.rotationRad, child: child), + ); + } +} diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index 3e9faf78d..ca6ae6c3d 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; @@ -181,7 +182,7 @@ class MarkerLayer extends StatelessWidget { const MarkerLayer({ super.key, - this.markers = const [], + required this.markers, this.anchorPos, this.rotate = false, this.rotateOrigin, @@ -238,6 +239,7 @@ class MarkerLayer extends StatelessWidget { ), ); } - return Stack(children: markerWidgets); + + return MobileLayerTransformer(child: Stack(children: markerWidgets)); } } diff --git a/lib/src/layer/overlay_image_layer.dart b/lib/src/layer/overlay_image_layer.dart index 2bc7abd6e..220ee7be2 100644 --- a/lib/src/layer/overlay_image_layer.dart +++ b/lib/src/layer/overlay_image_layer.dart @@ -1,5 +1,7 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; @@ -7,24 +9,37 @@ import 'package:latlong2/latlong.dart'; /// Base class for all overlay images. @immutable -abstract class BaseOverlayImage { - ImageProvider get imageProvider; - - double get opacity; +sealed class BaseOverlayImage extends StatelessWidget { + final ImageProvider imageProvider; + final double opacity; + final bool gaplessPlayback; - bool get gaplessPlayback; + const BaseOverlayImage({ + super.key, + required this.imageProvider, + this.opacity = 1, + this.gaplessPlayback = false, + }); - Positioned buildPositionedForOverlay(MapCamera map); + Widget _render( + BuildContext context, { + required Image child, + required MapCamera camera, + }); - Image buildImageForOverlay() { - return Image( - image: imageProvider, - fit: BoxFit.fill, - color: Color.fromRGBO(255, 255, 255, opacity), - colorBlendMode: BlendMode.modulate, - gaplessPlayback: gaplessPlayback, - ); - } + @override + @nonVirtual + Widget build(BuildContext context) => _render( + context, + child: Image( + image: imageProvider, + fit: BoxFit.fill, + color: Color.fromRGBO(255, 255, 255, opacity), + colorBlendMode: BlendMode.modulate, + gaplessPlayback: gaplessPlayback, + ), + camera: MapCamera.of(context), + ); } /// Unrotated overlay image that spans between a given bounding box. @@ -34,32 +49,34 @@ abstract class BaseOverlayImage { @immutable class OverlayImage extends BaseOverlayImage { final LatLngBounds bounds; - @override - final ImageProvider imageProvider; - @override - final double opacity; - @override - final bool gaplessPlayback; - OverlayImage( - {required this.bounds, - required this.imageProvider, - this.opacity = 1.0, - this.gaplessPlayback = false}); + const OverlayImage({ + super.key, + required super.imageProvider, + required this.bounds, + super.opacity, + super.gaplessPlayback, + }); @override - Positioned buildPositionedForOverlay(MapCamera map) { + Widget _render( + BuildContext context, { + required Image child, + required MapCamera camera, + }) { // northWest is not necessarily upperLeft depending on projection - final bounds = Bounds( - map.project(this.bounds.northWest).subtract(map.pixelOrigin), - map.project(this.bounds.southEast).subtract(map.pixelOrigin), + final bounds = Bounds( + camera.project(this.bounds.northWest).subtract(camera.pixelOrigin), + camera.project(this.bounds.southEast).subtract(camera.pixelOrigin), ); + return Positioned( - left: bounds.topLeft.x.toDouble(), - top: bounds.topLeft.y.toDouble(), - width: bounds.size.x.toDouble(), - height: bounds.size.y.toDouble(), - child: buildImageForOverlay()); + left: bounds.topLeft.x, + top: bounds.topLeft.y, + width: bounds.size.x, + height: bounds.size.y, + child: child, + ); } } @@ -72,45 +89,41 @@ class OverlayImage extends BaseOverlayImage { /// corner point is derived from the other points. @immutable class RotatedOverlayImage extends BaseOverlayImage { - @override - final ImageProvider imageProvider; - final LatLng topLeftCorner; final LatLng bottomLeftCorner; final LatLng bottomRightCorner; - - @override - final double opacity; - - @override - final bool gaplessPlayback; - - /// The filter quality when rotating the image. final FilterQuality? filterQuality; - RotatedOverlayImage( - {required this.imageProvider, - required this.topLeftCorner, - required this.bottomLeftCorner, - required this.bottomRightCorner, - this.opacity = 1.0, - this.gaplessPlayback = false, - this.filterQuality = FilterQuality.medium}); + const RotatedOverlayImage({ + super.key, + required super.imageProvider, + required this.topLeftCorner, + required this.bottomLeftCorner, + required this.bottomRightCorner, + this.filterQuality = FilterQuality.medium, + super.opacity, + super.gaplessPlayback, + }); @override - Positioned buildPositionedForOverlay(MapCamera map) { - final pxTopLeft = map.project(topLeftCorner).subtract(map.pixelOrigin); + Widget _render( + BuildContext context, { + required Image child, + required MapCamera camera, + }) { + final pxTopLeft = + camera.project(topLeftCorner).subtract(camera.pixelOrigin); final pxBottomRight = - map.project(bottomRightCorner).subtract(map.pixelOrigin); + camera.project(bottomRightCorner).subtract(camera.pixelOrigin); final pxBottomLeft = - map.project(bottomLeftCorner).subtract(map.pixelOrigin); + camera.project(bottomLeftCorner).subtract(camera.pixelOrigin); /// calculate pixel coordinate of top-right corner by calculating the /// vector from bottom-left to top-left and adding it to bottom-right final pxTopRight = pxTopLeft - pxBottomLeft + pxBottomRight; /// update/enlarge bounds so the new corner points fit within - final bounds = Bounds(pxTopLeft, pxBottomRight) + final bounds = Bounds(pxTopLeft, pxBottomRight) .extend(pxTopRight) .extend(pxBottomLeft); @@ -126,15 +139,16 @@ class RotatedOverlayImage extends BaseOverlayImage { final ty = offset.y; return Positioned( - left: bounds.topLeft.x.toDouble(), - top: bounds.topLeft.y.toDouble(), - width: bounds.size.x.toDouble(), - height: bounds.size.y.toDouble(), - child: Transform( - transform: - Matrix4(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1), - filterQuality: filterQuality, - child: buildImageForOverlay())); + left: bounds.topLeft.x, + top: bounds.topLeft.y, + width: bounds.size.x, + height: bounds.size.y, + child: Transform( + transform: Matrix4(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1), + filterQuality: filterQuality, + child: child, + ), + ); } } @@ -142,18 +156,10 @@ class RotatedOverlayImage extends BaseOverlayImage { class OverlayImageLayer extends StatelessWidget { final List overlayImages; - const OverlayImageLayer({super.key, this.overlayImages = const []}); + const OverlayImageLayer({super.key, required this.overlayImages}); @override - Widget build(BuildContext context) { - final map = MapCamera.of(context); - return ClipRect( - child: Stack( - children: [ - for (final overlayImage in overlayImages) - overlayImage.buildPositionedForOverlay(map), - ], - ), - ); - } + Widget build(BuildContext context) => MobileLayerTransformer( + child: ClipRect(child: Stack(children: overlayImages)), + ); } diff --git a/lib/src/layer/label.dart b/lib/src/layer/polygon_layer/label.dart similarity index 93% rename from lib/src/layer/label.dart rename to lib/src/layer/polygon_layer/label.dart index 7e3e43718..d0efc8cd1 100644 --- a/lib/src/layer/label.dart +++ b/lib/src/layer/polygon_layer/label.dart @@ -2,7 +2,8 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; +import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:polylabel/polylabel.dart'; void Function(Canvas canvas)? buildLabelTextPainter({ diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart similarity index 96% rename from lib/src/layer/polygon_layer.dart rename to lib/src/layer/polygon_layer/polygon_layer.dart index 0e9a992b0..8b6410151 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -2,7 +2,8 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/layer/label.dart'; +import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; +import 'package:flutter_map/src/layer/polygon_layer/label.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI @@ -96,7 +97,7 @@ class PolygonLayer extends StatelessWidget { const PolygonLayer({ super.key, - this.polygons = const [], + required this.polygons, this.polygonCulling = false, this.polygonLabels = true, }); @@ -112,10 +113,12 @@ class PolygonLayer extends StatelessWidget { }).toList() : polygons; - return CustomPaint( - painter: PolygonPainter(pgons, map, polygonLabels), - size: size, - isComplex: true, + return MobileLayerTransformer( + child: CustomPaint( + painter: PolygonPainter(pgons, map, polygonLabels), + size: size, + isComplex: true, + ), ); } } diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index bde513217..46e661355 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -2,8 +2,7 @@ import 'dart:core'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/plugin_api.dart'; import 'package:latlong2/latlong.dart'; class Polyline { @@ -59,7 +58,7 @@ class PolylineLayer extends StatelessWidget { const PolylineLayer({ super.key, - this.polylines = const [], + required this.polylines, this.polylineCulling = false, }); @@ -67,17 +66,19 @@ class PolylineLayer extends StatelessWidget { Widget build(BuildContext context) { final map = MapCamera.of(context); - return CustomPaint( - painter: PolylinePainter( - polylineCulling - ? polylines - .where((p) => p.boundingBox.isOverlapping(map.visibleBounds)) - .toList() - : polylines, - map, + return MobileLayerTransformer( + child: CustomPaint( + painter: PolylinePainter( + polylineCulling + ? polylines + .where((p) => p.boundingBox.isOverlapping(map.visibleBounds)) + .toList() + : polylines, + map, + ), + size: Size(map.size.x, map.size.y), + isComplex: true, ), - size: Size(map.size.x, map.size.y), - isComplex: true, ); } } diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index c37e7238d..698bd1234 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -4,14 +4,28 @@ import 'dart:math' show Point; import 'package:collection/collection.dart' show MapEquality; import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/src/geo/crs.dart'; +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_builder.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_display.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_image.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_image_manager.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_provider/base_tile_provider.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_provider/network_tile_provider.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_range_calculator.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_scale_calculator.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_update_event.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_update_transformer.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/src/map/controller/map_controller.dart'; +import 'package:flutter_map/src/misc/point_extensions.dart'; +import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:flutter_map/src/misc/private/util.dart' as util; import 'package:http/retry.dart'; @@ -488,35 +502,36 @@ class _TileLayerState extends State with TickerProviderStateMixin { _tileScaleCalculator.clearCacheUnlessZoomMatches(map.zoom); - return _addBackgroundColor( - Stack( - children: [ - ..._tileImageManager - .inRenderOrder(widget.maxZoom, tileZoom) - .map((tileImage) { - return Tile( - // Must be an ObjectKey, not a ValueKey using the coordinates, in - // case we remove and replace the TileImage with a different one. - key: ObjectKey(tileImage), - scaledTileSize: _tileScaleCalculator.scaledTileSize( - map.zoom, - tileImage.coordinates.z, - ), - currentPixelOrigin: currentPixelOrigin, - tileImage: tileImage, - tileBuilder: widget.tileBuilder, - ); - }), - ], + return MobileLayerTransformer( + // ignore: deprecated_member_use_from_same_package + child: _addBackgroundColor( + Stack( + children: [ + ..._tileImageManager + .inRenderOrder(widget.maxZoom, tileZoom) + .map((tileImage) { + return Tile( + // Must be an ObjectKey, not a ValueKey using the coordinates, in + // case we remove and replace the TileImage with a different one. + key: ObjectKey(tileImage), + scaledTileSize: _tileScaleCalculator.scaledTileSize( + map.zoom, + tileImage.coordinates.z, + ), + currentPixelOrigin: currentPixelOrigin, + tileImage: tileImage, + tileBuilder: widget.tileBuilder, + ); + }), + ], + ), ), ); } - /// This can be removed once the deprecated backgroundColor option is removed. + @Deprecated('Remove once `backgroundColor` is removed') Widget _addBackgroundColor(Widget child) { - // ignore: deprecated_member_use_from_same_package final color = widget.backgroundColor; - return color == null ? child : ColoredBox(color: color, child: child); } diff --git a/lib/src/map/layers_stack.dart b/lib/src/map/layers_stack.dart deleted file mode 100644 index 159da726c..000000000 --- a/lib/src/map/layers_stack.dart +++ /dev/null @@ -1,79 +0,0 @@ -part of 'widget.dart'; - -/// Batches all 'normal' layers into as few Stacks as possible whilst not moving -/// any 'anchored' layers, and applies necessary transformations to the Stacks -class _LayersStack extends StatefulWidget { - const _LayersStack({ - required this.camera, - required this.options, - required this.children, - }); - - final MapCamera camera; - final MapOptions options; - final List children; - - @override - State<_LayersStack> createState() => _LayersStackState(); -} - -class _LayersStackState extends State<_LayersStack> { - Iterable _prepareChildren() sync* { - final stackChildren = []; - - Widget prepareRotateStack() { - final box = OverflowBox( - minWidth: widget.camera.size.x, - maxWidth: widget.camera.size.x, - minHeight: widget.camera.size.y, - maxHeight: widget.camera.size.y, - child: Transform.rotate( - angle: widget.camera.rotationRad, - child: Stack(children: List.of(stackChildren)), - ), - ); - stackChildren.clear(); - return box; - } - - for (final Widget child in widget.children) { - if (child is! AnchoredLayer) { - stackChildren.add( - widget.options.applyPointerTranslucencyToLayers - ? TranslucentPointer(child: child) - : child, - ); - continue; - } - - if (stackChildren.isNotEmpty) yield prepareRotateStack(); - final overlayChild = _AnchoredLayerDetectorAncestor(child: child); - yield widget.options.applyPointerTranslucencyToLayers - ? TranslucentPointer(child: overlayChild) - : overlayChild; - } - if (stackChildren.isNotEmpty) yield prepareRotateStack(); - } - - List _outputChildren = []; - - @override - void initState() { - super.initState(); - _outputChildren = _prepareChildren().toList(); - } - - @override - void didUpdateWidget(covariant _LayersStack oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.children != oldWidget.children || - widget.camera != oldWidget.camera || - widget.options.applyPointerTranslucencyToLayers != - oldWidget.options.applyPointerTranslucencyToLayers) { - _outputChildren = _prepareChildren().toList(); - } - } - - @override - Widget build(BuildContext context) => Stack(children: _outputChildren); -} diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index f9623899a..ec34ab2c1 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -1,13 +1,14 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:math'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/layer/attribution_layer/shared.dart'; +import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/general/translucent_pointer.dart'; -import 'package:flutter_map/src/layer/overlay_image_layer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; -import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/map/controller/impl.dart'; import 'package:flutter_map/src/map/controller/internal.dart'; @@ -15,9 +16,6 @@ import 'package:flutter_map/src/map/controller/map_controller.dart'; import 'package:flutter_map/src/map/inherited_model.dart'; import 'package:flutter_map/src/map/options.dart'; -part '../layer/general/anchored_layer.dart'; -part 'layers_stack.dart'; - /// An interactive geographical map /// /// See the online documentation for more information about set-up, @@ -33,7 +31,13 @@ class FlutterMap extends StatefulWidget { this.mapController, required this.options, required this.children, - this.overlaidAnchoredChildren = const [], + @Deprecated( + 'Append all of these children to `children`. ' + 'This property has been removed to simplify the way layers are inserted ' + 'into the map, and allow for greater flexibility of layer positioning. ' + 'This property is deprecated since v6.', + ) + this.nonRotatedChildren = const [], }); /// Creates an interactive geographical map @@ -75,42 +79,33 @@ class FlutterMap extends StatefulWidget { if (attribution != null) attribution, ], mapController = null, - overlaidAnchoredChildren = []; + nonRotatedChildren = []; - /// Layer widgets to be placed onto the map in a [Stack]-like fashion - /// - /// These may be any widgets, be that prebuilt layers, [AnchoredLayer]s, or - /// custom widgets. - /// - /// --- - /// - /// Note that using [AnchoredLayer]s at the end of this list is equivalent to - /// using them in [overlaidAnchoredChildren] instead. + /// Widgets to be placed onto the map in a [Stack]-like fashion /// - /// When inserting [AnchoredLayer]s inbetween children (ie. not at the end), - /// there is likely to be a very small performance penalty. + /// Widgets that use [MobileLayerTransformer] will be mobile, will move and + /// rotate with the map. Other widgets will be static (and should usually use + /// [Align] or another method to position themselves). /// - /// {@macro anchored_layer_warning} + /// [TranslucentPointer] will be wrapped around each child by default, unless + /// [MapOptions.applyPointerTranslucencyToLayers] is `false`. final List children; - /// Same as [children], except these are [AnchoredLayer]s only + /// This member has been deprecated as of v6, and will be removed in the next + /// version. /// - /// These may be any widgets, be that prebuilt layers or custom widgets, but - /// they must also be [AnchoredLayer]s by way of mixin or being wrapped in an - /// [AnchoredLayer]. - /// - /// These are overlaid above all normal [children] layers in the order of - /// specification. To use an [AnchoredLayer] in a non-overlaid position - /// instead, insert it directly into [children]. - /// - /// See [AnchoredLayer] for more information. - /// - /// Not to be confused with [OverlayImageLayer]. - /// - /// --- + /// To migrate, append all of these children to [children]. In most cases, no + /// other migration will be necessary. /// - /// {@macro anchored_layer_warning} - final List overlaidAnchoredChildren; + /// This will simplify the way layers are inserted into the map, and allow for + /// greater flexibility of layer positioning. + @Deprecated( + 'Append all of these children to `children`. ' + 'This property has been removed to simplify the way layers are inserted ' + 'into the map, and allow for greater flexibility of layer positioning. ' + 'This property is deprecated since v6.', + ) + final List nonRotatedChildren; /// Configure this map's permanent rules and initial state /// @@ -183,14 +178,24 @@ class _FlutterMapStateContainer extends State { options: options, camera: camera, child: ClipRect( - child: ColoredBox( - color: options.backgroundColor, - child: _LayersStack( - camera: camera, - options: options, - children: widget.children - ..addAll(widget.overlaidAnchoredChildren), - ), + child: Stack( + children: [ + Positioned.fill( + child: ColoredBox(color: options.backgroundColor), + ), + ...widget.children.map( + (child) => TranslucentPointer( + translucent: options.applyPointerTranslucencyToLayers, + child: child, + ), + ), + ...widget.nonRotatedChildren.map( + (child) => TranslucentPointer( + translucent: options.applyPointerTranslucencyToLayers, + child: child, + ), + ), + ], ), ), ), diff --git a/test/layer/polygon_layer_test.dart b/test/layer/polygon_layer_test.dart index d59983624..73010c7de 100644 --- a/test/layer/polygon_layer_test.dart +++ b/test/layer/polygon_layer_test.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/src/layer/polygon_layer.dart'; +import 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; import 'package:flutter_map/src/map/widget.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; diff --git a/test/test_utils/test_app.dart b/test/test_utils/test_app.dart index acfc2bed3..eeb722745 100644 --- a/test/test_utils/test_app.dart +++ b/test/test_utils/test_app.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/src/layer/circle_layer.dart'; import 'package:flutter_map/src/layer/marker_layer.dart'; -import 'package:flutter_map/src/layer/polygon_layer.dart'; +import 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; import 'package:flutter_map/src/layer/polyline_layer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; import 'package:flutter_map/src/map/controller/map_controller.dart'; From 762d435315ff39f600252219dd7a842bd0b460c5 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 21 Aug 2023 13:06:20 +0100 Subject: [PATCH 11/14] Fixed bugs Removed flaky(?) test --- lib/src/map/controller/map_controller.dart | 7 ++++--- lib/src/map/options.dart | 7 ++++++- test/flutter_map_test.dart | 6 ------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/src/map/controller/map_controller.dart b/lib/src/map/controller/map_controller.dart index 80a8f5c5d..394a4459e 100644 --- a/lib/src/map/controller/map_controller.dart +++ b/lib/src/map/controller/map_controller.dart @@ -8,6 +8,7 @@ import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/map/controller/impl.dart'; import 'package:flutter_map/src/map/inherited_model.dart'; +import 'package:flutter_map/src/map/widget.dart'; import 'package:flutter_map/src/misc/center_zoom.dart'; import 'package:flutter_map/src/misc/fit_bounds_options.dart'; import 'package:flutter_map/src/misc/move_and_rotate_result.dart'; @@ -140,9 +141,9 @@ abstract class MapController { /// documentation. bool fitCamera(CameraFit cameraFit); - /// Current [MapCamera]. Accessing the camera from this getter is an - /// anti-pattern. It is preferable to use [MapCamera.of(context)] in a child - /// widget of FlutterMap. + /// Access the current [MapCamera] + /// + /// From inside a [FlutterMap]'s [BuildContext], prefer using [MapCamera.of]. MapCamera get camera; /// Move and zoom the map to perfectly fit [bounds], with additional diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index b82bdaa3a..f5ef843c6 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -334,7 +334,10 @@ class MapOptions { onMapReady == other.onMapReady && maxBounds == other.maxBounds && keepAlive == other.keepAlive && - interactionOptions == other.interactionOptions; + interactionOptions == other.interactionOptions && + backgroundColor == other.backgroundColor && + applyPointerTranslucencyToLayers == + other.applyPointerTranslucencyToLayers; @override int get hashCode => Object.hashAll([ @@ -361,6 +364,8 @@ class MapOptions { keepAlive, maxBounds, interactionOptions, + backgroundColor, + applyPointerTranslucencyToLayers, ]); } diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index 4b2d904ba..13d979941 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/plugin_api.dart'; -import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; @@ -104,13 +103,8 @@ void main() { ), ), ); - - // Check that taps are still received. await tester.pumpWidget(map); expect(taps, 0); - await tester.tap(find.byType(FlutterMap)); - await tester.pumpAndSettle(FlutterMapInteractiveViewerState.doubleTapDelay); - expect(taps, 1); // Store the camera before pinch zooming. final cameraBeforePinchZoom = camera; From 8ab2d6c84eff6c5dea2e06979983a3de46e354f8 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 21 Aug 2023 13:19:22 +0100 Subject: [PATCH 12/14] Updated documentation --- lib/src/layer/attribution_layer/rich/widget.dart | 3 --- lib/src/layer/attribution_layer/simple.dart | 6 ------ lib/src/map/options.dart | 10 ++++++---- lib/src/map/widget.dart | 8 +++++--- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/src/layer/attribution_layer/rich/widget.dart b/lib/src/layer/attribution_layer/rich/widget.dart index 637d256fe..3afe62ae3 100644 --- a/lib/src/layer/attribution_layer/rich/widget.dart +++ b/lib/src/layer/attribution_layer/rich/widget.dart @@ -46,9 +46,6 @@ enum AttributionAlignment { /// property for more information. By default, a simple fade/opacity animation /// is provided by [FadeRAWA]. [ScaleRAWA] is also available. /// -/// This layer is an anchored layer, so an additional [AnchoredLayer] should not -/// be used. -/// /// Read the documentation on the individual properties for more information and /// customizability. /// diff --git a/lib/src/layer/attribution_layer/simple.dart b/lib/src/layer/attribution_layer/simple.dart index 804a29931..91be231e9 100644 --- a/lib/src/layer/attribution_layer/simple.dart +++ b/lib/src/layer/attribution_layer/simple.dart @@ -5,9 +5,6 @@ part of 'shared.dart'; /// Displayed as a padded translucent [backgroundColor] box with the following /// text: 'flutter_map | © [source]', where [source] is wrapped with [onTap]. /// -/// This layer is an anchored layer, so an additional [AnchoredLayer] should not -/// be used. -/// /// See also: /// /// * [RichAttributionWidget], which is dynamic, supports more customization, @@ -31,9 +28,6 @@ class SimpleAttributionWidget extends StatelessWidget /// /// Displayed as a padded translucent white box with the following text: /// 'flutter_map | © [source]'. - /// - /// This layer is an anchored layer, so an additional [AnchoredLayer] should - /// not be used. const SimpleAttributionWidget({ super.key, required this.source, diff --git a/lib/src/map/options.dart b/lib/src/map/options.dart index f5ef843c6..a100ac972 100644 --- a/lib/src/map/options.dart +++ b/lib/src/map/options.dart @@ -114,16 +114,18 @@ class MapOptions { /// Whether to apply pointer translucency to all layers automatically /// - /// This means that layers are invisible to its parent when hit testing, but - /// still allows its subtree to receive pointer events. + /// This will mean that each layer can handle all the gestures that enter the + /// map themselves. Without this, only the top layer may handle gestures. /// /// Note that layers that are visually obscured behind another layer will /// recieve events, if this is enabled. /// - /// Also note that this applies to (overlaid) [AnchoredLayer]s as well. + /// Technically, layers become invisible to the parent `Stack` when hit + /// testing (and thus `Stack` will keep bubbling gestures down all layers), but + /// will still allow their subtree to receive pointer events. /// /// If this is `false` (defaults to `true`), then [TranslucentPointer] may be - /// applied to individual layers. + /// manually applied to individual layers. final bool applyPointerTranslucencyToLayers; final InteractionOptions? _interactionOptions; diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index ec34ab2c1..beedba3fd 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -83,9 +83,11 @@ class FlutterMap extends StatefulWidget { /// Widgets to be placed onto the map in a [Stack]-like fashion /// - /// Widgets that use [MobileLayerTransformer] will be mobile, will move and - /// rotate with the map. Other widgets will be static (and should usually use - /// [Align] or another method to position themselves). + /// Widgets that use [MobileLayerTransformer] will be 'mobile', will move and + /// rotate with the map. Other widgets will be 'static' (and should usually use + /// [Align] or another method to position themselves). Widgets/layers may or + /// may not identify which type they are in their documentation, but it should + /// be relatively self-explanatory from their purpose. /// /// [TranslucentPointer] will be wrapped around each child by default, unless /// [MapOptions.applyPointerTranslucencyToLayers] is `false`. From cf0affec19bdda475389784a7f200da4a7d54ed8 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 25 Sep 2023 17:23:18 +0100 Subject: [PATCH 13/14] Fixed new menu opener button being obscured on some devices --- example/lib/pages/home.dart | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/example/lib/pages/home.dart b/example/lib/pages/home.dart index df620d9f3..a5e0e5600 100644 --- a/example/lib/pages/home.dart +++ b/example/lib/pages/home.dart @@ -137,24 +137,27 @@ class _HomePageState extends State { PositionedDirectional( start: 16, top: 16, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(999), - ), - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Builder( - builder: (context) => IconButton( - onPressed: () => Scaffold.of(context).openDrawer(), - icon: const Icon(Icons.menu), + child: SafeArea( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(999), + ), + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Builder( + builder: (context) => IconButton( + onPressed: () => Scaffold.of(context).openDrawer(), + icon: const Icon(Icons.menu), + ), ), - ), - const SizedBox(width: 8), - Image.asset('assets/ProjectIcon.png', height: 32, width: 32), - const SizedBox(width: 8), - ], + const SizedBox(width: 8), + Image.asset('assets/ProjectIcon.png', + height: 32, width: 32), + const SizedBox(width: 8), + ], + ), ), ), ) From e991671dd66ebb09ab11464c6241894bfc9debf2 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 7 Oct 2023 16:46:02 +0100 Subject: [PATCH 14/14] Removed `FlutterMap.simple` constructor Removed joint `AttributionWidget` class Improved marker layer efficiency --- example/lib/pages/home.dart | 42 ++++++++++-------- lib/flutter_map.dart | 5 ++- .../attribution_layer/rich/animation.dart | 3 +- .../layer/attribution_layer/rich/source.dart | 3 +- .../layer/attribution_layer/rich/widget.dart | 11 +++-- lib/src/layer/attribution_layer/shared.dart | 21 --------- lib/src/layer/attribution_layer/simple.dart | 5 +-- lib/src/layer/marker_layer.dart | 18 ++++---- lib/src/map/widget.dart | 43 ------------------- 9 files changed, 49 insertions(+), 102 deletions(-) delete mode 100644 lib/src/layer/attribution_layer/shared.dart diff --git a/example/lib/pages/home.dart b/example/lib/pages/home.dart index a5e0e5600..59b3d2dcc 100644 --- a/example/lib/pages/home.dart +++ b/example/lib/pages/home.dart @@ -103,7 +103,7 @@ class _HomePageState extends State { drawer: buildDrawer(context, HomePage.route), body: Stack( children: [ - FlutterMap.simple( + FlutterMap( options: MapOptions( initialCenter: const LatLng(51.5, -0.09), initialZoom: 5, @@ -114,25 +114,29 @@ class _HomePageState extends State { ), ), ), - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - attribution: RichAttributionWidget( - popupInitialDisplayDuration: const Duration(seconds: 5), - animationConfig: const ScaleRAWA(), - showFlutterMapAttribution: false, - attributions: [ - TextSourceAttribution( - 'OpenStreetMap contributors', - onTap: () => launchUrl( - Uri.parse('https://openstreetmap.org/copyright'), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ), + RichAttributionWidget( + popupInitialDisplayDuration: const Duration(seconds: 5), + animationConfig: const ScaleRAWA(), + showFlutterMapAttribution: false, + attributions: [ + TextSourceAttribution( + 'OpenStreetMap contributors', + onTap: () => launchUrl( + Uri.parse('https://openstreetmap.org/copyright'), + ), ), - ), - const TextSourceAttribution( - 'This attribution is the same throughout this app, except where otherwise specified', - prependCopyright: false, - ), - ], - ), + const TextSourceAttribution( + 'This attribution is the same throughout this app, except where otherwise specified', + prependCopyright: false, + ), + ], + ), + ], ), PositionedDirectional( start: 16, diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 3857cb9c4..80a332977 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -5,7 +5,10 @@ export 'package:flutter_map/src/geo/latlng_bounds.dart'; export 'package:flutter_map/src/gestures/interactive_flag.dart'; export 'package:flutter_map/src/gestures/map_events.dart'; export 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; -export 'package:flutter_map/src/layer/attribution_layer/shared.dart'; +export 'package:flutter_map/src/layer/attribution_layer/rich/animation.dart'; +export 'package:flutter_map/src/layer/attribution_layer/rich/source.dart'; +export 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart'; +export 'package:flutter_map/src/layer/attribution_layer/simple.dart'; export 'package:flutter_map/src/layer/circle_layer.dart'; export 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; export 'package:flutter_map/src/layer/general/translucent_pointer.dart'; diff --git a/lib/src/layer/attribution_layer/rich/animation.dart b/lib/src/layer/attribution_layer/rich/animation.dart index 56dcd5348..d45f50e9c 100644 --- a/lib/src/layer/attribution_layer/rich/animation.dart +++ b/lib/src/layer/attribution_layer/rich/animation.dart @@ -1,4 +1,5 @@ -part of '../shared.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart'; /// Animation provider interface for a [RichAttributionWidget] /// diff --git a/lib/src/layer/attribution_layer/rich/source.dart b/lib/src/layer/attribution_layer/rich/source.dart index 65092e5ee..e504dff88 100644 --- a/lib/src/layer/attribution_layer/rich/source.dart +++ b/lib/src/layer/attribution_layer/rich/source.dart @@ -1,4 +1,5 @@ -part of '../shared.dart'; +import 'package:flutter/material.dart'; +import 'package:meta/meta.dart'; /// Base class for attributions that render themselves as widgets in a /// [RichAttributionWidget] diff --git a/lib/src/layer/attribution_layer/rich/widget.dart b/lib/src/layer/attribution_layer/rich/widget.dart index 713e7f65c..b37a30059 100644 --- a/lib/src/layer/attribution_layer/rich/widget.dart +++ b/lib/src/layer/attribution_layer/rich/widget.dart @@ -1,4 +1,10 @@ -part of '../shared.dart'; +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/src/gestures/map_events.dart'; +import 'package:flutter_map/src/layer/attribution_layer/rich/animation.dart'; +import 'package:flutter_map/src/layer/attribution_layer/rich/source.dart'; +import 'package:flutter_map/src/map/controller/map_controller.dart'; /// Position to anchor [RichAttributionWidget] to relative to the [FlutterMap] /// @@ -55,8 +61,7 @@ enum AttributionAlignment { /// attribution layer /// {@endtemplate} @immutable -class RichAttributionWidget extends StatefulWidget - implements AttributionWidget { +class RichAttributionWidget extends StatefulWidget { /// List of attributions to display /// /// [TextSourceAttribution]s are shown in a popup box (toggled by a tap/click diff --git a/lib/src/layer/attribution_layer/shared.dart b/lib/src/layer/attribution_layer/shared.dart deleted file mode 100644 index 1a4952ac4..000000000 --- a/lib/src/layer/attribution_layer/shared.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; -import 'package:meta/meta.dart'; - -part 'rich/animation.dart'; -part 'rich/source.dart'; -part 'rich/widget.dart'; -part 'simple.dart'; - -/// Layer widget intended to attribute a source -/// -/// Implemented by [RichAttributionWidget] & [SimpleAttributionWidget]. -/// -/// Has no effect other than as a label to group the provided layers together -/// for the [FlutterMap.simple] constructor. -@immutable -sealed class AttributionWidget extends Widget { - const AttributionWidget._(); -} diff --git a/lib/src/layer/attribution_layer/simple.dart b/lib/src/layer/attribution_layer/simple.dart index 91be231e9..199280468 100644 --- a/lib/src/layer/attribution_layer/simple.dart +++ b/lib/src/layer/attribution_layer/simple.dart @@ -1,4 +1,4 @@ -part of 'shared.dart'; +import 'package:flutter/material.dart'; /// A simple, classic style, attribution layer /// @@ -10,8 +10,7 @@ part of 'shared.dart'; /// * [RichAttributionWidget], which is dynamic, supports more customization, /// and has a more complex appearance. @immutable -class SimpleAttributionWidget extends StatelessWidget - implements AttributionWidget { +class SimpleAttributionWidget extends StatelessWidget { /// Attribution text, such as 'OpenStreetMap contributors' final Text source; diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index 19d304589..57480dba4 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -105,11 +105,9 @@ class MarkerLayer extends StatelessWidget { return MobileLayerTransformer( child: Stack( - children: (List.generate( - markers.length, - (i) { - final m = markers[i]; - + // ignore: avoid_types_on_closure_parameters + children: (List markers) sync* { + for (final m in markers) { // Resolve real alignment final left = 0.5 * m.width * ((m.alignment ?? alignment).x + 1); final top = 0.5 * m.height * ((m.alignment ?? alignment).y + 1); @@ -125,12 +123,12 @@ class MarkerLayer extends StatelessWidget { Point(pxPoint.x + left, pxPoint.y - bottom), Point(pxPoint.x - right, pxPoint.y + top), ), - )) return null; + )) continue; // Apply map camera to marker position final pos = pxPoint.subtract(map.pixelOrigin); - return Positioned( + yield Positioned( key: m.key, width: m.width, height: m.height, @@ -144,9 +142,9 @@ class MarkerLayer extends StatelessWidget { ) : m.child, ); - }, - )..retainWhere((w) => w != null)) - .cast(), + } + }(markers) + .toList(), ), ); } diff --git a/lib/src/map/widget.dart b/lib/src/map/widget.dart index 10d805bcb..8f3f9658c 100644 --- a/lib/src/map/widget.dart +++ b/lib/src/map/widget.dart @@ -5,10 +5,8 @@ import 'dart:math'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/gestures/flutter_map_interactive_viewer.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; -import 'package:flutter_map/src/layer/attribution_layer/shared.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/general/translucent_pointer.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; import 'package:flutter_map/src/map/camera/camera_fit.dart'; import 'package:flutter_map/src/map/controller/impl.dart'; import 'package:flutter_map/src/map/controller/internal.dart'; @@ -40,47 +38,6 @@ class FlutterMap extends StatefulWidget { this.nonRotatedChildren = const [], }); - /// Creates an interactive geographical map - /// - /// This constructor is a shortcut intended for only the most simple of maps. - /// It does not support customization of the underlying [TileLayer], and lacks - /// the ability to add feature layers (except for an attribution layer) or - /// attach a [MapController]. Use the standard constructor if these are - /// required. - /// - /// See the properties and online documentation for more information about - /// set-up, configuration, and usage. - /// - /// --- - /// - /// Provide a standard slippy map URL template to the [urlTemplate] argument, - /// with `{x}`, `{y}`, and `{z}` placeholders. Subdomain support is not - /// supported through this simple constructor. - /// - /// Provide the application's correct package name, such as 'com.example.app', - /// to the [userAgentPackageName] argument, to allow the tile server to - /// identify your application. For more information, see - /// [TileLayer.tileProvider]'s documentation. - /// - /// It is recommended to provide a [RichAttributionWidget] or - /// [SimpleAttributionWidget] to the [attribution] argument. For more - /// information, see their documentation. - FlutterMap.simple({ - super.key, - required this.options, - required String urlTemplate, - required String userAgentPackageName, - AttributionWidget? attribution, - }) : children = [ - TileLayer( - urlTemplate: urlTemplate, - userAgentPackageName: userAgentPackageName, - ), - if (attribution != null) attribution, - ], - mapController = null, - nonRotatedChildren = []; - /// Widgets to be placed onto the map in a [Stack]-like fashion /// /// Widgets that use [MobileLayerTransformer] will be 'mobile', will move and