Skip to content

fix(android): MarkerView no longer blocks map pan/pinch (new arch)#4258

Open
mfazekas wants to merge 1 commit into
mainfrom
claude/markerview-gesture-android-01b1ab
Open

fix(android): MarkerView no longer blocks map pan/pinch (new arch)#4258
mfazekas wants to merge 1 commit into
mainfrom
claude/markerview-gesture-android-01b1ab

Conversation

@mfazekas

@mfazekas mfazekas commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #4255

On Android with the new architecture (Fabric), a MarkerView blocks map pan/pinch: a gesture that starts on a marker never reaches the map, so the marker acts as a dead zone. iOS is unaffected.

Root cause: a side effect of the touch fix in #4176, which called requestDisallowInterceptTouchEvent(true) on every ACTION_DOWN to keep child Pressables tappable. That also stopped the map from ever seeing the gesture — and, for pinch, from seeing the second finger's ACTION_POINTER_DOWN.

Fix — decide gesture ownership at touch-down from what's under the finger, instead of always grabbing:

  1. Touch on a scrollable descendant (ScrollView/FlatList/ViewPager, detected via canScrollHorizontally/Vertically) → disallow map interception so the child keeps the gesture. The map is an ancestor and would otherwise steal the drag on its own touch-slop before the child could claim it.
  2. Anything else → leave interception alone. Taps still reach child Pressables (Mapbox uses its own touch-slop, so a tap isn't intercepted), while a pan/pinch that merely starts on the marker moves the map.

Two more things:

  • A tap that lands on a MarkerView is absorbed by the marker rather than falling through to touchable sources (symbol/shape layers) or pins underneath it. pointerEvents="none"/"box-none" markers stay transparent.
  • New stopGesturePropagation prop (Android-only): children that handle drags purely in JS (PanResponder, e.g. some sliders) never claim the native gesture and can be taken over by the map — a nondeterministic race. Setting stopGesturePropagation makes the marker keep every gesture. iOS gesture recognisers already coexist with marker content, so it's a no-op there (mirrors react-native-maps' iOS-only stopPropagation, inverted).

Ran yarn generate (docs updated). Native + JS change; no style-spec changes.

Checklist

  • I've read CONTRIBUTING.md
  • I updated the doc/other generated code with running yarn generate in the root folder
  • I have tested the new feature on /example app.
    • In V11 mode/ios
    • In New Architecture mode/ios
    • In V11 mode/android
    • In New Architecture mode/android
  • I added/updated a sample (/example Marker View: scrollable-in-marker, feature-under-marker, and a stopGesturePropagation toggle)

Screenshot OR Video

Verified on a physical Android device (New Architecture), driving the same gestures before/after:

  • Drag starting on a (non-scrollable) marker — before: map frozen; after: map pans.
  • Tap child Pressable/counter — works; tap on marker over a feature — feature underneath is not selected; the same feature just outside the marker still is.
  • Native ScrollView inside a marker — scrolls the child, map does not pan.
  • @rneui slider (JS PanResponder) inside a marker — races the map (nondeterministic; reproduced both a clean drag and a leak on the same slider). With stopGesturePropagation, three consecutive slider drags never pan the map.
  • pointerEvents="none" — tap/drag still passes through and selects the feature underneath.

Pinch-zoom starting on a marker zooms the map (confirmed on device) — the marker no longer grabs on DOWN for non-scrollable content, so the map sees the whole gesture including the second finger.

Component to reproduce the issue you're fixing

Reproducer — pan-through, tap shield, scrollable, and slider opt-in

Drag on the pink marker: on the unfixed build the map doesn't move; with the fix it pans. Tapping the marker doesn't select the red circle underneath. The horizontal strip scrolls without panning the map. The @rneui slider may occasionally hand the drag to the map — set stopGesturePropagation to make it keep the gesture every time.

import React from 'react';
import { View, Text, Pressable, ScrollView, StyleSheet } from 'react-native';
import Mapbox, { MapView, Camera, MarkerView, ShapeSource, CircleLayer } from '@rnmapbox/maps';

const CENTER: [number, number] = [-122.4194, 37.7749];

export default function MarkerViewGestureRepro(): React.JSX.Element {
  const [mapMoved, setMapMoved] = React.useState(0);
  const [featurePressed, setFeaturePressed] = React.useState(0);

  const shape: GeoJSON.FeatureCollection = {
    type: 'FeatureCollection',
    features: [{ type: 'Feature', properties: {}, geometry: { type: 'Point', coordinates: CENTER } }],
  };

  return (
    <View style={styles.container}>
      <MapView style={styles.map} onRegionDidChange={() => setMapMoved((c) => c + 1)}>
        <Camera defaultSettings={{ centerCoordinate: CENTER, zoomLevel: 13 }} />

        <ShapeSource id="under" shape={shape} onPress={() => setFeaturePressed((c) => c + 1)}>
          <CircleLayer id="under-circle" style={{ circleRadius: 60, circleColor: 'rgba(255,0,0,0.35)' }} />
        </ShapeSource>

        <MarkerView coordinate={CENTER} anchor={{ x: 0.5, y: 0.5 }} allowOverlap>
          <Pressable style={styles.marker} onPress={() => {}}>
            <Text style={styles.markerText}>TAP</Text>
          </Pressable>
        </MarkerView>

        {/* native scrollable: scrolls without panning the map */}
        <MarkerView coordinate={CENTER} anchor={{ x: 0.5, y: 2 }} allowOverlap>
          <View style={styles.strip} collapsable={false}>
            <ScrollView horizontal>
              {Array.from({ length: 12 }).map((_, i) => (
                <View key={i} style={[styles.box, { backgroundColor: `hsl(${i * 30},70%,60%)` }]}>
                  <Text style={styles.boxText}>{i}</Text>
                </View>
              ))}
            </ScrollView>
          </View>
        </MarkerView>
      </MapView>

      <View style={styles.hud} pointerEvents="none">
        <Text>Map moved: {mapMoved}</Text>
        <Text>Feature (under marker) pressed: {featurePressed}</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  map: { flex: 1 },
  hud: { position: 'absolute', bottom: 24, left: 16, backgroundColor: 'white', padding: 8, borderRadius: 8 },
  marker: { width: 64, height: 64, borderRadius: 32, backgroundColor: '#ff2d87', borderWidth: 3, borderColor: '#fff', alignItems: 'center', justifyContent: 'center' },
  markerText: { color: '#fff', fontWeight: '800' },
  strip: { width: 180, height: 54, backgroundColor: '#fff', borderRadius: 8, borderWidth: 1, borderColor: '#333' },
  box: { width: 50, height: 50, margin: 2, borderRadius: 6, alignItems: 'center', justifyContent: 'center' },
  boxText: { color: '#fff', fontWeight: 'bold' },
});

On Android with Fabric, a MarkerView captured the whole touch stream, so a
pan or pinch that started on a marker never reached the map (#4255). This was a
side effect of the touch fix in #4176, which called
requestDisallowInterceptTouchEvent(true) on every ACTION_DOWN to keep child
Pressables tappable — that also prevented the map from ever seeing the gesture
(and, for pinch, the second finger's ACTION_POINTER_DOWN).

Decide gesture ownership at touch-down based on what is under the finger:

- Touch on a scrollable descendant (ScrollView/FlatList/ViewPager, detected via
  canScroll*): disallow map interception so the child keeps the gesture. The map
  is an ancestor and would otherwise steal the drag on its own touch slop before
  the child could claim it.
- Otherwise: leave interception alone. Taps still reach child Pressables (Mapbox
  uses its own touch slop, so a tap is not intercepted) while a pan/pinch that
  merely starts on the marker moves the map.

A tap that lands on a MarkerView is absorbed by the marker rather than falling
through to touchable sources (symbol/shape layers) or pins underneath it.
pointerEvents="none"/"box-none" markers stay transparent as before.

Children that handle drags purely in JS (PanResponder, e.g. some sliders) never
claim the native gesture and can be taken over by the map. For those, add a
`stopGesturePropagation` prop: when set, the marker keeps every gesture. It is
Android-only (iOS gesture recognisers already coexist with marker content).
@mfazekas
mfazekas force-pushed the claude/markerview-gesture-android-01b1ab branch from c593669 to b12d6e3 Compare July 20, 2026 06:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Android: can't pan / pinch-zoom the map when the gesture starts on a MarkerView (new arch)

1 participant