Skip to content

Commit a4c30ad

Browse files
authored
feat: added keyboard controls for map gestures (#1987)
1 parent d197dba commit a4c30ad

File tree

8 files changed

+989
-112
lines changed

8 files changed

+989
-112
lines changed

example/lib/pages/interactive_test_page.dart

Lines changed: 225 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,16 @@ class InteractiveFlagsPage extends StatefulWidget {
1414
}
1515

1616
class _InteractiveFlagsPageState extends State<InteractiveFlagsPage> {
17-
static const availableFlags = {
18-
'Movement': {
19-
InteractiveFlag.drag: 'Drag',
20-
InteractiveFlag.flingAnimation: 'Fling',
21-
InteractiveFlag.pinchMove: 'Pinch',
22-
},
23-
'Zooming': {
24-
InteractiveFlag.pinchZoom: 'Pinch',
25-
InteractiveFlag.scrollWheelZoom: 'Scroll',
26-
InteractiveFlag.doubleTapZoom: 'Double tap',
27-
InteractiveFlag.doubleTapDragZoom: '+ drag',
28-
},
29-
'Rotation': {
30-
InteractiveFlag.rotate: 'Twist',
31-
},
32-
};
17+
final flagsSet =
18+
ValueNotifier(InteractiveFlag.drag | InteractiveFlag.pinchZoom);
3319

34-
int flags = InteractiveFlag.drag | InteractiveFlag.pinchZoom;
3520
bool keyboardCursorRotate = false;
21+
bool keyboardArrowsMove = false;
22+
bool keyboardWASDMove = false;
23+
bool keyboardQERotate = false;
24+
bool keyboardRFZoom = false;
3625

3726
MapEvent? _latestEvent;
38-
3927
@override
4028
Widget build(BuildContext context) {
4129
final screenWidth = MediaQuery.sizeOf(context).width;
@@ -50,58 +38,181 @@ class _InteractiveFlagsPageState extends State<InteractiveFlagsPage> {
5038
direction: screenWidth >= 600 ? Axis.horizontal : Axis.vertical,
5139
mainAxisSize: MainAxisSize.max,
5240
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
53-
children: availableFlags.entries
54-
.map<Widget?>(
55-
(category) => Column(
41+
children: [
42+
Column(
43+
children: [
44+
const Text(
45+
'Move/Pan',
46+
style: TextStyle(
47+
fontWeight: FontWeight.bold,
48+
),
49+
),
50+
const SizedBox(height: 6),
51+
Row(
52+
mainAxisSize: MainAxisSize.min,
53+
crossAxisAlignment: CrossAxisAlignment.start,
5654
children: [
57-
Text(
58-
category.key,
59-
style: const TextStyle(fontWeight: FontWeight.bold),
55+
InteractiveFlagCheckbox(
56+
name: 'Drag',
57+
flag: InteractiveFlag.drag,
58+
flagsSet: flagsSet,
59+
),
60+
const SizedBox(width: 8),
61+
InteractiveFlagCheckbox(
62+
name: 'Fling',
63+
flag: InteractiveFlag.flingAnimation,
64+
flagsSet: flagsSet,
6065
),
61-
Row(
62-
mainAxisSize: MainAxisSize.min,
63-
children: <Widget>[
64-
...category.value.entries.map(
65-
(e) => Column(
66-
children: [
67-
Checkbox.adaptive(
68-
value:
69-
InteractiveFlag.hasFlag(e.key, flags),
70-
onChanged: (enabled) {
71-
if (!enabled!) {
72-
setState(() => flags &= ~e.key);
73-
return;
74-
}
75-
setState(() => flags |= e.key);
76-
},
77-
),
78-
Text(e.value),
79-
],
66+
const SizedBox(width: 8),
67+
InteractiveFlagCheckbox(
68+
name: 'Pinch',
69+
flag: InteractiveFlag.pinchMove,
70+
flagsSet: flagsSet,
71+
),
72+
const SizedBox(width: 8),
73+
Column(
74+
children: [
75+
Checkbox.adaptive(
76+
value: keyboardArrowsMove,
77+
onChanged: (enabled) => setState(
78+
() => keyboardArrowsMove = enabled!,
8079
),
8180
),
82-
if (category.key == 'Rotation') ...[
83-
Column(
84-
children: [
85-
Checkbox.adaptive(
86-
value: keyboardCursorRotate,
87-
onChanged: (enabled) => setState(
88-
() => keyboardCursorRotate = enabled!),
89-
),
90-
const Text('Cursor & CTRL'),
91-
],
81+
const Text(
82+
'Keyboard\nArrows',
83+
textAlign: TextAlign.center,
84+
),
85+
],
86+
),
87+
const SizedBox(width: 8),
88+
Column(
89+
children: [
90+
Checkbox.adaptive(
91+
value: keyboardWASDMove,
92+
onChanged: (enabled) => setState(
93+
() => keyboardWASDMove = enabled!,
9294
),
93-
]
94-
].interleave(const SizedBox(width: 12)).toList()
95-
..removeLast(),
96-
)
95+
),
96+
const Text(
97+
'Keyboard\nW/A/S/D',
98+
textAlign: TextAlign.center,
99+
),
100+
],
101+
),
97102
],
103+
)
104+
],
105+
),
106+
const SizedBox(width: 12),
107+
Column(
108+
children: [
109+
const Text(
110+
'Zoom',
111+
style: TextStyle(
112+
fontWeight: FontWeight.bold,
113+
),
98114
),
99-
)
100-
.interleave(
101-
screenWidth >= 600 ? null : const SizedBox(height: 12),
102-
)
103-
.whereType<Widget>()
104-
.toList(),
115+
const SizedBox(height: 6),
116+
Row(
117+
mainAxisSize: MainAxisSize.min,
118+
crossAxisAlignment: CrossAxisAlignment.start,
119+
children: [
120+
InteractiveFlagCheckbox(
121+
name: 'Pinch',
122+
flag: InteractiveFlag.pinchZoom,
123+
flagsSet: flagsSet,
124+
),
125+
const SizedBox(width: 8),
126+
InteractiveFlagCheckbox(
127+
name: 'Scroll',
128+
flag: InteractiveFlag.scrollWheelZoom,
129+
flagsSet: flagsSet,
130+
),
131+
const SizedBox(width: 8),
132+
InteractiveFlagCheckbox(
133+
name: 'Double tap',
134+
flag: InteractiveFlag.doubleTapZoom,
135+
flagsSet: flagsSet,
136+
),
137+
const SizedBox(width: 8),
138+
InteractiveFlagCheckbox(
139+
name: '+ drag',
140+
flag: InteractiveFlag.doubleTapDragZoom,
141+
flagsSet: flagsSet,
142+
),
143+
const SizedBox(width: 8),
144+
Column(
145+
children: [
146+
Checkbox.adaptive(
147+
value: keyboardRFZoom,
148+
onChanged: (enabled) => setState(
149+
() => keyboardRFZoom = enabled!,
150+
),
151+
),
152+
const Text(
153+
'Keyboard\nR/F',
154+
textAlign: TextAlign.center,
155+
),
156+
],
157+
),
158+
],
159+
)
160+
],
161+
),
162+
const SizedBox(width: 12),
163+
Column(
164+
children: [
165+
const Text(
166+
'Rotate',
167+
style: TextStyle(
168+
fontWeight: FontWeight.bold,
169+
),
170+
),
171+
const SizedBox(height: 6),
172+
Row(
173+
mainAxisSize: MainAxisSize.min,
174+
crossAxisAlignment: CrossAxisAlignment.start,
175+
children: [
176+
InteractiveFlagCheckbox(
177+
name: 'Twist',
178+
flag: InteractiveFlag.rotate,
179+
flagsSet: flagsSet,
180+
),
181+
const SizedBox(width: 8),
182+
Column(
183+
children: [
184+
Checkbox.adaptive(
185+
value: keyboardCursorRotate,
186+
onChanged: (enabled) => setState(
187+
() => keyboardCursorRotate = enabled!,
188+
),
189+
),
190+
const Text(
191+
'Cursor\n& CTRL',
192+
textAlign: TextAlign.center,
193+
),
194+
],
195+
),
196+
const SizedBox(width: 8),
197+
Column(
198+
children: [
199+
Checkbox.adaptive(
200+
value: keyboardQERotate,
201+
onChanged: (enabled) => setState(
202+
() => keyboardQERotate = enabled!,
203+
),
204+
),
205+
const Text(
206+
'Keyboard\nQ/E',
207+
textAlign: TextAlign.center,
208+
),
209+
],
210+
),
211+
],
212+
)
213+
],
214+
),
215+
],
105216
),
106217
const Divider(),
107218
Padding(
@@ -115,23 +226,33 @@ class _InteractiveFlagsPageState extends State<InteractiveFlagsPage> {
115226
),
116227
),
117228
Expanded(
118-
child: FlutterMap(
119-
options: MapOptions(
120-
onMapEvent: (evt) => setState(() => _latestEvent = evt),
121-
initialCenter: const LatLng(51.5, -0.09),
122-
initialZoom: 11,
123-
interactionOptions: InteractionOptions(
124-
flags: flags,
125-
cursorKeyboardRotationOptions:
126-
CursorKeyboardRotationOptions(
127-
isKeyTrigger: (key) =>
128-
keyboardCursorRotate &&
129-
CursorKeyboardRotationOptions.defaultTriggerKeys
130-
.contains(key),
229+
child: ValueListenableBuilder(
230+
valueListenable: flagsSet,
231+
builder: (context, value, child) => FlutterMap(
232+
options: MapOptions(
233+
onMapEvent: (evt) => setState(() => _latestEvent = evt),
234+
initialCenter: const LatLng(51.5, -0.09),
235+
initialZoom: 11,
236+
interactionOptions: InteractionOptions(
237+
flags: value,
238+
cursorKeyboardRotationOptions:
239+
CursorKeyboardRotationOptions(
240+
isKeyTrigger: (key) =>
241+
keyboardCursorRotate &&
242+
CursorKeyboardRotationOptions.defaultTriggerKeys
243+
.contains(key),
244+
),
245+
keyboardOptions: KeyboardOptions(
246+
enableArrowKeysPanning: keyboardArrowsMove,
247+
enableWASDPanning: keyboardWASDMove,
248+
enableQERotating: keyboardQERotate,
249+
enableRFZooming: keyboardRFZoom,
250+
),
131251
),
132252
),
253+
children: [child!],
133254
),
134-
children: [openStreetMapTileLayer],
255+
child: openStreetMapTileLayer,
135256
),
136257
),
137258
],
@@ -186,11 +307,32 @@ class _InteractiveFlagsPageState extends State<InteractiveFlagsPage> {
186307
}
187308
}
188309

189-
extension _IterableExt<E> on Iterable<E> {
190-
Iterable<E> interleave(E separator) sync* {
191-
for (int i = 0; i < length; i++) {
192-
yield elementAt(i);
193-
if (i < length) yield separator;
194-
}
310+
class InteractiveFlagCheckbox extends StatelessWidget {
311+
const InteractiveFlagCheckbox({
312+
super.key,
313+
required this.name,
314+
required this.flag,
315+
required this.flagsSet,
316+
});
317+
318+
final String name;
319+
final int flag;
320+
final ValueNotifier<int> flagsSet;
321+
322+
@override
323+
Widget build(BuildContext context) {
324+
return Column(
325+
children: [
326+
ValueListenableBuilder(
327+
valueListenable: flagsSet,
328+
builder: (context, value, _) => Checkbox.adaptive(
329+
value: InteractiveFlag.hasFlag(flag, value),
330+
onChanged: (enabled) =>
331+
flagsSet.value = !enabled! ? value &= ~flag : value |= flag,
332+
),
333+
),
334+
Text(name),
335+
],
336+
);
195337
}
196338
}

lib/flutter_map.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ export 'package:flutter_map/src/map/controller/map_controller.dart';
5757
export 'package:flutter_map/src/map/controller/map_controller_impl.dart';
5858
export 'package:flutter_map/src/map/options/cursor_keyboard_rotation.dart';
5959
export 'package:flutter_map/src/map/options/interaction.dart';
60+
export 'package:flutter_map/src/map/options/keyboard.dart';
6061
export 'package:flutter_map/src/map/options/options.dart';
6162
export 'package:flutter_map/src/map/widget.dart';

0 commit comments

Comments
 (0)