Hoy vamos a aprender cómo podemos detectar colisiones entre dos elementos usando React Native.
Detectar colisiones es muy útil por si queremos realizar pequeños juegos con React Native incluso con Expo.
Lo primero que haremos es tomar de referencia nuestro elemento del tutorial de Permitir arrastrar elementos por la pantalla en React Native
DragComponent.tsx
import React, { useRef, ReactNode, useState } from 'react'; import { Animated, PanResponder, StyleProp, ViewStyle } from 'react-native'; interface DragableProps { children: ReactNode; style?: StyleProp<ViewStyle>; } const DragComponent: React.FC<DraggableProps> = ({ children, style }) => { const [offset, setOffset] = useState({ x: 0, y: 0 }); const pan = useRef(new Animated.ValueXY()).current; const panResponder = PanResponder.create({ onStartShouldSetPanResponder: () => true, onPanResponderMove: (_, gesture) => { pan.setValue({ x: gesture.dx + offset.x, y: gesture.dy + offset.y }); }, onPanResponderRelease: (_, gesture) => { setOffset({ x: gesture.dx + offset.x, y: gesture.dy + offset.y }); }, }); return ( <Animated.View {...panResponder.panHandlers} style={[ { transform: [{ translateX: pan.x }, { translateY: pan.y }], }, style, ]} > {children} </Animated.View> ); }; export default DragComponent;
Y ahora añadiremos el código que nos permitirá detectar colisiones:
- Creamos un objeto llamado ElementDrag
import { LayoutRectangle } from "react-native"; export interface ElementDrag { id: string; layout: LayoutRectangle; }
Ahora en la pantalla principal (dónde estén los elementos en los que queremos detectar colisiones) Pantalla.tsx
const [otherLayouts, setOtherLayouts] = useState<ElementDrag[]>([]); // Referencias a los otros elementos const ref1 = useRef<View>(null); const ref2 = useRef<View>(null); useEffect(() => { ref1.current?.measureInWindow((x, y, width, height) => { setOtherLayouts((layouts) => [...layouts, { id: 'elemento1', layout: { x, y, width, height } }]); }); ref2.current?.measureInWindow((x, y, width, height) => { setOtherLayouts((layouts) => [...layouts, { id: 'elemento2', layout: { x, y, width, height } }]); }); }, []);
Hemos añadido un array dónde almacenamos los elementos que queremos detectar como colisión y dos referencias para asignarles el layout (posiciones) y un id.
Ahora debemos asignar la referencia al elemento o elementos:
<View ref={ref1} style={{ width: 80, height: 120, backgroundColor: 'red' }} > </View> ... <View ref={ref2} style={{ width: 80, height: 120, backgroundColor: 'green' }} > </View>
Y ahora vamos a añadir la lógica de colisión en nuestro elemento que se arrastra:
- Vamos a DragComponent.tsx y añadimos:
import React, { useRef, ReactNode, useState } from 'react'; import { Animated, PanResponder, StyleProp, ViewStyle } from 'react-native'; interface DragableProps { children: ReactNode; style?: StyleProp<ViewStyle>; onCollision?: (a: ElementDrag, b: React.Dispatch<React.SetStateAction<{ x: number; y: number; }>>) => void; otherLayouts?: ElementDrag[]; } const DragComponent: React.FC<DraggableProps> = ({ children, style }) => { const [offset, setOffset] = useState({ x: 0, y: 0 }); const pan = useRef(new Animated.ValueXY()).current; function detectarColision(gestureState: PanResponderGestureState) { if (layout.current && otherLayouts) { const x = gestureState.moveX - layout.current.width / 2; const y = gestureState.moveY - layout.current.height / 2; const currentLayout = { ...layout.current, x, y }; otherLayouts.forEach((elemento: ElementDrag) => { const collision = checkCollision(currentLayout, elemento.layout); if (collision && onCollision) { onCollision(elemento, setNuevoPan); } }); } } const panResponder = PanResponder.create({ onStartShouldSetPanResponder: () => true, onPanResponderMove: (_, gesture) => { pan.setValue({ x: gesture.dx + offset.x, y: gesture.dy + offset.y }); detectarColision(gesture); }, onPanResponderRelease: (_, gesture) => { setOffset({ x: gesture.dx + offset.x, y: gesture.dy + offset.y }); }, }); return ( <Animated.View {...panResponder.panHandlers} style={[ { transform: [{ translateX: pan.x }, { translateY: pan.y }], }, style, ]} > {children} </Animated.View> ); }; export default DragComponent;
Ahora voy a explicar lo que hace:
Inicialización del estado y referencias:
- Se inicializa un estado llamado
offset
, que mantiene el desplazamiento del componente arrastrable. - Se crea una referencia
pan
utilizandouseRef
que contiene una instancia deAnimated.ValueXY()
para rastrear las coordenadas del gesto de arrastre.
Función de detección de colisión (detectarColision
):
- Esta función se llama cada vez que hay un movimiento de gesto (
onPanResponderMove
). - Primero, verifica si existen referencias al diseño (
layout.current
) y a los diseños de otros elementos (otherLayouts
). - Calcula las coordenadas actuales del componente arrastrable (
currentLayout
) basándose en las coordenadas del gesto y el tamaño del diseño del componente. - Itera sobre cada otro elemento (
elemento
) proporcionado enotherLayouts
. - Llama a una función
checkCollision(currentLayout, elemento.layout)
para verificar si hay una colisión entre el componente arrastrable y el otro elemento. - Si hay una colisión y se proporciona una función
onCollision
, la llama pasando el elemento con el que se ha producido la colisión (elemento
) y una funciónsetNuevoPan
, que probablemente actualiza el estado del desplazamiento del componente arrastrable.
PanResponder:
- Se utiliza
PanResponder
para manejar los gestos de arrastre. onPanResponderMove
actualiza continuamente la posición del componente arrastrable según el gesto de arrastre del usuario y luego llama adetectarColision
para verificar si hay alguna colisión.onPanResponderRelease
actualiza el estado de desplazamiento (offset
) cuando el gesto de arrastre se suelta.
Renderización:
- Renderiza un componente
Animated.View
que utiliza el estado depan
para aplicar transformaciones de traducción, lo que mueve visualmente el componente arrastrable. - Los controladores de gestos (
panHandlers
) se aplican al componenteAnimated.View
para permitir el arrastre interactivo.
El código detecta colisiones verificando continuamente si el componente arrastrable se superpone con otros elementos en la pantalla durante el gesto de arrastre. Si se detecta una colisión, se llama a una función proporcionada para manejarla.
Y ahora vamos a añadir al elemento principal la función para detectar colisiones:
<DragComponent onCollision={(elemento: ElementDrag, setNuevoPan: React.Dispatch<React.SetStateAction<{ x: number; y: number; }>>) => { console.log('¡Colisión detectada con el elemento ' + elemento.id + ' !'); }} otherLayouts={otherLayouts} > <View ref={ref2} style={{ width: 80, height: 120, backgroundColor: 'blue' }} > </View> </DragComponent>
Cuando detecte la colisión aparecerá por consola: