software-mansion / react-native-gesture-handler

Declarative API exposing platform native touch and gesture system to React Native.
https://docs.swmansion.com/react-native-gesture-handler/
MIT License
5.85k stars 954 forks source link

Drag Gesture Handler #963

Open ShaMan123 opened 4 years ago

ShaMan123 commented 4 years ago

EDITED 22/05/21

Motivation #928

  1. Drag and drop is a commonly used gesture in native platforms and is a top choice for app UI/UX.
  2. There are many unique attributes and behaviors of the drag and drop gesture that are different than PanGestureHandler -> using PanGestureHandler requires a lot of boilerplate to get it to function as a synthetic drag and drop handler (I've tried it rn-drag-drop).
  3. Without reanimated it is not responsive. With reanimated it causes a lot of overhead (lots of nodes created for each synthetic handler), which makes it not good enough of a solution (e.g. Using this as items of a FlatList is a KILLER).
  4. Drag events between apps are now supported by both platforms. This can be handled only with native drag events. There is no way of achieving this currently in react-native. In this PR I've achieved it on android already.
  5. The way I see it, react-native-gesture-handler aims to be a plug and play library, this is why I think drag and drop gesture handling is essential.

Changes

I've added 2 new handlers: DragGestureHandler and DropGestureHandler both of them extend PanGestureHandler. I have made adaptions mainly to GestureHandlerOrchestrator in the android native code and added examples under Drag & Drop and Drag & Drop in FlatList. In addition I've revived the native android example, this was done by adjusting the gesture handler registry and mocking a root view.

The only thing out of scope is this fix, which fixes passing down the wrong handlerTag to native making relations config wrong for NativeViewGestureHandler. https://github.com/software-mansion/react-native-gesture-handler/pull/963/files#diff-6eb54a5e8a566ff2be948cdfe0abb5f4

Logic

Gesture handling is in progress. PanGestureHandler super class of DragGestureHandler receives events. It is in charge of moving from State.UNDETERMINED to State.BEGAN and State.ACTIVE based on it's own logic. Once active, DragGestureHandler begins dragging. From this point on the GestureHandlerOrchestrator receives a DragEvent which is first adapted to a MotionEvent and delivered to all handlers. This is done to activate DropGestureHandlers (DropGestureHandler is the same as DragGestureHandler in terms of PanGestureHandler responsibilities), run simultaneous handlers and cancel the rest. Then the DragEvent is delivered to all handlers, DropGestureHandlers first, then the rest. Extracting DropGestureHandlers is done for each event to obtain the top most handler. If DropGestureHandlers are extracted the top most will be activated notifying the other drag/drop handlers with an appropriate action.

Multi Window

Android supports multi window. The AndroidManifest.xml file needs to be edited to get this working. To persist the same behavior I had to add some logic to bridge the framework not dispatching drag events back to the app that started the drag gesture, leaving DragGestureHandler unaware of interactions with DropGestureHandlers. So, I've added a small broadcasting mechanism that broadcasts changes of the drag action to the source app. To test this I suggest running both AndroidNativeExample and Example side by side.

image

Simultaneous Handlers

There is a caveat regarding simultaneous handlers. A drag event (on android) has only one pointer so simultaneous handling with RotationGestureHandler or PinchGestureHandler won't work unfortunately. Passing DragGestureHandlers before the event begins will join them to the drag event and add them to the drag shadow. See example.

Props

  1. types = number | number[] | null, if a DragGestureHandler and a DropGestureHandler share one type they can interact.

    DragGestureHandler props

  2. data - pass object to DropGestureHandler on drop via gesture event.
  3. Drag Shadow There are several options here, all listed in react-native-gesture-handler.d.ts
    1. dragMode - handles the visibilty of DragGestureHandler while dragging
      1. move
      2. move-restore - after drop occurs, restores visibility to the DragGestureHandler's view
      3. copy
      4. none
    2. shadow = component | element | tag | null to render view while dragging
    3. shadowEnabled merged as dragMode='none'
    4. shadowConfig - control position, opacity, margin, etc. Drag shadow can update during a drag only for nougat and higher.

Events

Extending PanGestureHandler event.

  type Map = { [index: string]: any };
  type DragData<T extends Map> = T & { nativeProps?: Map };
    type DropData<T extends Map> = (DragData<T> | {
    /**This property will be available if an error occured while trying to parse the data */
    rawData: string
  }) & { readonly target: number };

  export enum DragState {
    BEGAN,
    ACTIVE,
    DROP,
    END,
    ENTERED,
    EXITED
  }

  export enum DragMode {
    MOVE,
    /** After drop occurs, restores visibility to the DragGestureHandler's view  */
    MOVE_RESTORE,
    COPY
  }

  interface DragGestureHandlerEventExtra extends PanGestureHandlerEventExtra {
    dragTarget: number,
    dragTargets: number[],
    dropTarget: number,
    dragState: DragState
  }

  interface DropGestureHandlerEventExtra<T extends Map> extends DragGestureHandlerEventExtra {
    /**
     * The data received from the DragGestureHandler
     */
    data: DropData<T>[] | null,
    /**
     * The id of the app the event originated from.
     * This property will be available once a drop occurs for an event that originated from a different app.
     */
    sourceAppID?: string
  }

https://github.com/software-mansion/react-native-gesture-handler/pull/963#issuecomment-588223553

I believe iOS will be much easier to implement.

Please consider adding this to the road map. Drag gesture handling is the missing piece of RNGH!

ShaMan123 commented 4 years ago

Hi guys, I had this PR in mind for a few months and now found the time, need and stamina to dive into it. It was hard at first to understand the inner workings of GestureHandlerOrchestrator but I pulled through. The more deeper I went, the more I understood what a fine job you guys do. Both libraries, this and reanimated, make developing in react-native possible, easy and even effortless, yielding great results. I wanted it to be even more possible, easy and even effortless, hence this PR. As far as I'm concerned this is ready for android. It is stable and seems reliable. The android native example is alive and kicking, you should take a look. It is very convenient to debug with.

In Depth

  1. I've introduced 2 new gesture handlers: DragGestureHandler & DropGestureHandler, both extend PanGestureHandler.

  2. Activation logic:

    1. A touch starts.
    2. DragGestureHandler's PanGestureHandler decides if to activate or not. If it does a drag session begins.
    3. DropGestureHandlers are continuously activated as the event proceeds and cancelled once the pointer exits their bounds.
    4. The event ends with a drop/end action
  3. I've decided on a simple mechanism to determine if a drag event can relate with a drop handler. It is passed as types: number | number[] prop. If both share a type the event is delivered.

  4. Android doesn't fire MotionEvent during DragEvent so I've synthesized one and used GestureHandlerOrchestrator to deliver it normally in order to support simultaneous handlers etc... I think this is a sound approach but might be vulnerable. If issues will rise due to this (e.g. bad MotionEvents) then I suggest adjusting the synthetic MotionEvent to better impersonate a real one.

  5. Speaking of, I've managed to run a drag handler and a scroll handler simultaneously only on the native example and couldn't get it working in react-native. I hope you guys can suggest the source of this issue, the only thing that I can think of is a root view that needs to listen to onDrag as well as intercepting the event (which sounds wrong) or some internal react-native logic. FIXED

  6. I've written the handlers to be compatible with external drag events coming from different apps (both platforms seem to support this) but there's some minor work/testing to be done to finalize it.

  7. Best practice regarding nesting Drag/Drop handlers fora view hierarchy: Drag at top, Drop nested deep.

  8. In the example I've tested a few situations. One of them being using the event data only and handling UI with pan event data. This is possible when passing shadowEnabled=false. The thing with this is elevation, no problem on the native example (guessing it is caused by ShadowNode overriding it). This is a caveat for future work, drag shadow works well and is the native approach.

Conclusion

Already I can say this saves tons of boilerplate and logic that mimics drag&drop interactions. I am sure it will resolve a lot of performance issues as well, especially when integrated with reanimated. I have an idea for this too, using a PropsNode to update a drop zone with the data received from the drag event, all without crossing the bridge once. Sounds exciting, and you can see it opens up new possibilities.

Next Up iOS

ShaMan123 commented 4 years ago

I found a MAJOR bug that was preventing interaction management with native view handlers. cf8e911 fixes the ability to configure interactions with NativeViewGestureHandler based on createHandler#transformIntoHandlerTags.

ShaMan123 commented 4 years ago

Check it out - Multi window support!

multi window drag gesture handling

ShaMan123 commented 4 years ago

@osdnk @kmagiera android is ready for reviewing

kmagiera commented 4 years ago

Thank you for working on this however I need to point out that this PR is very difficult to review at the current state. What could help is to split it into smaller changes (not sure if I understand correcly but seem like there are some unrelated changes here), and or provide some pointers in the PR description as to what changes are being made, in which files and why.

Apart from that I am not sure if I understand the motivation and why PanGestureHandler cannot be used for the use case you are describing?

ShaMan123 commented 4 years ago

@kmagiera please refer to the top. I've edited the first comment, hope it gives a better understanding of this PR in terms of motivation, necessity and logic. I've updated the fork so it is even with master.

ShaMan123 commented 4 years ago

android is done

osdnk commented 4 years ago

Thanks for your work. I regret to say that this code is unreviewable. I hope it's a valuable code but I'm afraid merging over 4k lines in one batch. Can you maybe split your work in a few independent PRs to make it possible to review?

ShaMan123 commented 4 years ago

@osdnk What do you suggest? How should I split the code? sounds awkward. I can't think of an effective way of doing it. Are there any other options?

ShaMan123 commented 4 years ago

The only thing I can think of is extracting the android native app upgrade (1K of changes)

osdnk commented 4 years ago

So there's no way for making it a bit smaller? I'll be even happy with merging smaller chanks of compilable non-breaking code if we'll be able to review it

ShaMan123 commented 4 years ago

I'll try something. What about DMNodes? I've tried pinging you there

robertherber commented 4 years ago

@ShaMan123 I tried the examples but for some reason can't get it to work. Views are showing but nothing happens when I long press the items on Android. Tried adding the resizeableActivity flag - any other ideas on how to proceed?

Really excited about this possibility since I'm interested in both cross-app functionality and smooth drag-and-drop in lists. I'd be invested in seeing it working on iOS and the web as well. Running a moto g7 play on Android 9 (React Native 0.62.2).

ShaMan123 commented 4 years ago

Which example did you try Drag & Drop or Drag & Drop in FlatList? Did you try just dragging without long pressing?

robertherber commented 4 years ago

@ShaMan123 I tried this and the list

ShaMan123 commented 4 years ago

In Example/draggable index.js is a simple PanGestureHandler, not a DragGestureHandler. list.js should work as should drag.js. Are you running it in the example app or in your own app? I recommend you first check it works in the example to eliminate issues/wrong config.

ShaMan123 commented 4 years ago

And you should pull again and rebuild + change your manifest to match the example's

neiker commented 3 years ago

Awesome work! Looking forward to seeing this merged soon.

I have tried something similar on iOS long time ago but I finally gave up https://github.com/neiker/react-native-drag-n-drop

magrinj commented 3 years ago

Hey guys ! I'm really looking for this adding to RNGH ! Thanks a lot for your work @ShaMan123, hope it will be merge soon !

emclab commented 3 years ago

Thank you and looking forward to the new feature soon!

ShaMan123 commented 3 years ago

Updated from origin, build passes

ShaMan123 commented 3 years ago

fixed a bunch of edge case bugs

ShaMan123 commented 3 years ago

I think android is ready for roll out

nandorojo commented 3 years ago

Would this support web too?

ShaMan123 commented 3 years ago

Would this support web too?

@nandorojo Why not? It can wrap around react-dnd

nandorojo commented 3 years ago

That would be awesome

nandorojo commented 2 years ago

@ShaMan123 would it be possible to move this PR into a standalone npm package, which uses gesture handler as a peer dependency? That way, you could get community support and testing to help get this merged.

It doesn't seem likely that this will get merged as-is right now, but I'd love to get to use it in my app. I think making it a separate package (like native-dnd) would be awesome.

What do you think?

ShaMan123 commented 2 years ago

I think it isn't feasible. The code can't be separated from the core. And can't be patched together. There is logic that changed. You could PR my fork and I'll merge it. I think this is worth working on to support ios.

בתאריך יום ד׳, 19 במאי 2021, 15:31, מאת Fernando Rojo ‏< @.***>:

@ShaMan123 https://github.com/ShaMan123 would it be possible to move this PR into a standalone npm package, which uses gesture handler as a peer dependency? That way, you could get community support and testing to help get this merged.

It doesn't seem likely that this will get merged as-is right now, but I'd love to get to use this. I think making it a separate package (like native-dnd) would be awesome.

What do you think?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/software-mansion/react-native-gesture-handler/pull/963#issuecomment-844058932, or unsubscribe https://github.com/notifications/unsubscribe-auth/AIGAW4OW25HSYEEDKHAFZFDTOOVTDANCNFSM4KTI6WOQ .

nandorojo commented 2 years ago

I see. How far along is it? Is it all working for you?

ShaMan123 commented 2 years ago

Android works completely. iOS isn't supported. Clone the fork and run the example app

In Example/draggable index.js is a simple PanGestureHandler, not a DragGestureHandler. list.js should work as should drag.js. Are you running it in the example app or in your own app? I recommend you first check it works in the example to eliminate issues/wrong config.

artemis-prime commented 1 year ago

+1 Let's get this merged in!