Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drag Gesture Handler #963

Open
wants to merge 248 commits into
base: main
Choose a base branch
from

Conversation

ShaMan123
Copy link
Contributor

@ShaMan123 ShaMan123 commented Feb 11, 2020

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

  1. data - pass object to DropGestureHandler on drop via gesture event.
  2. 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
  }

#963 (comment)

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!

organize code
add cancel handler check
Was caused because view was hidden before the drag events began to propagate causing a situation that  if a drag has started but no events would be emitted because the finger stopped touching the screen the view would remain invisible.
This was fixed by:
Moving `setVisibility(false)` to `onHandle` instead of `startDragging`.
Reinstating `onHandle(MotionEvent event)` to the super pan handler so it can handle this exact case of `ACTION_UP` fired after `startDragging` and propagate state changes
revert ACTION_DRAG_CANCELLED approach
finalizes 8b0e840
`setOnDragListener` is called from the root helper
dosn't seem to yield the desired effect... but is logially correct
…t to unmount

We want to avoid flickering in case the handler has been reset after being cancelled because it's view is being removed from the tree.
We don't want the view reappearing just to get removed a few frames later, so we use postDelayed to reset visibility after react has unmounted the view .
```
@ShaMan123
Copy link
Contributor Author

fixed a bunch of edge case bugs

update drag shadow after joining drag handler cancellation
resetHandlerViewState is now called once handler is finished, not reset, which is the correct approach. Hope this doesn't intoduce bugs.
add test cases for handler cancellation  edge cases
cancel remaining handlers that originated from the MotionEvent that aren't related to drag event and are probably configured as simultaneous/waitFor handlers on drag handlers
@ShaMan123
Copy link
Contributor Author

I think android is ready for roll out

@nandorojo
Copy link
Contributor

Would this support web too?

@ShaMan123
Copy link
Contributor Author

Would this support web too?

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

@nandorojo
Copy link
Contributor

That would be awesome

@nandorojo
Copy link
Contributor

nandorojo commented May 19, 2021

@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
Copy link
Contributor Author

ShaMan123 commented May 19, 2021 via email

@nandorojo
Copy link
Contributor

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

@ShaMan123
Copy link
Contributor Author

ShaMan123 commented May 22, 2021

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
Copy link

+1 Let's get this merged in!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants