diff --git a/example/lib/pages/gestures_page.dart b/example/lib/pages/gestures_page.dart index 6754c92b7..e53b7d6e8 100644 --- a/example/lib/pages/gestures_page.dart +++ b/example/lib/pages/gestures_page.dart @@ -29,6 +29,7 @@ class _GesturesPageState extends State { 'Rotation': { InteractiveFlag.twoFingerRotate: 'Twist', InteractiveFlag.keyTriggerDragRotate: 'CTRL+Drag', + InteractiveFlag.keyTriggerClickRotate: 'CTRL+Click', }, }; @@ -47,7 +48,7 @@ class _GesturesPageState extends State { child: Column( children: [ Flex( - direction: screenWidth >= 750 ? Axis.horizontal : Axis.vertical, + direction: screenWidth >= 850 ? Axis.horizontal : Axis.vertical, mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: availableFlags.entries diff --git a/lib/src/map/gestures/map_interactive_viewer.dart b/lib/src/map/gestures/map_interactive_viewer.dart index 40a170e0e..b05da49ad 100644 --- a/lib/src/map/gestures/map_interactive_viewer.dart +++ b/lib/src/map/gestures/map_interactive_viewer.dart @@ -42,6 +42,7 @@ class MapInteractiveViewerState extends State DragGestureService? _drag; DoubleTapDragZoomGestureService? _doubleTapDragZoom; KeyTriggerDragRotateGestureService? _keyTriggerDragRotate; + KeyTriggerClickRotateGestureService? _keyTriggerClickRotate; MapControllerImpl get _controller => widget.controller; @@ -105,6 +106,7 @@ class MapInteractiveViewerState extends State _drag != null || _doubleTapDragZoom != null || _twoFingerInput != null; + final useTapCallback = _tap != null || _keyTriggerClickRotate != null; return Listener( onPointerDown: (event) { @@ -166,9 +168,39 @@ class MapInteractiveViewerState extends State onPointerPanZoomEnd: _trackpadZoom?.end, child: GestureDetector( - onTapDown: _tap?.setDetails, - onTapCancel: _tap?.reset, - onTap: _tap?.submit, + onTapDown: useTapCallback + ? (details) { + if (_keyTriggerClickRotate?.keyPressed ?? false) { + _keyTriggerClickRotate!.setDetails(details); + return; + } + _tap?.setDetails(details); + } + : null, + onTapCancel: useTapCallback + ? () { + if (_keyTriggerClickRotate?.isActive ?? false) { + _keyTriggerClickRotate!.reset(); + return; + } + _tap?.reset(); + } + : null, + onTap: useTapCallback + ? () { + if (_keyTriggerClickRotate?.keyPressed ?? false) { + // Normally we would wait until the tap gesture is confirmed. + // For this gesture however we call it directly for faster + // response time. (Note that `onTap` still has a small delay) + // This however has the trade-off that the gesture could turn + // out to be a double click and both gesture would fire. + final screenSize = MediaQuery.sizeOf(context); + _keyTriggerClickRotate!.submit(screenSize); + return; + } + _tap?.submit(); + } + : null, onLongPressStart: _longPress?.submit, @@ -208,7 +240,8 @@ class MapInteractiveViewerState extends State onScaleStart: useScaleCallback ? (details) { if (_keyTriggerDragRotate?.keyPressed ?? false) { - _keyTriggerDragRotate!.start(); + final screenSize = MediaQuery.sizeOf(context); + _keyTriggerDragRotate!.start(screenSize); } else if (_doubleTapDragZoom?.isActive ?? false) { _doubleTapDragZoom!.start(details); } else if (details.pointerCount == 1) { @@ -297,6 +330,13 @@ class MapInteractiveViewerState extends State _keyTriggerDragRotate = null; } + if (newGestures.keyTriggerClickRotate) { + _keyTriggerClickRotate = + KeyTriggerClickRotateGestureService(controller: _controller); + } else { + _keyTriggerClickRotate = null; + } + if (newGestures.doubleTapDragZoom) { _doubleTapDragZoom = DoubleTapDragZoomGestureService(controller: _controller); diff --git a/lib/src/map/gestures/services/base_services.dart b/lib/src/map/gestures/services/base_services.dart index f67669e59..0a63f76eb 100644 --- a/lib/src/map/gestures/services/base_services.dart +++ b/lib/src/map/gestures/services/base_services.dart @@ -5,10 +5,12 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:vector_math/vector_math.dart'; part 'double_tap.dart'; part 'double_tap_drag_zoom.dart'; part 'drag.dart'; +part 'key_trigger_click_rotate.dart'; part 'key_trigger_drag_rotate.dart'; part 'long_press.dart'; part 'scroll_wheel_zoom.dart'; @@ -76,3 +78,22 @@ Offset _rotateOffset(MapCamera camera, Offset offset) { return Offset(nx, ny); } + +/// Get the Rotation in degrees in relation to the cursor position. +/// +/// By clicking at the top of the map the map gets set to 0°-ish, by clicking +/// on the left side of the map the rotation is set to 270°-ish. +/// +/// Calculation thanks to +/// https://stackoverflow.com/questions/48916517/javascript-click-and-drag-to-rotate +double _getCursorRotationDegrees( + Size screenSize, + Offset cursorOffset, +) { + const degreesCorrection = 180; + final degrees = -math.atan2(cursorOffset.dx - screenSize.width / 2, + cursorOffset.dy - screenSize.height / 2) * + radians2Degrees; + + return degrees + degreesCorrection; +} diff --git a/lib/src/map/gestures/services/key_trigger_click_rotate.dart b/lib/src/map/gestures/services/key_trigger_click_rotate.dart new file mode 100644 index 000000000..8a2b59211 --- /dev/null +++ b/lib/src/map/gestures/services/key_trigger_click_rotate.dart @@ -0,0 +1,45 @@ +part of 'base_services.dart'; + +/// Service to handle the key-trigger and click gesture to rotate the map. +/// By clicking at the top of the map the map gets set to 0°-ish, by clicking +/// on the left side of the map the rotation is set to 270°-ish. +/// +/// The key is by default the CTRL key on the keyboard. +class KeyTriggerClickRotateGestureService extends _BaseGestureService { + TapDownDetails? details; + + /// Getter for the keyboard keys that trigger the drag to rotate gesture. + List get keys => + _options.interactionOptions.keyTriggerDragRotateKeys; + + /// Returns true if the service has consumed a [TapDownDetails] for the + /// tap gesture. + bool get isActive => details != null; + + /// Create a new service that rotates the map if the map gets dragged while + /// a specified key is pressed. + KeyTriggerClickRotateGestureService({required super.controller}); + + void setDetails(TapDownDetails newDetails) => details = newDetails; + + void reset() => details = null; + + /// Called when the gesture receives an update, updates the [MapCamera]. + void submit(Size screenSize) { + if (details == null) return; + + controller.rotateRaw( + _getCursorRotationDegrees( + screenSize, + details!.localPosition, + ), + hasGesture: true, + source: MapEventSource.keyTriggerDragRotate, + ); + } + + /// Checks if one of the specified keys that enable this gesture is pressed. + bool get keyPressed => RawKeyboard.instance.keysPressed + .where((key) => keys.contains(key)) + .isNotEmpty; +} diff --git a/lib/src/map/gestures/services/key_trigger_drag_rotate.dart b/lib/src/map/gestures/services/key_trigger_drag_rotate.dart index 821d239d7..aed7c8ca7 100644 --- a/lib/src/map/gestures/services/key_trigger_drag_rotate.dart +++ b/lib/src/map/gestures/services/key_trigger_drag_rotate.dart @@ -10,6 +10,17 @@ class KeyTriggerDragRotateGestureService extends _BaseGestureService { /// drag updates. bool isActive = false; + /// The size of the screen when the gesture starts. Because it is very + /// unlikely that the size of the screen changes during the gesture we use the + /// screen size of when the gesture starts. + Size? _screenSize; + + /// The rotation to the start on the gesture + double? _degreeCorrection; + + /// Start rotation + double? _startRotation; + /// Getter for the keyboard keys that trigger the drag to rotate gesture. List get keys => _options.interactionOptions.keyTriggerDragRotateKeys; @@ -19,7 +30,9 @@ class KeyTriggerDragRotateGestureService extends _BaseGestureService { KeyTriggerDragRotateGestureService({required super.controller}); /// Called when the gesture is started, stores important values. - void start() { + void start(Size screenSize) { + _screenSize = screenSize; + _startRotation = _camera.rotation; controller.emitMapEvent( MapEventRotateStart( camera: _camera, @@ -30,8 +43,16 @@ class KeyTriggerDragRotateGestureService extends _BaseGestureService { /// Called when the gesture receives an update, updates the [MapCamera]. void update(ScaleUpdateDetails details) { + if (_screenSize == null || _startRotation == null) return; + + final rotation = _getCursorRotationDegrees( + _screenSize!, + details.localFocalPoint, + ); + _degreeCorrection ??= rotation; + controller.rotateRaw( - _camera.rotation - (details.focalPointDelta.dy * 0.5), + rotation - _degreeCorrection! + _startRotation!, hasGesture: true, source: MapEventSource.keyTriggerDragRotate, ); @@ -39,6 +60,8 @@ class KeyTriggerDragRotateGestureService extends _BaseGestureService { /// Called when the gesture ends, cleans up the previously stored values. void end() { + _screenSize = null; + _degreeCorrection = null; controller.emitMapEvent( MapEventRotateEnd( camera: _camera, diff --git a/lib/src/map/options/map_gestures.dart b/lib/src/map/options/map_gestures.dart index 1205b16a9..1ac4729e6 100644 --- a/lib/src/map/options/map_gestures.dart +++ b/lib/src/map/options/map_gestures.dart @@ -20,6 +20,7 @@ class MapGestures { required this.scrollWheelZoom, required this.twoFingerRotate, required this.keyTriggerDragRotate, + required this.keyTriggerClickRotate, required this.trackpadZoom, }); @@ -37,6 +38,7 @@ class MapGestures { this.scrollWheelZoom = true, this.twoFingerRotate = true, this.keyTriggerDragRotate = true, + this.keyTriggerClickRotate = true, this.trackpadZoom = true, }); @@ -54,6 +56,7 @@ class MapGestures { this.scrollWheelZoom = false, this.twoFingerRotate = false, this.keyTriggerDragRotate = false, + this.keyTriggerClickRotate = false, this.trackpadZoom = false, }); @@ -77,6 +80,7 @@ class MapGestures { twoFingerZoom: zoom, twoFingerRotate: rotate, keyTriggerDragRotate: rotate, + keyTriggerClickRotate: rotate, trackpadZoom: zoom, ); @@ -127,6 +131,8 @@ class MapGestures { InteractiveFlag.hasFlag(flags, InteractiveFlag.twoFingerRotate), keyTriggerDragRotate: InteractiveFlag.hasFlag(flags, InteractiveFlag.keyTriggerDragRotate), + keyTriggerClickRotate: + InteractiveFlag.hasFlag(flags, InteractiveFlag.keyTriggerClickRotate), trackpadZoom: InteractiveFlag.hasFlag(flags, InteractiveFlag.trackpadZoom), ); @@ -163,6 +169,13 @@ class MapGestures { /// or finger. final bool keyTriggerDragRotate; + /// Enable rotation by pressing the defined keyboard key (by default CTRL key) + /// and clicking on the map. + /// + /// By clicking at the top of the map the map gets set to 0°-ish, by clicking + /// on the left side of the map the rotation is set to 270°-ish. + final bool keyTriggerClickRotate; + /// Wither to change the value of some gestures. Returns a new /// [MapGestures] object. MapGestures copyWith({ @@ -175,6 +188,7 @@ class MapGestures { bool? scrollWheelZoom, bool? twoFingerRotate, bool? keyTriggerDragRotate, + bool? keyTriggerClickRotate, bool? trackpadZoom, }) => MapGestures( @@ -186,6 +200,8 @@ class MapGestures { scrollWheelZoom: scrollWheelZoom ?? this.scrollWheelZoom, twoFingerRotate: twoFingerRotate ?? this.twoFingerRotate, keyTriggerDragRotate: keyTriggerDragRotate ?? this.keyTriggerDragRotate, + keyTriggerClickRotate: + keyTriggerClickRotate ?? this.keyTriggerClickRotate, trackpadZoom: trackpadZoom ?? this.trackpadZoom, ); @@ -201,6 +217,7 @@ class MapGestures { doubleTapDragZoom == other.doubleTapDragZoom && scrollWheelZoom == other.scrollWheelZoom && twoFingerRotate == other.twoFingerRotate && + keyTriggerClickRotate == other.keyTriggerClickRotate && keyTriggerDragRotate == other.keyTriggerDragRotate; @override @@ -212,6 +229,7 @@ class MapGestures { doubleTapDragZoom, scrollWheelZoom, twoFingerRotate, + keyTriggerClickRotate, keyTriggerDragRotate, ); } @@ -243,7 +261,8 @@ abstract class InteractiveFlag { scrollWheelZoom | twoFingerRotate | trackpadZoom | - keyTriggerDragRotate; + keyTriggerDragRotate | + keyTriggerClickRotate; /// No enabled interactive flags, use as `flags: InteractiveFlag.none` to /// have a non interactive map. @@ -282,9 +301,6 @@ abstract class InteractiveFlag { static const int scrollWheelZoom = 1 << 6; /// Enable rotation with two-finger twist gesture - /// - /// For controlling cursor/keyboard rotation, see - /// [InteractionOptions.cursorKeyboardRotationOptions]. static const int twoFingerRotate = 1 << 7; /// Enable rotation with two-finger twist gesture. @@ -293,12 +309,17 @@ abstract class InteractiveFlag { /// Enable rotation by pressing the defined keyboard keys /// (by default CTRL Key) and drag the map with the cursor. - /// To change the key see [InteractionOptions.cursorKeyboardRotationOptions]. + /// To change the key see [InteractionOptions.keyTriggerDragRotateKeys]. static const int keyTriggerDragRotate = 1 << 8; /// Enable zooming by using the trackpad / touchpad of a device. static const int trackpadZoom = 1 << 9; + /// Enable rotation by pressing the defined keyboard keys + /// (by default CTRL Key) and click on the map. + /// To change the key see [InteractionOptions.keyTriggerDragRotateKeys]. + static const int keyTriggerClickRotate = 1 << 10; + /// Returns `true` if [leftFlags] has at least one member in [rightFlags] /// (intersection) for example /// [leftFlags] = [InteractiveFlag.drag] | [InteractiveFlag.twoFingerRotate]