Detectar colisiones entre dos elementos en React Native

Tiempo de lectura: 4 minutos

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 utilizando useRef que contiene una instancia de Animated.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 en otherLayouts.
  • 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ón setNuevoPan, 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 a detectarColision 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 de pan para aplicar transformaciones de traducción, lo que mueve visualmente el componente arrastrable.
  • Los controladores de gestos (panHandlers) se aplican al componente Animated.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:

Deja un comentario