Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multi(gestures): add keyTriggerClickRotate gesture, fix keyTriggerDragRotate gesture #1810

Draft
wants to merge 15 commits into
base: gesture-handling
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion example/lib/pages/gestures_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class _GesturesPageState extends State<GesturesPage> {
'Rotation': {
InteractiveFlag.twoFingerRotate: 'Twist',
InteractiveFlag.keyTriggerDragRotate: 'CTRL+Drag',
InteractiveFlag.keyTriggerClickRotate: 'CTRL+Click',
},
};

Expand All @@ -47,7 +48,7 @@ class _GesturesPageState extends State<GesturesPage> {
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
Expand Down
48 changes: 44 additions & 4 deletions lib/src/map/gestures/map_interactive_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
DragGestureService? _drag;
DoubleTapDragZoomGestureService? _doubleTapDragZoom;
KeyTriggerDragRotateGestureService? _keyTriggerDragRotate;
KeyTriggerClickRotateGestureService? _keyTriggerClickRotate;

MapControllerImpl get _controller => widget.controller;

Expand Down Expand Up @@ -105,6 +106,7 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
_drag != null ||
_doubleTapDragZoom != null ||
_twoFingerInput != null;
final useTapCallback = _tap != null || _keyTriggerClickRotate != null;

return Listener(
onPointerDown: (event) {
Expand Down Expand Up @@ -166,9 +168,39 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
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,

Expand Down Expand Up @@ -208,7 +240,8 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
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) {
Expand Down Expand Up @@ -297,6 +330,13 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
_keyTriggerDragRotate = null;
}

if (newGestures.keyTriggerClickRotate) {
_keyTriggerClickRotate =
KeyTriggerClickRotateGestureService(controller: _controller);
} else {
_keyTriggerClickRotate = null;
}

if (newGestures.doubleTapDragZoom) {
_doubleTapDragZoom =
DoubleTapDragZoomGestureService(controller: _controller);
Expand Down
21 changes: 21 additions & 0 deletions lib/src/map/gestures/services/base_services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
45 changes: 45 additions & 0 deletions lib/src/map/gestures/services/key_trigger_click_rotate.dart
Original file line number Diff line number Diff line change
@@ -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<LogicalKeyboardKey> 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;
}
27 changes: 25 additions & 2 deletions lib/src/map/gestures/services/key_trigger_drag_rotate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<LogicalKeyboardKey> get keys =>
_options.interactionOptions.keyTriggerDragRotateKeys;
Expand All @@ -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,
Expand All @@ -30,15 +43,25 @@ 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,
);
}

/// Called when the gesture ends, cleans up the previously stored values.
void end() {
_screenSize = null;
_degreeCorrection = null;
controller.emitMapEvent(
MapEventRotateEnd(
camera: _camera,
Expand Down
31 changes: 26 additions & 5 deletions lib/src/map/options/map_gestures.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class MapGestures {
required this.scrollWheelZoom,
required this.twoFingerRotate,
required this.keyTriggerDragRotate,
required this.keyTriggerClickRotate,
required this.trackpadZoom,
});

Expand All @@ -37,6 +38,7 @@ class MapGestures {
this.scrollWheelZoom = true,
this.twoFingerRotate = true,
this.keyTriggerDragRotate = true,
this.keyTriggerClickRotate = true,
this.trackpadZoom = true,
});

Expand All @@ -54,6 +56,7 @@ class MapGestures {
this.scrollWheelZoom = false,
this.twoFingerRotate = false,
this.keyTriggerDragRotate = false,
this.keyTriggerClickRotate = false,
this.trackpadZoom = false,
});

Expand All @@ -77,6 +80,7 @@ class MapGestures {
twoFingerZoom: zoom,
twoFingerRotate: rotate,
keyTriggerDragRotate: rotate,
keyTriggerClickRotate: rotate,
trackpadZoom: zoom,
);

Expand Down Expand Up @@ -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),
);
Expand Down Expand Up @@ -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({
Expand All @@ -175,6 +188,7 @@ class MapGestures {
bool? scrollWheelZoom,
bool? twoFingerRotate,
bool? keyTriggerDragRotate,
bool? keyTriggerClickRotate,
bool? trackpadZoom,
}) =>
MapGestures(
Expand All @@ -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,
);

Expand All @@ -201,6 +217,7 @@ class MapGestures {
doubleTapDragZoom == other.doubleTapDragZoom &&
scrollWheelZoom == other.scrollWheelZoom &&
twoFingerRotate == other.twoFingerRotate &&
keyTriggerClickRotate == other.keyTriggerClickRotate &&
keyTriggerDragRotate == other.keyTriggerDragRotate;

@override
Expand All @@ -212,6 +229,7 @@ class MapGestures {
doubleTapDragZoom,
scrollWheelZoom,
twoFingerRotate,
keyTriggerClickRotate,
keyTriggerDragRotate,
);
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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]
Expand Down
Loading