diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index 6c12ecaa0..df0ff50f7 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -16,24 +16,28 @@ class MarkerPage extends StatefulWidget { class MarkerPageState extends State { final alignments = { - 315: AnchorAlign.topLeft, - 0: AnchorAlign.top, - 45: AnchorAlign.topRight, - 270: AnchorAlign.left, - null: AnchorAlign.center, - 90: AnchorAlign.right, - 225: AnchorAlign.bottomLeft, - 180: AnchorAlign.bottom, - 135: AnchorAlign.bottomRight, + 315: Alignment.topLeft, + 0: Alignment.topCenter, + 45: Alignment.topRight, + 270: Alignment.centerLeft, + null: Alignment.center, + 90: Alignment.centerRight, + 225: Alignment.bottomLeft, + 180: Alignment.bottomCenter, + 135: Alignment.bottomRight, }; - AnchorAlign anchorAlign = AnchorAlign.top; + Alignment anchorAlign = Alignment.topCenter; bool counterRotate = false; - final customMarkers = []; + late final customMarkers = [ + buildPin(const LatLng(51.51868093513547, -0.12835376940892318)), + buildPin(const LatLng(53.33360293799854, -6.284001062079881)), + ]; Marker buildPin(LatLng point) => Marker( point: point, - builder: (ctx) => const Icon(Icons.location_pin, size: 60), + builder: (ctx) => + const Icon(Icons.location_pin, size: 60, color: Colors.black), width: 60, height: 60, ); @@ -43,136 +47,127 @@ class MarkerPageState extends State { return Scaffold( appBar: AppBar(title: const Text('Markers')), drawer: buildDrawer(context, MarkerPage.route), - body: Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox.square( - dimension: 130, - child: GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 5, - crossAxisSpacing: 5, - ), - itemCount: 9, - itemBuilder: (_, index) { - final deg = alignments.keys.elementAt(index); - final align = alignments.values.elementAt(index); - - return IconButton.outlined( - onPressed: () => setState(() => anchorAlign = 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, - size: deg == null ? 16 : null, - ), - ), - ); - }, + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox.square( + dimension: 130, + child: GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 5, + crossAxisSpacing: 5, ), - ), - const SizedBox(width: 16), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Tap the map to add markers!'), - const SizedBox(height: 10), - Row( - children: [ - const Text('Counter-rotation'), - const SizedBox(width: 10), - Switch.adaptive( - value: counterRotate, - onChanged: (v) => setState(() => counterRotate = v), + itemCount: 9, + itemBuilder: (_, index) { + final deg = alignments.keys.elementAt(index); + final align = alignments.values.elementAt(index); + + return IconButton.outlined( + onPressed: () => setState(() => anchorAlign = 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, + size: deg == null ? 16 : null, ), - ], - ), - ], + ), + ); + }, ), - ], - ), + ), + const SizedBox(width: 16), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Tap the map to add markers!'), + const SizedBox(height: 10), + Row( + children: [ + const Text('Counter-rotation'), + const SizedBox(width: 10), + Switch.adaptive( + value: counterRotate, + onChanged: (v) => setState(() => counterRotate = v), + ), + ], + ), + ], + ), + ], ), - Flexible( - child: FlutterMap( - options: MapOptions( - initialCenter: const LatLng(51.5, -0.09), - initialZoom: 5, - onTap: (_, p) => - setState(() => customMarkers.add(buildPin(p))), - interactionOptions: const InteractionOptions( - flags: ~InteractiveFlag.doubleTapZoom, - ), + ), + Flexible( + child: FlutterMap( + options: MapOptions( + initialCenter: const LatLng(51.5, -0.09), + initialZoom: 5, + onTap: (_, p) => setState(() => customMarkers.add(buildPin(p))), + interactionOptions: const InteractionOptions( + flags: ~InteractiveFlag.doubleTapZoom, ), - children: [ - TileLayer( - urlTemplate: - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), - MarkerLayer( - rotate: counterRotate, - anchorPos: AnchorPos.align(anchorAlign), - markers: [ - buildPin(const LatLng( - 51.51868093513547, -0.12835376940892318)), - buildPin( - const LatLng(53.33360293799854, -6.284001062079881)), - Marker( - point: const LatLng( - 47.18664724067855, -1.5436768515939427), - width: 64, - height: 64, - anchorPos: const AnchorPos.align(AnchorAlign.left), - builder: (context) => const ColoredBox( - color: Colors.lightBlue, - child: Align( - alignment: Alignment.centerRight, - child: Text('-->'), - ), + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ), + MarkerLayer( + rotate: counterRotate, + anchorPos: AnchorPos.defaultAnchorPos, + markers: [ + Marker( + point: + const LatLng(47.18664724067855, -1.5436768515939427), + width: 64, + height: 64, + anchorPos: const AnchorPos.align(Alignment.centerLeft), + builder: (context) => const ColoredBox( + color: Colors.lightBlue, + child: Align( + alignment: Alignment.centerRight, + child: Text('-->'), ), ), - Marker( - point: const LatLng( - 47.18664724067855, -1.5436768515939427), - width: 64, - height: 64, - anchorPos: const AnchorPos.align(AnchorAlign.right), - builder: (context) => const ColoredBox( - color: Colors.pink, - child: Align( - alignment: Alignment.centerLeft, - child: Text('<--'), - ), + ), + Marker( + point: + const LatLng(47.18664724067855, -1.5436768515939427), + width: 64, + height: 64, + anchorPos: const AnchorPos.align(Alignment.centerRight), + builder: (context) => const ColoredBox( + color: Colors.pink, + child: Align( + alignment: Alignment.centerLeft, + child: Text('<--'), ), ), - Marker( - point: const LatLng( - 47.18664724067855, -1.5436768515939427), - rotate: false, - builder: (context) => - const ColoredBox(color: Colors.black), - ), - ], - ), - MarkerLayer( - markers: customMarkers, - rotate: counterRotate, - anchorPos: AnchorPos.align(anchorAlign), - ), - ], - ), + ), + Marker( + point: + const LatLng(47.18664724067855, -1.5436768515939427), + rotate: false, + builder: (context) => + const ColoredBox(color: Colors.black), + ), + ], + ), + MarkerLayer( + markers: customMarkers, + rotate: counterRotate, + anchorPos: AnchorPos.align(anchorAlign), + ), + ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index 3e9faf78d..e6de5eb87 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -7,20 +7,54 @@ 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 defined by its [Marker.height] & [Marker.width] +/// of its bounding box /// /// Can be defined exactly (using [AnchorPos.exactly] with an [Anchor]) or in -/// a relative alignment (using [AnchorPos.align] with an [AnchorAlign]). +/// 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 { - static const defaultAnchorPos = AnchorPos.align(AnchorAlign.center); + /// 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; - final AnchorAlign? alignment; - const AnchorPos.exactly(Anchor this.anchor) : alignment = null; + /// 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; - const AnchorPos.align(AnchorAlign this.alignment) : anchor = 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 @@ -39,41 +73,32 @@ class Anchor { if (pos.anchor case final anchor?) return anchor; if (pos.alignment case final alignment?) { return Anchor( - switch (alignment._x) { -1 => 0, 1 => width, _ => width / 2 }, - switch (alignment._y) { 1 => 0, -1 => height, _ => height / 2 }, + 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(); } } -/// Relative alignment for a [Marker.builder] widget relative to the center -/// of its bounding box defined by its [Marker.height] & [Marker.width] -enum AnchorAlign { - topLeft(-1, 1), - topRight(1, 1), - bottomLeft(-1, -1), - bottomRight(1, -1), - center(0, 0), - - /// Top center - top(0, 1), - - /// Bottom center - bottom(0, -1), - - /// Left center - left(-1, 0), - - /// Right center - right(1, 0); - - final int _x; - final int _y; - - const AnchorAlign(this._x, this._y); -} - /// Represents a coordinate point on the map with an attached widget [builder], /// rendered by [MarkerLayer] /// @@ -112,6 +137,9 @@ class Marker { /// 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. /// @@ -133,9 +161,11 @@ class Marker { AnchorPos? anchorPos, this.rotate, this.rotateOrigin, - this.rotateAlignment, - }) : anchor = - anchorPos == null ? null : Anchor.fromPos(anchorPos, width, height); + AlignmentGeometry? rotateAlignment, + }) : anchor = + anchorPos == null ? null : Anchor.fromPos(anchorPos, width, height), + rotateAlignment = rotateAlignment ?? + (anchorPos?.alignment != null ? anchorPos!.alignment! * -1 : null); } @immutable @@ -165,6 +195,9 @@ class MarkerLayer extends StatelessWidget { /// 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. /// @@ -185,7 +218,7 @@ class MarkerLayer extends StatelessWidget { this.anchorPos, this.rotate = false, this.rotateOrigin, - this.rotateAlignment = Alignment.center, + this.rotateAlignment, }); @override @@ -217,12 +250,17 @@ class MarkerLayer extends StatelessWidget { 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, + alignment: + marker.rotateAlignment ?? rotateAlignment ?? defaultAlignment, child: marker.builder(context), ) : marker.builder(context);