From 2ea31100ab9f83688d8bc5a790c8130b1da6501b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 12:17:29 +0200 Subject: [PATCH] Reworked markers (#1659) - No more `builder`s: now we use `Marker.child` for extra efficiency - `Marker`'s constructor is now `const`ant, along with `MarkerLayer` - `rotateOrigin` and `rotateAlignment` have been removed, as not sure of their valid use-case - `Anchor`, `AnchorPos`, and all anchor terminology have been removed, to simplify - Only relative `Alignment`s now supported, but now also non pre-provided ones - Resolved confusion around meaning of anchors/position/alignment/relativity Co-authored-by: JaffaKetchup --- .../lib/pages/animated_map_controller.dart | 83 ++--- example/lib/pages/home.dart | 14 +- example/lib/pages/many_markers.dart | 16 +- example/lib/pages/map_controller.dart | 79 ++--- example/lib/pages/markers.dart | 48 ++- example/lib/pages/moving_markers.dart | 42 +-- example/lib/pages/overlay_image.dart | 24 +- example/lib/pages/point_to_latlng.dart | 2 +- example/lib/pages/reset_tile_layer.dart | 20 +- example/lib/pages/stateful_markers.dart | 9 +- example/lib/pages/tile_builder_example.dart | 8 +- lib/src/layer/marker_layer.dart | 321 ++++++------------ test/flutter_map_test.dart | 14 +- test/layer/marker_layer_test.dart | 6 +- 14 files changed, 262 insertions(+), 424 deletions(-) diff --git a/example/lib/pages/animated_map_controller.dart b/example/lib/pages/animated_map_controller.dart index 8aeb6a15d..df57a0102 100644 --- a/example/lib/pages/animated_map_controller.dart +++ b/example/lib/pages/animated_map_controller.dart @@ -20,9 +20,30 @@ class AnimatedMapControllerPageState extends State static const _inProgressId = 'AnimatedMapController#MoveInProgress'; static const _finishedId = 'AnimatedMapController#MoveFinished'; - static const london = LatLng(51.5, -0.09); - static const paris = LatLng(48.8566, 2.3522); - static const dublin = LatLng(53.3498, -6.2603); + static const _london = LatLng(51.5, -0.09); + static const _paris = LatLng(48.8566, 2.3522); + static const _dublin = LatLng(53.3498, -6.2603); + + static const _markers = [ + Marker( + width: 80, + height: 80, + point: _london, + child: FlutterLogo(key: ValueKey('blue')), + ), + Marker( + width: 80, + height: 80, + point: _dublin, + child: FlutterLogo(key: ValueKey('green')), + ), + Marker( + width: 80, + height: 80, + point: _paris, + child: FlutterLogo(key: ValueKey('purple')), + ), + ]; late final MapController mapController; @@ -88,36 +109,6 @@ class AnimatedMapControllerPageState extends State @override Widget build(BuildContext context) { - final markers = [ - Marker( - width: 80, - height: 80, - point: london, - builder: (ctx) => Container( - key: const Key('blue'), - child: const FlutterLogo(), - ), - ), - Marker( - width: 80, - height: 80, - point: dublin, - builder: (ctx) => const FlutterLogo( - key: Key('green'), - textColor: Colors.green, - ), - ), - Marker( - width: 80, - height: 80, - point: paris, - builder: (ctx) => Container( - key: const Key('purple'), - child: const FlutterLogo(textColor: Colors.purple), - ), - ), - ]; - return Scaffold( appBar: AppBar(title: const Text('Animated MapController')), drawer: buildDrawer(context, AnimatedMapControllerPage.route), @@ -130,21 +121,15 @@ class AnimatedMapControllerPageState extends State child: Row( children: [ MaterialButton( - onPressed: () { - _animatedMapMove(london, 10); - }, + onPressed: () => _animatedMapMove(_london, 10), child: const Text('London'), ), MaterialButton( - onPressed: () { - _animatedMapMove(paris, 5); - }, + onPressed: () => _animatedMapMove(_paris, 5), child: const Text('Paris'), ), MaterialButton( - onPressed: () { - _animatedMapMove(dublin, 5); - }, + onPressed: () => _animatedMapMove(_dublin, 5), child: const Text('Dublin'), ), ], @@ -157,9 +142,9 @@ class AnimatedMapControllerPageState extends State MaterialButton( onPressed: () { final bounds = LatLngBounds.fromPoints([ - dublin, - paris, - london, + _dublin, + _paris, + _london, ]); mapController.fitCamera( @@ -174,9 +159,9 @@ class AnimatedMapControllerPageState extends State MaterialButton( onPressed: () { final bounds = LatLngBounds.fromPoints([ - dublin, - paris, - london, + _dublin, + _paris, + _london, ]); final constrained = CameraFit.bounds( @@ -204,7 +189,7 @@ class AnimatedMapControllerPageState extends State userAgentPackageName: 'dev.fleaflet.flutter_map.example', tileUpdateTransformer: _animatedMoveTileUpdateTransformer, ), - MarkerLayer(markers: markers), + const MarkerLayer(markers: _markers), ], ), ), diff --git a/example/lib/pages/home.dart b/example/lib/pages/home.dart index 6015db39a..8a8f29bec 100644 --- a/example/lib/pages/home.dart +++ b/example/lib/pages/home.dart @@ -145,13 +145,13 @@ class _HomePageState extends State { 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayer( + const MarkerLayer( markers: [ Marker( width: 80, height: 80, - point: const LatLng(51.5, -0.09), - builder: (ctx) => const FlutterLogo( + point: LatLng(51.5, -0.09), + child: FlutterLogo( textColor: Colors.blue, key: ObjectKey(Colors.blue), ), @@ -159,8 +159,8 @@ class _HomePageState extends State { Marker( width: 80, height: 80, - point: const LatLng(53.3498, -6.2603), - builder: (ctx) => const FlutterLogo( + point: LatLng(53.3498, -6.2603), + child: FlutterLogo( textColor: Colors.green, key: ObjectKey(Colors.green), ), @@ -168,8 +168,8 @@ class _HomePageState extends State { Marker( width: 80, height: 80, - point: const LatLng(48.8566, 2.3522), - builder: (ctx) => const FlutterLogo( + point: LatLng(48.8566, 2.3522), + child: FlutterLogo( textColor: Colors.purple, key: ObjectKey(Colors.purple), ), diff --git a/example/lib/pages/many_markers.dart b/example/lib/pages/many_markers.dart index 55817f747..1441a5340 100644 --- a/example/lib/pages/many_markers.dart +++ b/example/lib/pages/many_markers.dart @@ -5,7 +5,7 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_example/widgets/drawer.dart'; import 'package:latlong2/latlong.dart'; -const maxMarkersCount = 5000; +const maxMarkersCount = 10000; /// On this page, [maxMarkersCount] markers are randomly generated /// across europe, and then you can limit them with a slider @@ -35,11 +35,8 @@ class _ManyMarkersPageState extends State { for (var x = 0; x < maxMarkersCount; x++) { allMarkers.add( Marker( - point: LatLng( - doubleInRange(r, 37, 55), - doubleInRange(r, -9, 30), - ), - builder: (context) => const Icon( + point: LatLng(doubleInRange(r, 37, 55), doubleInRange(r, -9, 30)), + child: const Icon( Icons.circle, color: Colors.red, size: 12, @@ -85,8 +82,11 @@ class _ManyMarkersPageState extends State { userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), MarkerLayer( - markers: allMarkers.sublist( - 0, min(allMarkers.length, _sliderVal))), + markers: allMarkers.sublist( + 0, + min(allMarkers.length, _sliderVal), + ), + ), ], ), ), diff --git a/example/lib/pages/map_controller.dart b/example/lib/pages/map_controller.dart index 6d173a4c7..e72aa3abc 100644 --- a/example/lib/pages/map_controller.dart +++ b/example/lib/pages/map_controller.dart @@ -14,14 +14,35 @@ class MapControllerPage extends StatefulWidget { } } -const LatLng london = LatLng(51.5, -0.09); -const LatLng paris = LatLng(48.8566, 2.3522); -const LatLng dublin = LatLng(53.3498, -6.2603); - class MapControllerPageState extends State { late final MapController _mapController; double _rotation = 0; + static const _london = LatLng(51.5, -0.09); + static const _paris = LatLng(48.8566, 2.3522); + static const _dublin = LatLng(53.3498, -6.2603); + + static const _markers = [ + Marker( + width: 80, + height: 80, + point: _london, + child: FlutterLogo(key: ValueKey('blue')), + ), + Marker( + width: 80, + height: 80, + point: _dublin, + child: FlutterLogo(key: ValueKey('green')), + ), + Marker( + width: 80, + height: 80, + point: _paris, + child: FlutterLogo(key: ValueKey('purple')), + ), + ]; + @override void initState() { super.initState(); @@ -30,36 +51,6 @@ class MapControllerPageState extends State { @override Widget build(BuildContext context) { - final markers = [ - Marker( - width: 80, - height: 80, - point: london, - builder: (ctx) => Container( - key: const Key('blue'), - child: const FlutterLogo(), - ), - ), - Marker( - width: 80, - height: 80, - point: dublin, - builder: (ctx) => const FlutterLogo( - key: Key('green'), - textColor: Colors.green, - ), - ), - Marker( - width: 80, - height: 80, - point: paris, - builder: (ctx) => Container( - key: const Key('purple'), - child: const FlutterLogo(textColor: Colors.purple), - ), - ), - ]; - return Scaffold( appBar: AppBar(title: const Text('MapController')), drawer: buildDrawer(context, MapControllerPage.route), @@ -72,21 +63,15 @@ class MapControllerPageState extends State { child: Row( children: [ MaterialButton( - onPressed: () { - _mapController.move(london, 18); - }, + onPressed: () => _mapController.move(_london, 18), child: const Text('London'), ), MaterialButton( - onPressed: () { - _mapController.move(paris, 5); - }, + onPressed: () => _mapController.move(_paris, 5), child: const Text('Paris'), ), MaterialButton( - onPressed: () { - _mapController.move(dublin, 5); - }, + onPressed: () => _mapController.move(_dublin, 5), child: const Text('Dublin'), ), ], @@ -99,9 +84,9 @@ class MapControllerPageState extends State { MaterialButton( onPressed: () { final bounds = LatLngBounds.fromPoints([ - dublin, - paris, - london, + _dublin, + _paris, + _london, ]); _mapController.fitCamera( @@ -163,7 +148,7 @@ class MapControllerPageState extends State { 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayer(markers: markers), + const MarkerLayer(markers: _markers), ], ), ), diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index df0ff50f7..e6648d7b8 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -9,13 +9,14 @@ class MarkerPage extends StatefulWidget { const MarkerPage({Key? key}) : super(key: key); @override - MarkerPageState createState() { - return MarkerPageState(); - } + State createState() => _MarkerPageState(); } -class MarkerPageState extends State { - final alignments = { +class _MarkerPageState extends State { + Alignment selectedAlignment = Alignment.topCenter; + bool counterRotate = false; + + static const alignments = { 315: Alignment.topLeft, 0: Alignment.topCenter, 45: Alignment.topRight, @@ -27,8 +28,6 @@ class MarkerPageState extends State { 135: Alignment.bottomRight, }; - Alignment anchorAlign = Alignment.topCenter; - bool counterRotate = false; late final customMarkers = [ buildPin(const LatLng(51.51868093513547, -0.12835376940892318)), buildPin(const LatLng(53.33360293799854, -6.284001062079881)), @@ -36,8 +35,7 @@ class MarkerPageState extends State { Marker buildPin(LatLng point) => Marker( point: point, - builder: (ctx) => - const Icon(Icons.location_pin, size: 60, color: Colors.black), + child: const Icon(Icons.location_pin, size: 60, color: Colors.black), width: 60, height: 60, ); @@ -69,12 +67,15 @@ class MarkerPageState extends State { final align = alignments.values.elementAt(index); return IconButton.outlined( - onPressed: () => setState(() => anchorAlign = align), + onPressed: () => + setState(() => selectedAlignment = align), icon: Transform.rotate( angle: deg == null ? 0 : deg * pi / 180, child: Icon( deg == null ? Icons.circle : Icons.arrow_upward, - color: anchorAlign == align ? Colors.green : null, + color: selectedAlignment == align + ? Colors.green + : null, size: deg == null ? 16 : null, ), ), @@ -120,15 +121,13 @@ class MarkerPageState extends State { ), MarkerLayer( rotate: counterRotate, - anchorPos: AnchorPos.defaultAnchorPos, - markers: [ + markers: const [ Marker( - point: - const LatLng(47.18664724067855, -1.5436768515939427), + point: LatLng(47.18664724067855, -1.5436768515939427), width: 64, height: 64, - anchorPos: const AnchorPos.align(Alignment.centerLeft), - builder: (context) => const ColoredBox( + alignment: Alignment.centerLeft, + child: ColoredBox( color: Colors.lightBlue, child: Align( alignment: Alignment.centerRight, @@ -137,12 +136,11 @@ class MarkerPageState extends State { ), ), Marker( - point: - const LatLng(47.18664724067855, -1.5436768515939427), + point: LatLng(47.18664724067855, -1.5436768515939427), width: 64, height: 64, - anchorPos: const AnchorPos.align(Alignment.centerRight), - builder: (context) => const ColoredBox( + alignment: Alignment.centerRight, + child: ColoredBox( color: Colors.pink, child: Align( alignment: Alignment.centerLeft, @@ -151,18 +149,16 @@ class MarkerPageState extends State { ), ), Marker( - point: - const LatLng(47.18664724067855, -1.5436768515939427), + point: LatLng(47.18664724067855, -1.5436768515939427), rotate: false, - builder: (context) => - const ColoredBox(color: Colors.black), + child: ColoredBox(color: Colors.black), ), ], ), MarkerLayer( markers: customMarkers, rotate: counterRotate, - anchorPos: AnchorPos.align(anchorAlign), + alignment: selectedAlignment, ), ], ), diff --git a/example/lib/pages/moving_markers.dart b/example/lib/pages/moving_markers.dart index 02b984034..eb4d93ce4 100644 --- a/example/lib/pages/moving_markers.dart +++ b/example/lib/pages/moving_markers.dart @@ -21,6 +21,27 @@ class _MovingMarkersPageState extends State { late final Timer _timer; int _markerIndex = 0; + static const _markers = [ + Marker( + width: 80, + height: 80, + point: LatLng(51.5, -0.09), + child: FlutterLogo(), + ), + Marker( + width: 80, + height: 80, + point: LatLng(53.3498, -6.2603), + child: FlutterLogo(), + ), + Marker( + width: 80, + height: 80, + point: LatLng(48.8566, 2.3522), + child: FlutterLogo(), + ), + ]; + @override void initState() { super.initState(); @@ -74,24 +95,3 @@ class _MovingMarkersPageState extends State { ); } } - -List _markers = [ - Marker( - width: 80, - height: 80, - point: const LatLng(51.5, -0.09), - builder: (ctx) => const FlutterLogo(), - ), - Marker( - width: 80, - height: 80, - point: const LatLng(53.3498, -6.2603), - builder: (ctx) => const FlutterLogo(), - ), - Marker( - width: 80, - height: 80, - point: const LatLng(48.8566, 2.3522), - builder: (ctx) => const FlutterLogo(), - ), -]; diff --git a/example/lib/pages/overlay_image.dart b/example/lib/pages/overlay_image.dart index f3310238f..58a701524 100644 --- a/example/lib/pages/overlay_image.dart +++ b/example/lib/pages/overlay_image.dart @@ -56,20 +56,22 @@ class OverlayImagePage extends StatelessWidget { userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), OverlayImageLayer(overlayImages: overlayImages), - MarkerLayer(markers: [ - Marker( + const MarkerLayer( + markers: [ + Marker( point: topLeftCorner, - builder: (context) => const _Circle( - color: Colors.redAccent, label: "TL")), - Marker( + child: _Circle(color: Colors.redAccent, label: "TL"), + ), + Marker( point: bottomLeftCorner, - builder: (context) => const _Circle( - color: Colors.redAccent, label: "BL")), - Marker( + child: _Circle(color: Colors.redAccent, label: "BL"), + ), + Marker( point: bottomRightCorner, - builder: (context) => const _Circle( - color: Colors.redAccent, label: "BR")), - ]) + child: _Circle(color: Colors.redAccent, label: "BR"), + ), + ], + ), ], ), ), diff --git a/example/lib/pages/point_to_latlng.dart b/example/lib/pages/point_to_latlng.dart index 5aff99507..bdb0f6be8 100644 --- a/example/lib/pages/point_to_latlng.dart +++ b/example/lib/pages/point_to_latlng.dart @@ -77,7 +77,7 @@ class PointToLatlngPage extends State { width: pointSize, height: pointSize, point: latLng!, - builder: (ctx) => const FlutterLogo(), + child: const FlutterLogo(), ) ], ), diff --git a/example/lib/pages/reset_tile_layer.dart b/example/lib/pages/reset_tile_layer.dart index 8393a8409..6ab2124d7 100644 --- a/example/lib/pages/reset_tile_layer.dart +++ b/example/lib/pages/reset_tile_layer.dart @@ -37,15 +37,6 @@ class ResetTileLayerPageState extends State { @override Widget build(BuildContext context) { - final markers = [ - Marker( - width: 80, - height: 80, - point: const LatLng(51.5, -0.09), - builder: (ctx) => const FlutterLogo(), - ), - ]; - return Scaffold( appBar: AppBar(title: const Text('TileLayer Reset')), drawer: buildDrawer(context, ResetTileLayerPage.route), @@ -82,7 +73,16 @@ class ResetTileLayerPageState extends State { subdomains: layerToggle ? const [] : const ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayer(markers: markers) + const MarkerLayer( + markers: [ + Marker( + width: 80, + height: 80, + point: LatLng(51.5, -0.09), + child: FlutterLogo(), + ), + ], + ), ], ), ), diff --git a/example/lib/pages/stateful_markers.dart b/example/lib/pages/stateful_markers.dart index 1b5468628..d1f0c7765 100644 --- a/example/lib/pages/stateful_markers.dart +++ b/example/lib/pages/stateful_markers.dart @@ -35,13 +35,16 @@ class _StatefulMarkersPageState extends State { } void _addMarker(String key) { - _markers.add(Marker( + _markers.add( + Marker( width: 40, height: 40, point: LatLng( _random.nextDouble() * 10 + 48, _random.nextDouble() * 10 - 6), - builder: (ctx) => const _ColorMarker(), - key: ValueKey(key))); + child: const _ColorMarker(), + key: ValueKey(key), + ), + ); } @override diff --git a/example/lib/pages/tile_builder_example.dart b/example/lib/pages/tile_builder_example.dart index cad94dec5..1de53c88a 100644 --- a/example/lib/pages/tile_builder_example.dart +++ b/example/lib/pages/tile_builder_example.dart @@ -133,15 +133,13 @@ class _TileBuilderPageState extends State { panBuffer: panBuffer, ), ), - MarkerLayer( + const MarkerLayer( markers: [ Marker( width: 80, height: 80, - point: const LatLng(51.5, -0.09), - builder: (ctx) => const FlutterLogo( - key: ObjectKey(Colors.blue), - ), + point: LatLng(51.5, -0.09), + child: FlutterLogo(key: ObjectKey(Colors.blue)), ), ], ), diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index e6de5eb87..1ba96fb3a 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -6,276 +6,145 @@ import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:flutter_map/src/misc/private/bounds.dart'; import 'package:latlong2/latlong.dart'; -/// Defines the positioning of a [Marker.builder] widget relative to the center -/// of its bounding box +/// A container for a [child] widget located at a geographic coordinate [point] /// -/// Can be defined exactly (using [AnchorPos.exactly] with an [Anchor]) or in -/// a relative/dynamic alignment (using [AnchorPos.align] with an [Alignment]). -/// -/// If using [AnchorPos.align], the provided [AlignmentGeometry]'s factors must -/// be either -1, 1, or 0 only (ie. the pre-provided [Alignment]s). -/// [textDirection] will default to [TextDirection.ltr], and is used to resolve -/// the [AlignmentGeometry]. -@immutable -class AnchorPos { - /// The default, central alignment - static const defaultAnchorPos = AnchorPos.align(Alignment.center); - - /// Exact left/top anchor - /// - /// Set only if constructed with [AnchorPos.exactly]. - final Anchor? anchor; - - /// Relative/dynamic alignment - /// - /// Transformed into [anchor] at runtime by [Anchor.fromPos]. Resolved by - /// [textDirection]. - /// - /// Set only if constructed with [AnchorPos.align]. - final AlignmentGeometry? alignment; - - /// Used to resolve [alignment]. - /// - /// Set only if constructed with [AnchorPos.align]. - final TextDirection? textDirection; - - /// Defines the positioning of a [Marker.builder] widget relative to the center - /// of its bounding box, with an exact left/top anchor - const AnchorPos.exactly(Anchor this.anchor) - : alignment = null, - textDirection = null; - - /// Defines the positioning of a [Marker.builder] widget relative to the center - /// of its bounding box, with a relative/dynamic alignment - /// - /// [alignment]'s factors must be either -1, 1, or 0 only (ie. the pre-provided - /// [Alignment]s). [textDirection] will default to [TextDirection.ltr], and is - /// used to resolve the [AlignmentGeometry]. - const AnchorPos.align( - AlignmentGeometry this.alignment, { - this.textDirection = TextDirection.ltr, - }) : anchor = null; -} - -/// Exact alignment for a [Marker.builder] widget relative to the center -/// of its bounding box defined by its [Marker.height] & [Marker.width] -/// -/// May be generated from an [AnchorPos] (usually with [AnchorPos.alignment] -/// defined) and dimensions through [Anchor.fromPos]. -@immutable -class Anchor { - final double left; - final double top; - - const Anchor(this.left, this.top); - - factory Anchor.fromPos(AnchorPos pos, double width, double height) { - if (pos.anchor case final anchor?) return anchor; - if (pos.alignment case final alignment?) { - return Anchor( - switch (alignment.resolve(pos.textDirection).x) { - -1 => 0, - 1 => width, - 0 => width / 2, - _ => throw ArgumentError.value( - alignment, - 'alignment', - 'The `x` factor must be -1, 1, or 0 only (ie. the pre-provided alignments)', - ), - }, - switch (alignment.resolve(pos.textDirection).y) { - -1 => 0, - 1 => height, - 0 => height / 2, - _ => throw ArgumentError.value( - alignment, - 'alignment', - 'The `y` factor must be -1, 1, or 0 only (ie. the pre-provided alignments)', - ), - }, - ); - } - throw Exception(); - } -} - -/// Represents a coordinate point on the map with an attached widget [builder], -/// rendered by [MarkerLayer] -/// -/// Some properties defaults will absorb the values from the parent [MarkerLayer], -/// if the reflected properties are defined there. +/// Some properties defaults will absorb the values from the parent +/// [MarkerLayer], if the reflected properties are defined there. @immutable class Marker { final Key? key; /// Coordinates of the marker + /// + /// This will be the center of the marker, assuming that [alignment] is + /// [Alignment.center] (default). final LatLng point; - /// Function that builds UI of the marker - final WidgetBuilder builder; + /// Widget tree of the marker, sized by [width] & [height] + /// + /// The [Marker] itself is not a widget. + final Widget child; - /// Bounding box width of the marker + /// Width of [child] final double width; - /// Bounding box height of the marker + /// Height of [child] final double height; - /// Positioning of the [builder] widget relative to the center of its bounding - /// box. - final Anchor? anchor; - - /// Whether to counter rotate markers to the map's rotation, to keep a fixed - /// orientation - final bool? rotate; - - /// The origin of the coordinate system (relative to the upper left corner of - /// this render object) in which to apply the matrix. + /// Alignment of the marker relative to the normal center at [point] /// - /// Setting an origin is equivalent to conjugating the transform matrix by a - /// translation. This property is provided just for convenience. - final Offset? rotateOrigin; - - /// The alignment of the origin, relative to the size of the box. + /// For example, [Alignment.topCenter] will mean the entire marker widget is + /// located above the [point]. + /// + /// The center of rotation (anchor) will be opposite this. /// - /// Automatically set to the opposite of `anchorPos`, if it was constructed by - /// [AnchorPos.align], but can be overridden. + /// Defaults to [Alignment.center] if also unset by [MarkerLayer]. + final Alignment? alignment; + + /// Whether to counter rotate this marker to the map's rotation, to keep a + /// fixed orientation /// - /// This is equivalent to setting an origin based on the size of the box. - /// If it is specified at the same time as the [rotateOrigin], both are applied. + /// When `true`, this marker will always appear upright and vertical from the + /// user's perspective. Defaults to `false` if also unset by [MarkerLayer]. /// - /// An [AlignmentDirectional.centerStart] value is the same as an [Alignment] - /// whose [Alignment.x] value is `-1.0` if [Directionality.of] returns - /// [TextDirection.ltr], and `1.0` if [Directionality.of] returns - /// [TextDirection.rtl]. Similarly [AlignmentDirectional.centerEnd] is the - /// same as an [Alignment] whose [Alignment.x] value is `1.0` if - /// [Directionality.of] returns [TextDirection.ltr], and `-1.0` if - /// [Directionality.of] returns [TextDirection.rtl]. - final AlignmentGeometry? rotateAlignment; + /// Note that this is not used to apply a custom rotation in degrees to the + /// marker. Use a widget inside [builder] to perform this. + final bool? rotate; - Marker({ + /// Creates a container for a [child] widget located at a geographic coordinate + /// [point] + /// + /// Some properties defaults will absorb the values from the parent + /// [MarkerLayer], if the reflected properties are defined there. + const Marker({ this.key, required this.point, - required this.builder, - this.width = 30.0, - this.height = 30.0, - AnchorPos? anchorPos, + required this.child, + this.width = 30, + this.height = 30, + this.alignment, this.rotate, - this.rotateOrigin, - AlignmentGeometry? rotateAlignment, - }) : anchor = - anchorPos == null ? null : Anchor.fromPos(anchorPos, width, height), - rotateAlignment = rotateAlignment ?? - (anchorPos?.alignment != null ? anchorPos!.alignment! * -1 : null); + }); } @immutable class MarkerLayer extends StatelessWidget { final List markers; - /// Positioning of the [Marker.builder] widget relative to the center of its - /// bounding box defined by its [Marker.height] & [Marker.width] - /// - /// Overriden on a per [Marker] basis if [Marker.anchorPos] is specified. - final AnchorPos? anchorPos; - - /// Whether to counter rotate markers to the map's rotation, to keep a fixed - /// orientation + /// Alignment of each marker relative to its normal center at [Marker.point] /// - /// Overriden on a per [Marker] basis if [Marker.rotate] is specified. - final bool rotate; - - /// The origin of the coordinate system (relative to the upper left corner of - /// this render object) in which to apply the matrix. + /// For example, [Alignment.topCenter] will mean the entire marker widget is + /// located above the [Marker.point]. /// - /// Setting an origin is equivalent to conjugating the transform matrix by a - /// translation. This property is provided just for convenience. + /// The center of rotation (anchor) will be opposite this. /// - /// Overriden on a per [Marker] basis if [Marker.rotateOrigin] is specified. - final Offset? rotateOrigin; + /// Defaults to [Alignment.center]. Overriden by [Marker.alignment] if set. + final Alignment alignment; - /// The alignment of the origin, relative to the size of the box. - /// - /// Automatically set to the opposite of `anchorPos`, if it was constructed by - /// [AnchorPos.align], but can be overridden. - /// - /// This is equivalent to setting an origin based on the size of the box. - /// If it is specified at the same time as the [rotateOrigin], both are applied. + /// Whether to counter rotate markers to the map's rotation, to keep a fixed + /// orientation /// - /// An [AlignmentDirectional.centerStart] value is the same as an [Alignment] - /// whose [Alignment.x] value is `-1.0` if [Directionality.of] returns - /// [TextDirection.ltr], and `1.0` if [Directionality.of] returns - /// [TextDirection.rtl]. Similarly [AlignmentDirectional.centerEnd] is the - /// same as an [Alignment] whose [Alignment.x] value is `1.0` if - /// [Directionality.of] returns [TextDirection.ltr], and `-1.0` if - /// [Directionality.of] returns [TextDirection.rtl]. + /// When `true`, markers will always appear upright and vertical from the + /// user's perspective. Defaults to `false`. Overriden by [Marker.rotate]. /// - /// Overriden on a per [Marker] basis if [Marker.rotateAlignment] is specified. - final AlignmentGeometry? rotateAlignment; + /// Note that this is not used to apply a custom rotation in degrees to the + /// markers. Use a widget inside [Marker.builder] to perform this. + final bool rotate; const MarkerLayer({ super.key, this.markers = const [], - this.anchorPos, + this.alignment = Alignment.center, this.rotate = false, - this.rotateOrigin, - this.rotateAlignment, }); @override Widget build(BuildContext context) { final map = MapCamera.of(context); - final markerWidgets = []; - - for (final marker in markers) { - final pxPoint = map.project(marker.point); - // See if any portion of the Marker rect resides in the map bounds - // If not, don't spend any resources on build function. - // This calculation works for any Anchor position whithin the Marker - // Note that Anchor coordinates of (0,0) are at bottom-right of the Marker - // unlike the map coordinates. - final anchor = marker.anchor ?? - Anchor.fromPos( - anchorPos ?? AnchorPos.defaultAnchorPos, - marker.width, - marker.height, + return Stack( + children: (List.generate( + markers.length, + (i) { + final m = markers[i]; + + // 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); + final right = m.width - left; + final bottom = m.height - top; + + // Perform projection + final pxPoint = map.project(m.point); + + // Cull if out of bounds + if (!map.pixelBounds.containsPartialBounds( + Bounds( + Point(pxPoint.x + left, pxPoint.y - bottom), + Point(pxPoint.x - right, pxPoint.y + top), + ), + )) return null; + + // Apply map camera to marker position + final pos = pxPoint.subtract(map.pixelOrigin); + + return Positioned( + key: m.key, + width: m.width, + height: m.height, + left: pos.x - right, + top: pos.y - bottom, + child: (m.rotate ?? rotate) + ? Transform.rotate( + angle: -map.rotationRad, + alignment: (m.alignment ?? alignment) * -1, + child: m.child, + ) + : m.child, ); - final rightPortion = marker.width - anchor.left; - final leftPortion = anchor.left; - final bottomPortion = marker.height - anchor.top; - final topPortion = anchor.top; - if (!map.pixelBounds.containsPartialBounds(Bounds( - Point(pxPoint.x + leftPortion, pxPoint.y - bottomPortion), - Point(pxPoint.x - rightPortion, pxPoint.y + topPortion)))) { - continue; - } - - final defaultAlignment = anchorPos?.alignment != null - ? anchorPos!.alignment! * -1 - : Alignment.center; - - final pos = pxPoint.subtract(map.pixelOrigin); - final markerWidget = (marker.rotate ?? rotate) - ? Transform.rotate( - angle: -map.rotationRad, - origin: marker.rotateOrigin ?? rotateOrigin ?? Offset.zero, - alignment: - marker.rotateAlignment ?? rotateAlignment ?? defaultAlignment, - child: marker.builder(context), - ) - : marker.builder(context); - - markerWidgets.add( - Positioned( - key: marker.key, - width: marker.width, - height: marker.height, - left: pos.x - rightPortion, - top: pos.y - bottomPortion, - child: markerWidget, - ), - ); - } - return Stack(children: markerWidgets); + }, + )..retainWhere((w) => w != null)) + .cast(), + ); } } diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index 4b2d904ba..3aa636deb 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -8,18 +8,18 @@ import 'test_utils/test_app.dart'; void main() { testWidgets('flutter_map', (tester) async { - final markers = [ - Marker( + final markers = [ + const Marker( width: 80, height: 80, - point: const LatLng(45.5231, -122.6765), - builder: (_) => const FlutterLogo(), + point: LatLng(45.5231, -122.6765), + child: FlutterLogo(), ), - Marker( + const Marker( width: 80, height: 80, - point: const LatLng(40, -120), // not visible - builder: (_) => const FlutterLogo(), + point: LatLng(40, -120), // not visible + child: FlutterLogo(), ), ]; diff --git a/test/layer/marker_layer_test.dart b/test/layer/marker_layer_test.dart index 0047edef9..455dfbe87 100644 --- a/test/layer/marker_layer_test.dart +++ b/test/layer/marker_layer_test.dart @@ -11,12 +11,12 @@ void main() { const key = Key('m-1'); final markers = [ - Marker( + const Marker( key: key, width: 80, height: 80, - point: const LatLng(45.5231, -122.6765), - builder: (_) => const FlutterLogo(), + point: LatLng(45.5231, -122.6765), + child: FlutterLogo(), ), ];