From cb2a0dfe224728576cde7549ef80672f85c10750 Mon Sep 17 00:00:00 2001 From: pinpong Date: Sun, 12 Apr 2026 15:15:55 +0800 Subject: [PATCH] fix(android): handle touch interception for maps nested in ScrollView Closes #100 --- .../rngooglemapsplus/GoogleMapsViewImpl.kt | 51 +++++++++ example/src/App.tsx | 6 ++ example/src/components/MapWrapper.tsx | 11 +- example/src/screens/HomeScreen.tsx | 1 + example/src/screens/ScrollViewScreen.tsx | 100 ++++++++++++++++++ example/src/types/navigation.ts | 1 + 6 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 example/src/screens/ScrollViewScreen.tsx diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 1ad1726..c52c259 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -10,6 +10,7 @@ import android.content.res.Configuration import android.graphics.Bitmap import android.location.Location import android.util.Size +import android.view.MotionEvent import android.view.View import android.widget.FrameLayout import androidx.lifecycle.Lifecycle @@ -106,6 +107,7 @@ class GoogleMapsViewImpl( private val kmlLayersById = mutableMapOf() private val urlTileOverlaysById = mutableMapOf() + private var parentTouchInterceptDisallowed = false private var cameraMoveReason = -1 val componentCallbacks = @@ -123,6 +125,54 @@ class GoogleMapsViewImpl( } } + private fun setParentTouchInterceptDisallowed(blocked: Boolean) { + if (parentTouchInterceptDisallowed == blocked) return + parentTouchInterceptDisallowed = blocked + var p = parent + while (p != null) { + p.requestDisallowInterceptTouchEvent(blocked) + p = p.parent + } + } + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + if (googleMapsOptions.liteMode == true) return super.dispatchTouchEvent(ev) + + val panEnabled = uiSettings?.scrollEnabled == true + val zoomEnabled = uiSettings?.zoomGesturesEnabled == true + val rotateEnabled = uiSettings?.rotateEnabled == true + val tiltEnabled = uiSettings?.tiltEnabled == true + + val multiTouchEnabled = zoomEnabled || rotateEnabled || tiltEnabled + val anyMapGestureEnabled = panEnabled || multiTouchEnabled + if (!anyMapGestureEnabled) return super.dispatchTouchEvent(ev) + + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_MOVE, + MotionEvent.ACTION_POINTER_DOWN, + -> { + val pointers = ev.pointerCount + val shouldBlockParent = pointers >= (if (panEnabled) 1 else 2) + setParentTouchInterceptDisallowed(shouldBlockParent) + } + + MotionEvent.ACTION_POINTER_UP -> { + val pointers = ev.pointerCount - 1 + val shouldBlockParent = pointers >= (if (panEnabled) 1 else 2) + setParentTouchInterceptDisallowed(shouldBlockParent) + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL, + -> { + setParentTouchInterceptDisallowed(false) + } + } + + return super.dispatchTouchEvent(ev) + } + init { MapsInitializer.initialize(reactContext) reactContext.registerComponentCallbacks(componentCallbacks) @@ -876,6 +926,7 @@ class GoogleMapsViewImpl( } override fun onDetachedFromWindow() { + setParentTouchInterceptDisallowed(false) lifecycleObserver?.let { lifecycle?.removeObserver(it) } lifecycle = null super.onDetachedFromWindow() diff --git a/example/src/App.tsx b/example/src/App.tsx index 9d52413..feb46c4 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -26,6 +26,7 @@ import LocationScreen from '@src/screens/LocationScreen'; import MarkersScreen from '@src/screens/MarkersScreen'; import PolygonsScreen from '@src/screens/PolygonsScreen'; import PolylinesScreen from '@src/screens/PolylinesScreen'; +import ScrollViewScreen from '@src/screens/ScrollViewScreen'; import SnapshotTestScreen from '@src/screens/SnaptshotTestScreen'; import StressTestScreen from '@src/screens/StressTestScreen'; import SvgMarkersScreen from '@src/screens/SvgMarkersScreen'; @@ -70,6 +71,11 @@ export default function App() { component={BasicMapScreen} options={{ title: 'Basic Map' }} /> + + backgroundColor: theme.bgPrimary, }, map: { - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, + height: '100%', + width: '100%', }, loadingOverlay: { ...StyleSheet.absoluteFill, diff --git a/example/src/screens/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index fa26f0a..e77c909 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -18,6 +18,7 @@ import type { const screens = [ { name: 'BasicMap', title: 'Basic Map' }, + { name: 'ScrollView', title: 'Map in ScrollView' }, { name: 'Markers', title: 'Markers' }, { name: 'SvgMarkers', title: 'SVG Markers' }, { name: 'Polygons', title: 'Polygons' }, diff --git a/example/src/screens/ScrollViewScreen.tsx b/example/src/screens/ScrollViewScreen.tsx new file mode 100644 index 0000000..505233f --- /dev/null +++ b/example/src/screens/ScrollViewScreen.tsx @@ -0,0 +1,100 @@ +import React, { useMemo, useRef } from 'react'; + +import { Platform, ScrollView, StyleSheet, View } from 'react-native'; + +import { + type EdgeInsets, + useSafeAreaInsets, +} from 'react-native-safe-area-context'; + +import MapWrapper from '@src/components/MapWrapper'; +import { useAppTheme } from '@src/hooks/useAppTheme'; +import type { AppTheme } from '@src/theme'; + +import type { + GoogleMapsViewRef, + RNLatLng, +} from 'react-native-google-maps-plus'; + +const LOCATIONS: RNLatLng[] = [ + { latitude: 37.7749, longitude: -122.4194 }, // San Francisco + { latitude: 40.7128, longitude: -74.006 }, // New York City + { latitude: 52.52, longitude: 13.405 }, // Berlin + { latitude: 48.8566, longitude: 2.3522 }, // Paris + { latitude: 35.6762, longitude: 139.6503 }, // Tokyo + { latitude: -33.8688, longitude: 151.2093 }, // Sydney + { latitude: 51.5074, longitude: -0.1278 }, // London + { latitude: 55.7558, longitude: 37.6173 }, // Moscow + { latitude: 19.4326, longitude: -99.1332 }, // Mexico City + { latitude: -23.5505, longitude: -46.6333 }, // São Paulo +]; + +export default function ScrollViewScreen() { + const theme = useAppTheme(); + const layout = useSafeAreaInsets(); + const styles = useMemo(() => getThemedStyles(theme, layout), [theme, layout]); + const mapRef = useRef(null); + + return ( + + {LOCATIONS.map((center, i) => { + const isLite = i % 2 !== 0; + return ( + + + + ); + })} + + ); +} + +const getThemedStyles = (theme: AppTheme, layout: EdgeInsets) => + StyleSheet.create({ + scrollView: { + flex: 1, + backgroundColor: theme.bgPrimary, + }, + content: { + padding: 16, + paddingBottom: layout.bottom + 24, + gap: 20, + }, + card: { + borderRadius: 10, + overflow: 'hidden', + backgroundColor: theme.bgHeader, + shadowColor: theme.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 2, + }, + map: { + height: 250, + }, + }); diff --git a/example/src/types/navigation.ts b/example/src/types/navigation.ts index f8a2553..706a12d 100644 --- a/example/src/types/navigation.ts +++ b/example/src/types/navigation.ts @@ -4,6 +4,7 @@ export type RootStackParamList = { Home: undefined; Blank: undefined; BasicMap: undefined; + ScrollView: undefined; Markers: undefined; SvgMarkers: undefined; Polygons: undefined;