|
1 | 1 | ---
|
2 | 2 | id: gesture-composition
|
3 |
| -title: Composing gestures |
4 |
| -sidebar_label: Composing gestures |
| 3 | +title: Gesture composition & interactions |
| 4 | +sidebar_label: Gesture composition & interactions |
5 | 5 | sidebar_position: 4
|
6 | 6 | ---
|
7 | 7 |
|
@@ -200,6 +200,209 @@ function App() {
|
200 | 200 | }
|
201 | 201 | ```
|
202 | 202 |
|
203 |
| -## Composition vs `simultaneousWithExternalGesture` and `requireExternalGestureToFail` |
| 203 | +# Cross-component interactions |
204 | 204 |
|
205 |
| -You may have noticed that gesture composition described above requires you to mount all of the composed gestures under a single `GestureDetector`, effectively attaching them to the same underlying component. If you want to make a relation between gestures that are attached to separate `GestureDetectors`, we have a separate mechanism for that: `simultaneousWithExternalGesture` and `requireExternalGestureToFail` methods that are available on every base gesture. They work exactly the same way as `simultaneousHandlers` and `waitFor` on gesture handlers, that is they just mark the relation between the gestures without joining them into single object. |
| 205 | +You may have noticed that gesture composition described above requires you to mount all of the composed gestures under a single `GestureDetector`, effectively attaching them to the same underlying component. You can customize how gestures interact with each other across multiple components in a couple of ways: |
| 206 | + |
| 207 | +## requireExternalGestureToFail |
| 208 | + |
| 209 | +`requireExternalGestureToFail` allows to delay activation of the handler until all handlers passed as arguments to this method fail (or don't begin at all). |
| 210 | + |
| 211 | +For example, you may want to have two nested components, both of them can be tapped by the user to trigger different actions: outer view requires one tap, but the inner one requires 2 taps. If you don't want the first tap on the inner view to activate the outer handler, you must make the outer gesture wait until the inner one fails: |
| 212 | + |
| 213 | +```jsx |
| 214 | +import React from 'react'; |
| 215 | +import { View, StyleSheet } from 'react-native'; |
| 216 | +import { |
| 217 | + GestureDetector, |
| 218 | + Gesture, |
| 219 | + GestureHandlerRootView, |
| 220 | +} from 'react-native-gesture-handler'; |
| 221 | + |
| 222 | +export default function Example() { |
| 223 | + const innerTap = Gesture.Tap() |
| 224 | + .numberOfTaps(2) |
| 225 | + .onStart(() => { |
| 226 | + console.log('inner tap'); |
| 227 | + }); |
| 228 | + |
| 229 | + const outerTap = Gesture.Tap() |
| 230 | + .onStart(() => { |
| 231 | + console.log('outer tap'); |
| 232 | + }) |
| 233 | + .requireExternalGestureToFail(innerTap); |
| 234 | + |
| 235 | + return ( |
| 236 | + <GestureHandlerRootView style={styles.container}> |
| 237 | + <GestureDetector gesture={outerTap}> |
| 238 | + <View style={styles.outer}> |
| 239 | + <GestureDetector gesture={innerTap}> |
| 240 | + <View style={styles.inner} /> |
| 241 | + </GestureDetector> |
| 242 | + </View> |
| 243 | + </GestureDetector> |
| 244 | + </GestureHandlerRootView> |
| 245 | + ); |
| 246 | +} |
| 247 | + |
| 248 | +const styles = StyleSheet.create({ |
| 249 | + container: { |
| 250 | + flex: 1, |
| 251 | + alignItems: 'center', |
| 252 | + justifyContent: 'center', |
| 253 | + }, |
| 254 | + outer: { |
| 255 | + width: 250, |
| 256 | + height: 250, |
| 257 | + backgroundColor: 'lightblue', |
| 258 | + }, |
| 259 | + inner: { |
| 260 | + width: 100, |
| 261 | + height: 100, |
| 262 | + backgroundColor: 'blue', |
| 263 | + alignSelf: 'center', |
| 264 | + }, |
| 265 | +}); |
| 266 | +``` |
| 267 | + |
| 268 | +## blocksExternalGesture |
| 269 | + |
| 270 | +`blocksExternalGesture` works similarily to `requireExternalGestureToFail` but the direction of the relation is reversed - instead of being one-to-many relation, it's many-to-one. It's especially useful for making lists where the `ScrollView` component needs to wait for every gesture underneath it. All that's required to do is to pass a ref, for example: |
| 271 | + |
| 272 | +```jsx |
| 273 | +import React, { useRef } from 'react'; |
| 274 | +import { StyleSheet } from 'react-native'; |
| 275 | +import { |
| 276 | + GestureDetector, |
| 277 | + Gesture, |
| 278 | + GestureHandlerRootView, |
| 279 | + ScrollView, |
| 280 | +} from 'react-native-gesture-handler'; |
| 281 | +import Animated, { |
| 282 | + useSharedValue, |
| 283 | + useAnimatedStyle, |
| 284 | + withTiming, |
| 285 | +} from 'react-native-reanimated'; |
| 286 | + |
| 287 | +const ITEMS = ['red', 'green', 'blue', 'yellow']; |
| 288 | + |
| 289 | +function Item({ backgroundColor, scrollRef }) { |
| 290 | + const scale = useSharedValue(1); |
| 291 | + const zIndex = useSharedValue(1); |
| 292 | + |
| 293 | + const pinch = Gesture.Pinch() |
| 294 | + .blocksExternalGesture(scrollRef) |
| 295 | + .onBegin(() => { |
| 296 | + zIndex.value = 100; |
| 297 | + }) |
| 298 | + .onChange((e) => { |
| 299 | + scale.value *= e.scaleChange; |
| 300 | + }) |
| 301 | + .onFinalize(() => { |
| 302 | + scale.value = withTiming(1, undefined, (finished) => { |
| 303 | + if (finished) { |
| 304 | + zIndex.value = 1; |
| 305 | + } |
| 306 | + }); |
| 307 | + }); |
| 308 | + |
| 309 | + const animatedStyles = useAnimatedStyle(() => ({ |
| 310 | + transform: [{ scale: scale.value }], |
| 311 | + zIndex: zIndex.value, |
| 312 | + })); |
| 313 | + |
| 314 | + return ( |
| 315 | + <GestureDetector gesture={pinch}> |
| 316 | + <Animated.View |
| 317 | + style={[ |
| 318 | + { backgroundColor: backgroundColor }, |
| 319 | + styles.item, |
| 320 | + animatedStyles, |
| 321 | + ]} |
| 322 | + /> |
| 323 | + </GestureDetector> |
| 324 | + ); |
| 325 | +} |
| 326 | + |
| 327 | +export default function Example() { |
| 328 | + const scrollRef = useRef(); |
| 329 | + |
| 330 | + return ( |
| 331 | + <GestureHandlerRootView style={styles.container}> |
| 332 | + <ScrollView style={styles.container} ref={scrollRef}> |
| 333 | + {ITEMS.map((item) => ( |
| 334 | + <Item backgroundColor={item} key={item} scrollRef={scrollRef} /> |
| 335 | + ))} |
| 336 | + </ScrollView> |
| 337 | + </GestureHandlerRootView> |
| 338 | + ); |
| 339 | +} |
| 340 | + |
| 341 | +const styles = StyleSheet.create({ |
| 342 | + container: { |
| 343 | + flex: 1, |
| 344 | + }, |
| 345 | + item: { |
| 346 | + flex: 1, |
| 347 | + aspectRatio: 1, |
| 348 | + }, |
| 349 | +}); |
| 350 | +``` |
| 351 | + |
| 352 | +## simultaneousWithExternalGesture |
| 353 | + |
| 354 | +`simultaneousWithExternalGesture` allows gestures across different components to be recognized simultaneously. For example, you may want to have two nested views, both with tap gesture attached. Both of them require one tap, but tapping the inner one should also activate the gesture attached to the outer view: |
| 355 | + |
| 356 | +```jsx |
| 357 | +import React from 'react'; |
| 358 | +import { View, StyleSheet } from 'react-native'; |
| 359 | +import { |
| 360 | + GestureDetector, |
| 361 | + Gesture, |
| 362 | + GestureHandlerRootView, |
| 363 | +} from 'react-native-gesture-handler'; |
| 364 | + |
| 365 | +export default function Example() { |
| 366 | + const innerTap = Gesture.Tap() |
| 367 | + .onStart(() => { |
| 368 | + console.log('inner tap'); |
| 369 | + }); |
| 370 | + |
| 371 | + const outerTap = Gesture.Tap() |
| 372 | + .onStart(() => { |
| 373 | + console.log('outer tap'); |
| 374 | + }) |
| 375 | + .simultaneousWithExternalGesture(innerTap); |
| 376 | + |
| 377 | + return ( |
| 378 | + <GestureHandlerRootView style={styles.container}> |
| 379 | + <GestureDetector gesture={outerTap}> |
| 380 | + <View style={styles.outer}> |
| 381 | + <GestureDetector gesture={innerTap}> |
| 382 | + <View style={styles.inner} /> |
| 383 | + </GestureDetector> |
| 384 | + </View> |
| 385 | + </GestureDetector> |
| 386 | + </GestureHandlerRootView> |
| 387 | + ); |
| 388 | +} |
| 389 | + |
| 390 | +const styles = StyleSheet.create({ |
| 391 | + container: { |
| 392 | + flex: 1, |
| 393 | + alignItems: 'center', |
| 394 | + justifyContent: 'center', |
| 395 | + }, |
| 396 | + outer: { |
| 397 | + width: 250, |
| 398 | + height: 250, |
| 399 | + backgroundColor: 'lightblue', |
| 400 | + }, |
| 401 | + inner: { |
| 402 | + width: 100, |
| 403 | + height: 100, |
| 404 | + backgroundColor: 'blue', |
| 405 | + alignSelf: 'center', |
| 406 | + }, |
| 407 | +}); |
| 408 | +``` |
0 commit comments