Skip to main content

MultipleDraggables

MultipleDraggables es un componente de alto nivel que construye una grilla de Draggable a partir de un arreglo de datos.

Resuelve el caso típico de Explorax:

  • Tengo un banco de opciones (letras, números, palabras, íconos, etc.).
  • Quiero mostrarlas en filas bien distribuidas.
  • Quiero que todas tengan la misma configuración de drag (reset, desaparecer, animaciones, etc.).
  • Quiero recibir en el render prop el índice global (idx) y el distance calculado por el sistema DnD.

Internamente, simplemente mapea cada item a un <Draggable /> y se encarga de:

  • Partir el arreglo de datos en filas balanceadas.
  • Calcular idx global (0, 1, 2, …) independientemente de la fila.
  • Pasar flags a Draggable (resetAfterDrop, disappearAfterDrop, etc.).
  • Aplicar una animación de escala opcional mientras se hace drag.
  • Renderizar una imagen de arrastre opcional sobre el ítem.

Firma

interface ChildrenProps<T> {
isDragging: boolean;
data: T;
idx: number;
distance: number;
}

interface MultipleDraggablesProps<T> {
data: T[];
children: (info: ChildrenProps<T>) => React.ReactNode;

resetAfterDrop?: boolean;
disableIfAllFilled?: boolean;
disappearAfterDrop?: boolean;
shakeItAnimation?: boolean;
blackHoleAnimation?: boolean;

styleContainer?: StyleProp<ViewStyle>;

workspaceDimensions: {
width: number;
height: number;
};

numberRows?: number; // número de filas (por defecto 2)
gap?: number; // espacio horizontal entre elementos
rowStyle?: StyleProp<ViewStyle>;

disable?: boolean; // deshabilita TODOS los draggables
movedTimeout?: number; // delay base de animación (se pasa a Draggable.timeMove)

// Escala dinámica mientras se arrastra
enableScale?: boolean;
customTargetScale?: number;
customTransformScale?: number;

// Imagen que se muestra mientras se arrastra
dragImage?: ImageProps | { uri: string };
styleDragImageProp?: StyleProp<ImageStyle>;
}

const MultipleDraggables = <T,>(props: MultipleDraggablesProps<T>) => JSX.Element;
export default MultipleDraggables;

Props

Datos y render

PropTipoRequeridoDescripción
dataT[]Arreglo de elementos lógicos que se volverán Draggable. Cada item se pasa luego a children como data.
children(info: ChildrenProps<T>) => ReactNodeRender prop para cada draggable. Recibe { isDragging, data, idx, distance }.

ChildrenProps<T>:

  • data: el item original del arreglo data.
  • isDragging: true mientras ese elemento específico está en drag.
  • idx: índice global (0, 1, 2, …) dentro de todo el conjunto, no solo de la fila.
  • distance: métrica de distancia al droppable más cercano, calculada por el sistema DnD.

Configuración de drag / estado

PropTipoDefaultDescripción
resetAfterDropbooleantrueSe pasa a cada Draggable. Si está en true, el elemento vuelve a su posición original después del drop.
disableIfAllFilledbooleanfalseSe pasa a cada Draggable. Permite deshabilitar drags cuando todos los droppables relevantes están llenos (según la lógica del slice).
disappearAfterDropbooleanfalseSe pasa a cada Draggable. Permite que el elemento desaparezca tras un drop válido (según mode del DNDProvider).
shakeItAnimationbooleanfalseActiva animaciones de “shake” en Draggable (feedback de error o rechazo).
blackHoleAnimationbooleanfalseActiva animaciones estilo “agujero negro” en Draggable cuando desaparece.
disablebooleanfalseSi true, se pasa disable a todos los Draggable (bloquea el banco de opciones completo).
movedTimeoutnumber0Se pasa como timeMove a cada Draggable; controla el tiempo base de animación de movimiento/rebote.

Layout y estilos

PropTipoDefaultDescripción
workspaceDimensions{ width: number; height: number }Dimensiones del espacio lógico de trabajo. height se usa para calcular gaps y el comportamiento de escala.
numberRowsnumber2Número máximo de filas en la grilla. Si hay menos datos que filas, se usan menos filas.
gapnumberheight * 0.02Espacio horizontal entre elementos. Si no se pasa, se calcula en función de la altura del workspace.
styleContainerStyleProp<ViewStyle>Estilo adicional para el contenedor de la grilla (envoltura vertical).
rowStyleStyleProp<ViewStyle>Estilo adicional para cada fila (envoltura horizontal).

Internamente:

  • El arreglo data se divide en rowsCount filas con distribución lo más uniforme posible:

    • Se calcula basePerRow y un número de filas que llevan un extra (extra) para repartir los sobrantes.
  • getStyles(gap, height) genera:

    • container: columna, centrada, con gap vertical ≈ height * 0.018 y marginVerticalheight * 0.1.
    • row: fila, centrada, con gap horizontal = gap (o height * 0.02 si no se pasó).

Escala dinámica y drag image

PropTipoDefaultDescripción
enableScalebooleanundefined (off)Si true, se activa una animación de escala basada en distance.
customTargetScalenumber0.65Escala mínima a la que puede llegar el elemento mientras se arrastra.
customTransformScalenumberSi se define, la escala se separa en scaleX y scaleY (solo se estira/achica en alto según este factor).
dragImageImageProps | { uri: string }Imagen a mostrar solo cuando el ítem está en drag (isDragging). Puede ser require() o { uri }.
styleDragImagePropStyleProp<ImageStyle>Estilo a aplicar a la imagen de drag.

Lógica de escala (resumen):

  • Se crea un scale = useSharedValue(1) por cada ítem.

  • En un useEffect dependiente de distance + isDragging:

    • Si isDragging y la distancia está en un rango entre ≈ 3% y 25% de la altura del workspace:

      • Se calcula una escala lineal entre originalScale = 1 y targetscale = customTargetScale ?? 0.65.
      • Entre más grande distance, más se acerca la escala a targetscale.
    • Si no se está arrastrando y la distancia es mayor a ≈ 10% de la altura:

      • Se resetea la escala a 1.
  • animatedStyleScale elige entre:

    • Escala uniforme: transform: [{ scale: scale.value }].

    • Escala diferenciada (si customTransformScale y isDragging):

      • scaleX = scale.value
      • scaleY = scale.value * customTransformScale

La imagen de drag se superpone así:

{isDragging && dragImage && (
<Image source={dragImage} style={styleDragImageProp} />
)}

Ejemplo básico: banco de números

import MultipleDraggables from "@components/DnDRevolution/MultipleDraggables";
import { Droppable, DNDProvider } from "@components/DnDRevolution";
import { View, Text } from "react-native";

type Option = { id: number; label: string };

const options: Option[] = Array.from({ length: 10 }, (_, i) => ({
id: i,
label: `${i}`,
}));

export const NumbersBank = () => {
const workspaceDimensions = { width: 360, height: 640 };

return (
<DNDProvider<Option>
workSpaceDimensions={workspaceDimensions}
mode="remplacement-multi"
>
<Droppable<Option> id="target">
{({ isOver, data }) => (
<View
style={{
height: 80,
borderWidth: 2,
borderRadius: 12,
borderColor: isOver ? "green" : "gray",
alignItems: "center",
justifyContent: "center",
}}
>
<Text>{data?.label ?? "Suelta un número aquí"}</Text>
</View>
)}
</Droppable>

<MultipleDraggables<Option>
data={options}
workspaceDimensions={workspaceDimensions}
numberRows={2}
resetAfterDrop
enableScale
>
{({ isDragging, data, idx, distance }) => (
<View
style={{
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
marginVertical: 4,
backgroundColor: isDragging ? "#4f46e5" : "#6366f1",
}}
>
<Text style={{ color: "white" }}>
{data.label} (#{idx})
</Text>
</View>
)}
</MultipleDraggables>
</DNDProvider>
);
};

Ejemplo con imagen de drag y tres filas

<MultipleDraggables<Option>
data={options}
workspaceDimensions={workspaceDimensions}
numberRows={3}
gap={12}
resetAfterDrop={false}
disappearAfterDrop
dragImage={require("@assets/drag-ghost.png")}
styleDragImageProp={{ width: 40, height: 40, position: "absolute" }}
enableScale
customTargetScale={0.7}
customTransformScale={1.2}
>
{({ isDragging, data }) => (
<View
style={{
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 10,
borderWidth: 1,
borderColor: isDragging ? "#22c55e" : "#e5e7eb",
backgroundColor: "white",
}}
>
<Text>{data.label}</Text>
</View>
)}
</MultipleDraggables>

Cómo se integra con el resto del sistema

  • Cada item de MultipleDraggables es un <Draggable /> configurado con los mismos flags.

  • La lógica de colisión, selección de droppable, historial, etc. sigue siendo responsabilidad del slice del DNDProvider y de Droppable.

  • MultipleDraggables solo resuelve:

    • Layout en filas.
    • Reutilización de props de Draggable.
    • Animación de escala y drag image para un conjunto de opciones.

Siguiente sección recomendada: 👉 PadDND para ver un pad de números/letras construido encima de MultipleDraggables.