Draggable chat-heads in React Native
Tags: reactnative, tutorial, javascript, react
Published: 2019-09-24 08:20:39

Most of us are familiar with Facebook’s floating heads that are screaming for your attention on top of all other apps. At the time, it was a novel concept, somewhat annoying, but still, something new.

Recently, we’ve had a client that requested similar behavior, just in-app, which would show draggable profile photos which could be paired by overlapping one over another.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/7zfkuqgp507z0gfy6pau.png)

Since you’re probably skimming this part to see if a solution you’re looking forward is here, let get straight to the point.

We’ve used **panResponder** and wrapped each person in one.

```tsx
constructor(props: Props) {
        super(props);
        this.pan.addListener((value) => this.position = value);

        this.panResponder = PanResponder.create({
            onMoveShouldSetPanResponder: (evt, gestureState) => true,

            onPanResponderGrant: () => {
                this.pan.setOffset({
                    x: this.position.x,
                    y: this.position.y
                });

                this.pan.setValue({ x: 0, y: 0 });
            },

            // Move object while onPress is active. Snapping is handled later.
            onPanResponderMove: Animated.event([
                null, { dx: this.pan.x, dy: this.pan.y }
            ]),

            // Handle swiper behaviour after object is released.
            onPanResponderRelease: (e, { dx, dy, vx }) => {
                // Fix jumping when moving the object second time.
                this.pan.flattenOffset();

                // Send ending position to parent component.
                this.props.calculateOverlapping(dx, dy, this.props.person.id);

                // Animate springy tuff.
                Animated.spring(this.pan, { toValue: { x: 0, y: 0 } }).start();
            }
        });
    }
```

#**Register initial people position**

Each person is wrapped in an Animated.View component which means it’s draggable. Animated.View, just as normal View, has an onLayout event which is invoked on mount and layout changes.

Once that event is triggered, we can register this person initial position. They are positioned absolutely, but when reporting position it will use XY coordinates based on the parent they are on (0,0 will be top left corner of the parent element).

```tsx
const newPersonPosition = new PersonPosition(Math.trunc(event.nativeEvent.layout.x + 30), 
Math.trunc(event.nativeEvent.layout.y + 30), userId);
```

The position is truncated since we don’t need extreme precision that horizontal and vertical displacements report (**dx** and **dy** in **onPanResponderRelease**).

PersonPosition here is just a constructor that creates an object with its horizontal and vertical position, together with userId which we can use later on to trigger events on that specific user.

Also, I’ve added 30, a magic number, which is half of the width and height of a component. Reported location (**event.nativeEvent.layout.x**) is a position in the top left corner of the component. If you want to be scientific about this, the proper way would be to check for a component's width and height and add half of it, but I know mine is 60, so I just added half manually. Now we save this since it’s a center of a component, and we need that for overlap calculation.

Position for each person is then pushed into an array which is saved to state:

```tsx
peoplePosition.push(newPersonPosition);this.setState({   peoplePosition});
```

This is to have an easier way of comparing future dropped components to all of the others (using array’s find method).

#**Checking for overlapping**

Main part is to check for overlapping after the user releases the person. We can get the drop coordinates like this:

```tsx
const droppedX = Math.trunc(draggedPerson.startingPointX + dx);
const droppedY = Math.trunc(draggedPerson.startingPointY + dy);
```

Where we take dragged person’s horizontal starting point and add the horizontal displacement and repeat for the vertical axis. The result is once again truncated to remove unneeded decimals.

Then, that **ending** position of the person is checked against the positions of all people that were not dragged:

```tsx
const matchedPerson = notDraggedPeople.find((personPosition: PersonPosition) => 
Math.abs(personPosition.startingPointX - droppedX) < 30 && Math.abs(personPosition.startingPointY - droppedY) < 30);
```

If the dropped person is anywhere inside a set distance from any of the people, we have a match! Here the radius is hardcoded to 30px, but you can set it to whatever you want.

Maybe the best is half the width of an element + some buffer to make it easier to overlap successfully. You definitely want to avoid making it larger than the total width of the elements you’re overlapping to avoid false positives.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/9tosuj4x8j5kl9bzmjki.png)

The distance of 0 means that the two components are perfectly overlapped (their centers match). Distance of 30 (in our case) means that they are touched by the edges. Tweak this number to determine how precise you have to be in order to get a sucesfull match.

If a match is successful, just push the person to the **matchedPeople** array and save it to the state:

```tsx
let matchedPeople = [];

if (matchedPerson) {
    matchedPeople.push(matchedPerson);    
    
    this.setState({
        matchedPeople
    });
}
```

#**Trigger action after ovelapping**

Finally, you probably want to do something after the user overlaps two heads successfully.

In our case, we just listened to state change for matchedPeople in ComponentWillUpdate:

```tsx
componentWillUpdate(nextProps: Props, nextState: State) {
    if (nextState.matchedPeople.length) {
       // Navigate away from the screen
    }
}
```

You should check for changes here to avoid excessive triggering for each component updates, but since we navigated away from this screen once a successful overlaps occur (matchedPeople array is populated), it’s a simple logic to check for.

Provided you’re experienced with panResponder, this code should be easy to replicate. In case you need a refresher on panResponder, I’ve written another article which tackles rotateable circle to select items here:


[https://medium.com/prototyped/circular-swiper-using-pan-responder-and-animated-library-b78eee9784a4](https://medium.com/prototyped/circular-swiper-using-pan-responder-and-animated-library-b78eee9784a4)


Did I mess up somewhere? Have better ideas? Drop us an email at hello@prototyp.digital or visit us at [https://prototyp.digital](https://prototyp.digital). Cheers!