skip to content

In Flutter, a native map embedded in a scrolling property page will not pan because the page scrolls instead; how do gestureRecognizers fix it?

level: middleimportance: should knowfreq 38%

answer

  1. the platform view joins the gesture arena
  2. empty set: only unclaimed gestures
  3. ListView claims vertical drags first
  4. EagerGestureRecognizer takes everything
  5. narrow set shares gestures with the page

basics

~20 s

A 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 s

Platform 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 lines
dart
import '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

for a junior

Remember that a native view inside a scrolling page needs gestureRecognizers, commonly an EagerGestureRecognizer, to receive drags.

for a middle

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.

for a senior

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.

for a principal

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.