Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4914ee9
begin
josxha Nov 25, 2025
4af80b7
turn moveCamera sync
josxha Nov 25, 2025
06c1b2a
fling
josxha Nov 26, 2025
6c1aa3e
remove triggerRepaint
josxha Nov 26, 2025
c0de8ad
ctrl key toggle
josxha Nov 27, 2025
85c6856
double tap drag zoom
josxha Nov 27, 2025
3c65551
click with two pointers to zoom out
josxha Nov 27, 2025
2a24b2e
fix repeated zoom out
josxha Nov 27, 2025
0f293f6
integrate gesture options
josxha Nov 27, 2025
f5bd5c6
Update map_state.dart
josxha Nov 27, 2025
13a7ddd
disable browser context menu
josxha Dec 3, 2025
7735058
format
josxha Dec 3, 2025
fccf3f0
Merge branch 'main' into feat/handle-gestures-in-flutter
josxha Dec 4, 2025
7084a29
move two pointers vertically to pitch
josxha Dec 4, 2025
7610e1c
do not switch between two pointer pitch gesture and other gestures
josxha Dec 6, 2025
a34fd6f
keyboard arrow keys
josxha Dec 6, 2025
7026015
use pointer center in pinch zoom gesture
josxha Dec 6, 2025
3681096
only pitch if two pointers start moving vertically
josxha Dec 6, 2025
c4d1063
do not move towards pointer center if zoom is disabled
josxha Dec 6, 2025
d81a324
rotate around pointer center in multi pointer gesture
josxha Dec 6, 2025
09cc35d
create interaction handlers
josxha Dec 7, 2025
2bc9883
integrate event system in new gestures
josxha Dec 7, 2025
470aad5
refactor web view registry factory
josxha Dec 7, 2025
cb47ef6
disable context menu on web
josxha Dec 7, 2025
ecffe50
format
josxha Dec 8, 2025
c4da791
Update pointer_handler.dart
josxha Dec 13, 2025
04bf6ba
Merge branch 'main' into feat/handle-gestures-in-flutter
josxha Dec 15, 2025
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
4 changes: 2 additions & 2 deletions example/integration_test/controller_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ void main() {
await tester.pumpWidget(app);
final ctrl = await ctrlCompleter.future;
events.clear();
await ctrl.moveCamera(
ctrl.moveCamera(
center: const Geographic(lon: 1, lat: 1),
bearing: 1,
zoom: 1,
Expand Down Expand Up @@ -707,7 +707,7 @@ void main() {
final app = App(onMapCreated: ctrlCompleter.complete);
await tester.pumpWidget(app);
final ctrl = await ctrlCompleter.future;
await ctrl.moveCamera(
ctrl.moveCamera(
center: const Geographic(lon: 1, lat: 2),
bearing: 1,
zoom: 1,
Expand Down
2 changes: 1 addition & 1 deletion example/integration_test/map_scalebar_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ void main() {
final widths = <double>[];

for (final zoom in zoomLevels) {
await ctrl.moveCamera(zoom: zoom);
ctrl.moveCamera(zoom: zoom);
await tester.pumpAndSettle(const Duration(seconds: 2));

final size = tester.getSize(customPaintFinder);
Expand Down
2 changes: 1 addition & 1 deletion example/lib/controller_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class _ControllerPageState extends State<ControllerPage> {
OutlinedButton(
onPressed: () async {
debugPrint('moveCamera start');
await _controller.moveCamera(
_controller.moveCamera(
center: const Geographic(lon: 172.4714, lat: -42.4862),
zoom: 4,
pitch: 0,
Expand Down
20 changes: 10 additions & 10 deletions example/lib/events_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,25 +44,25 @@ class _EventsPageState extends State<EventsPage> {
}

String eventToString(MapEvent event) => switch (event) {
MapEventMapCreated() => 'map created',
MapEventStyleLoaded() => 'style loaded',
MapEventMapCreated() => '[Map Created]',
MapEventStyleLoaded() => '[Style Loaded]',
MapEventMoveCamera() =>
'move camera: center ${_formatGeographic(event.camera.center)}, '
'[Move Camera] center ${_formatGeographic(event.camera.center)}, '
'zoom ${event.camera.zoom.toStringAsFixed(2)}, '
'pitch ${event.camera.pitch.toStringAsFixed(2)}, '
'bearing ${event.camera.bearing.toStringAsFixed(2)}',
MapEventStartMoveCamera() =>
'start move camera, reason: ${event.reason.name}',
'[Start Move Camera], reason: ${event.reason.name}',
MapEventClick() =>
'clicked: ${_formatGeographic(event.point)}, screen: ${_formatOffset(event.screenPoint)}',
'[Click] ${_formatGeographic(event.point)}, screen: ${_formatOffset(event.screenPoint)}',
MapEventDoubleClick() =>
'double clicked: ${_formatGeographic(event.point)}, screen: ${_formatOffset(event.screenPoint)}',
'[Double Click] ${_formatGeographic(event.point)}, screen: ${_formatOffset(event.screenPoint)}',
MapEventLongClick() =>
'long clicked: ${_formatGeographic(event.point)}, screen: ${_formatOffset(event.screenPoint)}',
'[Long Click] ${_formatGeographic(event.point)}, screen: ${_formatOffset(event.screenPoint)}',
MapEventSecondaryClick() =>
'secondary clicked: ${_formatGeographic(event.point)}, screen: ${_formatOffset(event.screenPoint)}',
MapEventIdle() => 'idle',
MapEventCameraIdle() => 'camera idle',
'[Secondary Click] ${_formatGeographic(event.point)}, screen: ${_formatOffset(event.screenPoint)}',
MapEventIdle() => '[Idle]',
MapEventCameraIdle() => '[Camera Idle]',
};

void _onEvent(MapEvent event) {
Expand Down
29 changes: 29 additions & 0 deletions maplibre/lib/src/interaction/interaction_handler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:maplibre/maplibre.dart';
import 'package:maplibre/src/map_state.dart';

/// Base class for gesture handlers.
abstract class InteractionHandler {
/// Creates a gesture handler with the given [controller].
const InteractionHandler(this.controller);

/// Reference to the map controller.
final MapLibreMapState controller;

/// Accessor to the map options.
MapOptions get options => controller.options;

/// Accessor to the current map camera.
MapCamera get camera => controller.camera ?? controller.getCamera();

/// Accessor to the map gestures configuration.
MapGestures get gestures => options.gestures;

/// Emits a [MapEventStartMoveCamera].
void emitMoveStartEvent() => controller.widget.onEvent?.call(
const MapEventStartMoveCamera(reason: CameraChangeReason.apiGesture),
);

/// Emits a [MapEventCameraIdle].
void emitCameraIdleEvent() =>
controller.widget.onEvent?.call(const MapEventCameraIdle());
}
98 changes: 98 additions & 0 deletions maplibre/lib/src/interaction/keyboard_handler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'package:flutter/animation.dart';
import 'package:flutter/services.dart';
import 'package:maplibre/src/interaction/interaction_handler.dart';
import 'package:maplibre/src/map_camera_tween.dart';
import 'package:maplibre/src/map_state.dart';

/// Handles keyboard interactions.
class KeyboardHandler extends InteractionHandler {
/// Creates a keyboard interaction handler with the given [controller].
KeyboardHandler(super.controller) {
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
}

final _arrowKeys = <PhysicalKeyboardKey>{
PhysicalKeyboardKey.arrowUp,
PhysicalKeyboardKey.arrowDown,
PhysicalKeyboardKey.arrowLeft,
PhysicalKeyboardKey.arrowRight,
};

/// [MapLibreMapState.dispose]
void dispose() {
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
}

/// Called when the animation status is completed. Returns true if the
/// interation is still ongoing.
bool onAnimationStatusCompleted() {
// restart animation if arrow keys are still pressed
return _updateKeyboardAnimation();
}

/// Handles keyboard events. Returns weather the event was handled.
bool _handleKeyEvent(KeyEvent event) {
// debugPrint('Keyboard event: $event');
if (_arrowKeys.contains(event.physicalKey) &&
(event is KeyDownEvent || event is KeyUpEvent)) {
_updateKeyboardAnimation();
return true;
}
return false;
}

bool _updateKeyboardAnimation() {
var direction = Offset.zero;
if (HardwareKeyboard.instance.isPhysicalKeyPressed(
PhysicalKeyboardKey.arrowUp,
)) {
direction += const Offset(0, -1);
}
if (HardwareKeyboard.instance.isPhysicalKeyPressed(
PhysicalKeyboardKey.arrowDown,
)) {
direction += const Offset(0, 1);
}
if (HardwareKeyboard.instance.isPhysicalKeyPressed(
PhysicalKeyboardKey.arrowLeft,
)) {
direction += const Offset(-1, 0);
}
if (HardwareKeyboard.instance.isPhysicalKeyPressed(
PhysicalKeyboardKey.arrowRight,
)) {
direction += const Offset(1, 0);
}
if (direction == Offset.zero) {
controller.animation = null;
controller.animationController.stop(canceled: false);
emitCameraIdleEvent();
return false;
}

// normalize direction
direction = Offset.fromDirection(direction.direction);
final camera = this.camera;
if (controller.animation == null) {
emitMoveStartEvent();
}
controller.animation =
MapCameraTween(
begin: camera,
end: camera.copyWith(
center: controller.toLngLat(
controller.toScreenLocation(camera.center) + direction * 300,
),
),
).animate(
CurvedAnimation(
parent: controller.animationController,
curve: Curves.linear,
),
);
controller.animationController
..duration = const Duration(seconds: 1)
..forward(from: 0);
return true;
}
}
Loading
Loading