In Flutter, a native map embedded in a scrolling property page will not pan because the page scrolls instead; how do gestureRecognizers fix it?
answer
- the platform view joins the gesture arena
- empty set: only unclaimed gestures
- ListView claims vertical drags first
- EagerGestureRecognizer takes everything
- narrow set shares gestures with the page
basics
~20 sA platform view only receives touches that no Flutter recognizer claims unless you list recognizers in gestureRecognizers. Adding an EagerGestureRecognizer gives the map every gesture starting on it; a narrower set, such as horizontal drags, lets the page keep vertical scrolling.
solid answer
~40 sPlatform views take part in Flutter's gesture arena, and the native view gets a pointer sequence only if it wins. With `gestureRecognizers` null or empty (the default), the map receives only sequences no other recognizer claimed — so the enclosing `ListView`'s vertical drag recognizer wins and the page scrolls. Passing recognizers in the set makes the platform view compete: if any of them wins, the **whole** pointer sequence from the down event goes to the native view. `Factory<OneSequenceGestureRecognizer>(() => EagerGestureRecognizer())` claims immediately on pointer down, so everything starting on the map pans the map — and the page can no longer be scrolled from that area. A narrower set, say a `HorizontalDragGestureRecognizer`, lets horizontal drags reach the map while vertical ones still scroll. `google_maps_flutter`'s `GoogleMap` and `webview_flutter`'s `WebViewWidget` forward the same parameter.
code
dart · 22 linesimport 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class HorizontalOnlyMap extends StatelessWidget {
const HorizontalOnlyMap({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 240,
child: AndroidView(
viewType: 'com.example.property/map',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<OneSequenceGestureRecognizer>(
() => HorizontalDragGestureRecognizer(),
),
},
),
);
}
}go deeper
Remember that a native view inside a scrolling page needs gestureRecognizers, commonly an EagerGestureRecognizer, to receive drags.
Explain that the platform view competes in the arena, that an empty set receives only unclaimed sequences, and that a win forwards the whole sequence.
Choose between eager, narrow and static-preview designs for usability, and recognise traps such as changing the set mid-gesture or trapping the user's scroll.
Set interaction guidelines for embedded native content across the app so maps, web views and players behave consistently inside scrollable layouts.
## The symptom A property detail page is a `ListView`: photos, price, description, and a native map of the location in a fixed-height box. Users try to drag the map and the whole page scrolls; pinch works poorly or not at all. Nothing is broken in the map SDK — the touches never reach it. ## Why: platform views join the gesture arena Flutter decides who owns a pointer sequence through its **gesture arena** (the arena's general rules are their own topic). The key facts for platform views, from the framework's own documentation of `AndroidView` and `UiKitView`: - The platform view participates in the arena and dispatches touch events to the native view **only if it wins**. - `gestureRecognizers` is a `Set<Factory<OneSequenceGestureRecognizer>>`. Recognizers built from these factories enter the arena for each pointer that lands on the view. - If **any** of them wins, the **entire** pointer sequence, starting from the pointer-down event, is sent to the native view. - When the set is **null or empty**, the native view receives a sequence only if **no other recognizer claimed it**. Inside a `ListView`, the scrollable's vertical drag recognizer is in the arena too. With the default empty set, it wins every vertical movement, so the page scrolls. ## Fix options | `gestureRecognizers` | Map receives | Page behaviour | |---|---|---| | empty (default) | only unclaimed sequences (taps, mostly) | scrolls from anywhere, including over the map | | `{Factory(() => EagerGestureRecognizer())}` | every sequence that starts on the map | cannot be scrolled by dragging on the map | | `{Factory(() => HorizontalDragGestureRecognizer())}` | horizontal drags | vertical drags still scroll the page | `EagerGestureRecognizer` is a special recognizer that claims the gesture immediately after pointer down. It is the usual choice for an interactive map, with enough page outside the map for users to scroll. ```dart import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class PropertyLocation extends StatelessWidget { const PropertyLocation({super.key, required this.location}); final LatLng location; @override Widget build(BuildContext context) { return SizedBox( height: 280, child: GoogleMap( initialCameraPosition: CameraPosition(target: location, zoom: 15), gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{ Factory<OneSequenceGestureRecognizer>(() => EagerGestureRecognizer()), }, ), ); } } ``` ## Design choices around it 1. **Leave scroll room.** With an eager map, users need space outside it to scroll; a full-height map inside a list traps them. 2. **Consider a static preview.** Many property pages show a non-interactive map (empty set) and open a full-screen interactive map on tap — no conflict at all. 3. **Do not duplicate types.** The set must not contain two factories with the same recognizer type. 4. **Changing the recognizer types mid-gesture** rejects any active arenas the view is in; rebuilding with an equal set of types is not a change, but swapping types while a finger is down cancels the gesture. 5. **`hitTestBehavior`** (default `opaque`) decides whether the view absorbs hits; it is not the same as winning the arena. ## iOS note On iOS, Flutter also has to stop the `UIView`'s own `UIGestureRecognizer`s when Flutter wins. `UiKitView` has a `gestureBlockingPolicy` (`UiKitViewGestureBlockingPolicy`, default `fallbackToPluginDefault`) for native views whose recognizers misbehave under that blocking; most apps never change it. ## Common wrong fixes - Wrapping the map in a `GestureDetector` with drag callbacks — that adds another competitor and makes things worse. - Setting `physics: NeverScrollableScrollPhysics()` on the page to "let the map win" — the page then cannot scroll at all.
- What does the native view receive when one of its forwarded recognizers wins?The entire pointer event sequence, starting from the pointer-down event, not just the events after the win. That is why the native map can treat the drag as its own from the first touch.
- When is a static map preview better than an eager interactive map on a scrolling page?When the map is secondary information and the page is long. A preview with no forwarded recognizers never fights the scroll, costs less while scrolling, and a tap can open a full-screen interactive map where eager gestures are harmless.
saying these in an interview costs you the question
- An empty gestureRecognizers set sends every touch to the native view.
- Wrapping the map in a GestureDetector makes it receive drags.
- The map SDK itself must be patched to work inside a ListView.
- hitTestBehavior alone decides which recognizer wins the arena.
- EagerGestureRecognizer still lets the page scroll from over the map.