mgcrea / react-native-dnd

Modern and easy-to-use drag&drop library for react-native.
https://mgcrea.github.io/react-native-dnd/
MIT License
135 stars 12 forks source link

Example UseDraggable implementation does not work #6

Closed GNUGradyn closed 10 months ago

GNUGradyn commented 11 months ago

Hello. I implemented UseDraggable as documented. The events are firing but the element is not following the cursor. I have made an MRE here.

https://github.com/GNUGradyn/RnDndMre

Using the provided Draggable component works, but not the custom one in this repo based on the sample on the docs

BrianJVarley commented 10 months ago

@GNUGradyn I've also added this package to a react-native@0.72.3 project. I can see the onBegin - onDragEnd events firing in my console but no movement of the DraggableComponent.

Like you've explained the DraggableComponent doesn't actually move or translate onto the Droppable component. I'm wondering if the latest version is not yet compatible with react-native@0.72.3? @mgcrea

mgcrea commented 10 months ago

@GNUGradyn in your example your custom component is lacking the required animatedStyle to actually move it along the drag gesture, you need something like this (tldr. useAnimatedStyle with transform translateX & translateY):

import { useDraggable } from "@mgcrea/react-native-dnd";
import { Text, View } from "react-native";
import Animated, {
  useAnimatedStyle,
  withSpring,
} from "react-native-reanimated";

const DraggableComponent = ({ id, data, disabled }) => {
  const { offset, setNodeRef, activeId, setNodeLayout, draggableState } = useDraggable({
    id,
    data,
    disabled,
  });

  const animatedStyle = useAnimatedStyle(() => {
    const isActive = activeId.value === id;
    return {
      opacity: isActive ? 0.9 : 1,
      zIndex: isActive ? 999 : 1,
      transform: [
        {
          translateX: isActive ? offset.x.value : withSpring(offset.x.value),
        },
        {
          translateY: isActive ? offset.y.value : withSpring(offset.y.value),
        },
      ],
    };
  }, [id]);

  return (
    <Animated.View ref={setNodeRef} onLayout={setNodeLayout} style={animatedStyle}>
      <View style={{width: 50, height: 50, backgroundColor:"green"}}>
      <Text>DRAG</Text>
      </View>
    </Animated.View>
  );
};

export default DraggableComponent;
BrianJVarley commented 10 months ago

@mgcrea I've setup a sample component similar to the snippet you've described above. But there are no drag callbacks triggered when I try to drag the DraggableCustomComponent and the block itself doesn't move position on screen.

  "dependencies": {
    "@mgcrea/react-native-dnd": "^1.4.0",
    "react": "18.2.0",
    "react-native": "^0.72.3",
    "react-native-gesture-handler": "^2.12.0",
    "react-native-haptic-feedback": "2.0.3",
    "react-native-reanimated": "^3.3.0",
  },

Any ideas what could be gone wrong in my component's below? (Although it does seem to be the exact same component code) Or maybe point me in the direction on how I can debug this further?

import {
  DndProvider,
  DndProviderProps,
  Draggable,
  DraggableProps,
  Droppable,
  DroppableProps,
} from '@mgcrea/react-native-dnd';
import React, {useState, type FunctionComponent} from 'react';
import {SafeAreaView, StyleSheet, Text, View, Dimensions} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import {runOnJS, useSharedValue} from 'react-native-reanimated';
import DraggableCustomComponent from './drag-n-drop/DraggableComponent';

const ScreenWidth = Dimensions.get('window').width;
const ScreenHeight = Dimensions.get('window').height;

export const DuctPlannerComponent: FunctionComponent = () => {
  const [count, setCount] = useState(0);
  const dynamicData = useSharedValue({count: 0});

  const onDragEnd = () => {
    setCount((count) => count + 1);
  };

  const handleDragEnd: DndProviderProps['onDragEnd'] = ({active, over}) => {
    'worklet';
    if (over) {
      console.log(`Current count is ${active.data.value.count}`);
      dynamicData.value = {
        ...dynamicData.value,
        count: dynamicData.value.count + 1,
      };
      runOnJS(onDragEnd)();
    }
  };

  const handleBegin: DndProviderProps['onBegin'] = () => {
    'worklet';
    console.log('onBegin');
  };

  const handleFinalize: DndProviderProps['onFinalize'] = () => {
    'worklet';
    console.log('onFinalize');
  };

  const draggableStyle: DraggableProps['animatedStyleWorklet'] = (
    style,
    {isActive, isActing},
  ) => {
    'worklet';
    return {
      opacity: isActing ? 0.5 : 1,
      backgroundColor: isActive ? 'lightseagreen' : 'seagreen',
    };
  };

  const droppableStyle: DroppableProps['animatedStyleWorklet'] = (
    style,
    {isActive},
  ) => {
    'worklet';
    return {
      backgroundColor: isActive ? 'lightsteelblue' : 'steelblue',
    };
  };

  return (
    <SafeAreaView style={styles.container}>
      <GestureHandlerRootView>
        <DndProvider
          onBegin={handleBegin}
          onFinalize={handleFinalize}
          onDragEnd={handleDragEnd}>
          <View style={styles.container}>
            <Droppable
              id="drop"
              style={styles.box}
              animatedStyleWorklet={droppableStyle}
              activeOpacity={1}>
              <Text style={styles.text}>DROP</Text>
            </Droppable>
            <DraggableCustomComponent
              id="drag"
              data={dynamicData}
              style={styles.box}
              animatedStyleWorklet={draggableStyle}
            />
            {/* The Draggable also did not respond to drag gestures */}
            {/* <Draggable
              id="drag"
              data={dynamicData}
              style={styles.box}
              animatedStyleWorklet={draggableStyle}>
              <Text style={styles.text}>DRAG</Text>
            </Draggable> */}
            <Text testID="button">count is {count}</Text>
          </View>
        </DndProvider>
      </GestureHandlerRootView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    width: ScreenWidth,
    height: ScreenHeight,
    alignItems: 'center',
    justifyContent: 'center',
  },
  box: {
    margin: 24,
    padding: 24,
    height: 96,
    width: 96,
    borderRadius: 8,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'darkseagreen',
  },
  text: {
    color: 'white',
  },
});
import {useDraggable} from '@mgcrea/react-native-dnd';
import {Text, View, StyleSheet} from 'react-native';
import Animated, {useAnimatedStyle, withSpring} from 'react-native-reanimated';

const DraggableCustomComponent = ({id, data, disabled}) => {
  const {offset, setNodeRef, activeId, setNodeLayout, draggableState} =
    useDraggable({
      id,
      data,
      disabled,
    });

  const animatedStyle = useAnimatedStyle(() => {
    const isActive = activeId.value === id;
    return {
      opacity: isActive ? 0.9 : 1,
      zIndex: isActive ? 999 : 1,
      transform: [
        {
          translateX: isActive ? offset.x.value : withSpring(offset.x.value),
        },
        {
          translateY: isActive ? offset.y.value : withSpring(offset.y.value),
        },
      ],
    };
  }, [id]);

  return (
    <Animated.View
      ref={setNodeRef}
      onLayout={setNodeLayout}
      style={animatedStyle}>
      <View style={styles.box}>
        <Text>DRAG 2</Text>
      </View>
    </Animated.View>
  );
};

const styles = StyleSheet.create({
  box: {
    margin: 24,
    padding: 24,
    height: 128,
    width: 128,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'darkseagreen',
  },
});

export default DraggableCustomComponent;
mgcrea commented 10 months ago

I did encounter issues when building/bundling this library for past releases and did not immediately notice it on my local test setup, for more info see https://github.com/software-mansion/react-native-reanimated/discussions/4979

For now you must use the exact same reanimated version that the one from the lib (currently the latest) or you will encounter version mismatch issues, can you retry your code with @mgcrea/react-native-dnd@1.5.5 and react-native-reanimated@3.4.2? It should work. Let me know if it does not and I'll investigate further.

mgcrea commented 10 months ago

I've just published a working demo project on GitHub if you want to check and plan to add more examples to it in the future to help test the lib: https://github.com/mgcrea/react-native-dnd-demo

BrianJVarley commented 10 months ago

@mgcrea, yes that's great this works now with @mgcrea/react-native-dnd@1.5.6 based on your example 👏

I've been playing around with the example and assumed a Draggable component would automatically "drop into" a Droppable component. But the Draggable component just snaps back to its starting position in the view. Looking at the docs I didn't see anything calling out how you enable the component to be dropped- https://mgcrea.github.io/react-native-dnd/components/draggable/

Do you plan to add some example of this or could guide me on the implementation?

BrianJVarley commented 10 months ago

@mgcrea This is what happens with your latest demo code, the Draggable component snaps back to its starting coordinates (0:0) on the View. I've also played around with a custom Draggable component, which uses a useAnimatedStyle - https://github.com/mgcrea/react-native-dnd/issues/6#issuecomment-1688940396. I can see that the updater function is transforming the component along the translateX & translateY coordinates. But on dragEnd (draggableState.value = 5) its resetting to starting coordinates - offset.x.value = 0, offfset.y.value = 0

rn-dnd-end-drag-behaviour

For example in the logs below you can see coordinates when I drag over the drop component, then let go of the DraggableComponennt, which resets to x & y = 0. Instead of dropping at the last coordinate before I let go of the item (offset.x.value = 0, offset.y.value = -0.013319007820699134)

In the meantime I'll study the reanimated docs to better understand useAnimatedStyle. But any help on this would be greatly appreciated.

 LOG  draggableState.value = 5
 LOG  isActive = false
 LOG  activeId.value = null
 LOG  id = bend-1-key
 LOG  offset.x.value = 0
 LOG  offset.y.value = -0.013319007820699134
 LOG  data.over = {"over":{"id":"drop","data":{"value":{},"_value":{},"_animation":null,"_isReanimatedSharedValue":true},"disabled":false}}
 LOG  draggableState.value = 5
 LOG  isActive = false
 LOG  activeId.value = null
 LOG  id = bend-1-key
 LOG  offset.x.value = 0
 LOG  offset.y.value = 0
 LOG  data.over = {"over":{"id":"drop","data":{"value":{},"_value":{},"_animation":null,"_isReanimatedSharedValue":true},"disabled":false}}
BrianJVarley commented 10 months ago

I see my mistake after reading https://docs.swmansion.com/react-native-reanimated/docs/core/useAnimatedStyle#returns, just need to return an empty object when draggableState = END

Although once I drop an item, I can no longer move it again. So it seems there is still a flaw in the logic of the animated style below?

  const animatedStyle = useAnimatedStyle(() => {
    const isActive = activeId.value === id;

    if (isActive && draggableState.value !== 5) {
      return {
        opacity: isActive ? 0.9 : 1,
        zIndex: isActive ? 999 : 1,
        transform: [
          {
            translateX: isActive ? offset.x.value : withSpring(offset.x.value),
          },
          {
            translateY: isActive ? offset.y.value : withSpring(offset.y.value),
          },
        ],
      };
    } else {
      return {};
    }
  }, [id, offset, activeId, draggableState]);
mgcrea commented 10 months ago

I've fixed reanimated bundling issues with the 1.6.0 release and everything should correctly works from now on, whatever the final app setup/dependencies is/are.

Feel free to open new issues if you encounter other issues.

@BrianJVarley your problem seems unrelated to this ticket so please open a separate one if you stil encounter issues.