Skip to content

Commit cb2a0df

Browse files
committed
fix(android): handle touch interception for maps nested in ScrollView
Closes #100
1 parent 21adddb commit cb2a0df

6 files changed

Lines changed: 163 additions & 7 deletions

File tree

android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import android.content.res.Configuration
1010
import android.graphics.Bitmap
1111
import android.location.Location
1212
import android.util.Size
13+
import android.view.MotionEvent
1314
import android.view.View
1415
import android.widget.FrameLayout
1516
import androidx.lifecycle.Lifecycle
@@ -106,6 +107,7 @@ class GoogleMapsViewImpl(
106107
private val kmlLayersById = mutableMapOf<String, KmlLayer>()
107108
private val urlTileOverlaysById = mutableMapOf<String, TileOverlay>()
108109

110+
private var parentTouchInterceptDisallowed = false
109111
private var cameraMoveReason = -1
110112

111113
val componentCallbacks =
@@ -123,6 +125,54 @@ class GoogleMapsViewImpl(
123125
}
124126
}
125127

128+
private fun setParentTouchInterceptDisallowed(blocked: Boolean) {
129+
if (parentTouchInterceptDisallowed == blocked) return
130+
parentTouchInterceptDisallowed = blocked
131+
var p = parent
132+
while (p != null) {
133+
p.requestDisallowInterceptTouchEvent(blocked)
134+
p = p.parent
135+
}
136+
}
137+
138+
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
139+
if (googleMapsOptions.liteMode == true) return super.dispatchTouchEvent(ev)
140+
141+
val panEnabled = uiSettings?.scrollEnabled == true
142+
val zoomEnabled = uiSettings?.zoomGesturesEnabled == true
143+
val rotateEnabled = uiSettings?.rotateEnabled == true
144+
val tiltEnabled = uiSettings?.tiltEnabled == true
145+
146+
val multiTouchEnabled = zoomEnabled || rotateEnabled || tiltEnabled
147+
val anyMapGestureEnabled = panEnabled || multiTouchEnabled
148+
if (!anyMapGestureEnabled) return super.dispatchTouchEvent(ev)
149+
150+
when (ev.actionMasked) {
151+
MotionEvent.ACTION_DOWN,
152+
MotionEvent.ACTION_MOVE,
153+
MotionEvent.ACTION_POINTER_DOWN,
154+
-> {
155+
val pointers = ev.pointerCount
156+
val shouldBlockParent = pointers >= (if (panEnabled) 1 else 2)
157+
setParentTouchInterceptDisallowed(shouldBlockParent)
158+
}
159+
160+
MotionEvent.ACTION_POINTER_UP -> {
161+
val pointers = ev.pointerCount - 1
162+
val shouldBlockParent = pointers >= (if (panEnabled) 1 else 2)
163+
setParentTouchInterceptDisallowed(shouldBlockParent)
164+
}
165+
166+
MotionEvent.ACTION_UP,
167+
MotionEvent.ACTION_CANCEL,
168+
-> {
169+
setParentTouchInterceptDisallowed(false)
170+
}
171+
}
172+
173+
return super.dispatchTouchEvent(ev)
174+
}
175+
126176
init {
127177
MapsInitializer.initialize(reactContext)
128178
reactContext.registerComponentCallbacks(componentCallbacks)
@@ -876,6 +926,7 @@ class GoogleMapsViewImpl(
876926
}
877927

878928
override fun onDetachedFromWindow() {
929+
setParentTouchInterceptDisallowed(false)
879930
lifecycleObserver?.let { lifecycle?.removeObserver(it) }
880931
lifecycle = null
881932
super.onDetachedFromWindow()

example/src/App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import LocationScreen from '@src/screens/LocationScreen';
2626
import MarkersScreen from '@src/screens/MarkersScreen';
2727
import PolygonsScreen from '@src/screens/PolygonsScreen';
2828
import PolylinesScreen from '@src/screens/PolylinesScreen';
29+
import ScrollViewScreen from '@src/screens/ScrollViewScreen';
2930
import SnapshotTestScreen from '@src/screens/SnaptshotTestScreen';
3031
import StressTestScreen from '@src/screens/StressTestScreen';
3132
import SvgMarkersScreen from '@src/screens/SvgMarkersScreen';
@@ -70,6 +71,11 @@ export default function App() {
7071
component={BasicMapScreen}
7172
options={{ title: 'Basic Map' }}
7273
/>
74+
<Stack.Screen
75+
name="ScrollView"
76+
component={ScrollViewScreen}
77+
options={{ title: 'Map in ScrollView' }}
78+
/>
7379
<Stack.Screen
7480
name="Markers"
7581
component={MarkersScreen}

example/src/components/MapWrapper.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export default function MapWrapper(props: Props) {
9797
const mapCallbacks = useMapCallbacks(props, props.mapRef, setMapLoaded);
9898

9999
return (
100-
<View style={styles.container}>
100+
<View style={[styles.container, props.style]}>
101101
<GoogleMapsView
102102
{...rest}
103103
initialProps={props.initialProps ?? initialProps}
@@ -106,7 +106,7 @@ export default function MapWrapper(props: Props) {
106106
trafficEnabled={props.trafficEnabled ?? false}
107107
indoorEnabled={props.indoorEnabled ?? false}
108108
transitEnabled={props.transitEnabled ?? false}
109-
style={[styles.map, props.style]}
109+
style={styles.map}
110110
userInterfaceStyle={
111111
props.userInterfaceStyle ??
112112
(theme.theme === 'dark' ? 'dark' : 'light')
@@ -134,11 +134,8 @@ const getThemedStyles = (theme: AppTheme) =>
134134
backgroundColor: theme.bgPrimary,
135135
},
136136
map: {
137-
position: 'absolute',
138-
left: 0,
139-
right: 0,
140-
top: 0,
141-
bottom: 0,
137+
height: '100%',
138+
width: '100%',
142139
},
143140
loadingOverlay: {
144141
...StyleSheet.absoluteFill,

example/src/screens/HomeScreen.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818

1919
const screens = [
2020
{ name: 'BasicMap', title: 'Basic Map' },
21+
{ name: 'ScrollView', title: 'Map in ScrollView' },
2122
{ name: 'Markers', title: 'Markers' },
2223
{ name: 'SvgMarkers', title: 'SVG Markers' },
2324
{ name: 'Polygons', title: 'Polygons' },
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React, { useMemo, useRef } from 'react';
2+
3+
import { Platform, ScrollView, StyleSheet, View } from 'react-native';
4+
5+
import {
6+
type EdgeInsets,
7+
useSafeAreaInsets,
8+
} from 'react-native-safe-area-context';
9+
10+
import MapWrapper from '@src/components/MapWrapper';
11+
import { useAppTheme } from '@src/hooks/useAppTheme';
12+
import type { AppTheme } from '@src/theme';
13+
14+
import type {
15+
GoogleMapsViewRef,
16+
RNLatLng,
17+
} from 'react-native-google-maps-plus';
18+
19+
const LOCATIONS: RNLatLng[] = [
20+
{ latitude: 37.7749, longitude: -122.4194 }, // San Francisco
21+
{ latitude: 40.7128, longitude: -74.006 }, // New York City
22+
{ latitude: 52.52, longitude: 13.405 }, // Berlin
23+
{ latitude: 48.8566, longitude: 2.3522 }, // Paris
24+
{ latitude: 35.6762, longitude: 139.6503 }, // Tokyo
25+
{ latitude: -33.8688, longitude: 151.2093 }, // Sydney
26+
{ latitude: 51.5074, longitude: -0.1278 }, // London
27+
{ latitude: 55.7558, longitude: 37.6173 }, // Moscow
28+
{ latitude: 19.4326, longitude: -99.1332 }, // Mexico City
29+
{ latitude: -23.5505, longitude: -46.6333 }, // São Paulo
30+
];
31+
32+
export default function ScrollViewScreen() {
33+
const theme = useAppTheme();
34+
const layout = useSafeAreaInsets();
35+
const styles = useMemo(() => getThemedStyles(theme, layout), [theme, layout]);
36+
const mapRef = useRef<GoogleMapsViewRef | null>(null);
37+
38+
return (
39+
<ScrollView
40+
style={styles.scrollView}
41+
contentContainerStyle={styles.content}
42+
>
43+
{LOCATIONS.map((center, i) => {
44+
const isLite = i % 2 !== 0;
45+
return (
46+
<View key={i} style={styles.card}>
47+
<MapWrapper
48+
mapRef={mapRef}
49+
style={styles.map}
50+
initialProps={{
51+
camera: { center, zoom: 12 },
52+
liteMode: isLite,
53+
}}
54+
uiSettings={
55+
isLite && Platform.OS === 'ios'
56+
? {
57+
allGesturesEnabled: false,
58+
compassEnabled: false,
59+
rotateEnabled: false,
60+
scrollEnabled: false,
61+
scrollDuringRotateOrZoomEnabled: false,
62+
tiltEnabled: false,
63+
zoomGesturesEnabled: false,
64+
}
65+
: undefined
66+
}
67+
mapPadding={{ top: 0, bottom: 0, right: 0, left: 0 }}
68+
/>
69+
</View>
70+
);
71+
})}
72+
</ScrollView>
73+
);
74+
}
75+
76+
const getThemedStyles = (theme: AppTheme, layout: EdgeInsets) =>
77+
StyleSheet.create({
78+
scrollView: {
79+
flex: 1,
80+
backgroundColor: theme.bgPrimary,
81+
},
82+
content: {
83+
padding: 16,
84+
paddingBottom: layout.bottom + 24,
85+
gap: 20,
86+
},
87+
card: {
88+
borderRadius: 10,
89+
overflow: 'hidden',
90+
backgroundColor: theme.bgHeader,
91+
shadowColor: theme.shadow,
92+
shadowOffset: { width: 0, height: 2 },
93+
shadowOpacity: 0.25,
94+
shadowRadius: 4,
95+
elevation: 2,
96+
},
97+
map: {
98+
height: 250,
99+
},
100+
});

example/src/types/navigation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type RootStackParamList = {
44
Home: undefined;
55
Blank: undefined;
66
BasicMap: undefined;
7+
ScrollView: undefined;
78
Markers: undefined;
89
SvgMarkers: undefined;
910
Polygons: undefined;

0 commit comments

Comments
 (0)