DEV Community

Sebastijan Dumancic for PROTOTYP

Posted on • Originally published at blog.prototyp.digital

Draggable chat-heads in React Native

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

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.

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();
            }
        });
    }
Enter fullscreen mode Exit fullscreen mode

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).

const newPersonPosition = new PersonPosition(Math.trunc(event.nativeEvent.layout.x + 30), 
Math.trunc(event.nativeEvent.layout.y + 30), userId);
Enter fullscreen mode Exit fullscreen mode

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:

peoplePosition.push(newPersonPosition);this.setState({   peoplePosition});
Enter fullscreen mode Exit fullscreen mode

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:

const droppedX = Math.trunc(draggedPerson.startingPointX + dx);
const droppedY = Math.trunc(draggedPerson.startingPointY + dy);
Enter fullscreen mode Exit fullscreen mode

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:

const matchedPerson = notDraggedPeople.find((personPosition: PersonPosition) => 
Math.abs(personPosition.startingPointX - droppedX) < 30 && Math.abs(personPosition.startingPointY - droppedY) < 30);
Enter fullscreen mode Exit fullscreen mode

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

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:

let matchedPeople = [];

if (matchedPerson) {
    matchedPeople.push(matchedPerson);    

    this.setState({
        matchedPeople
    });
}
Enter fullscreen mode Exit fullscreen mode

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:

componentWillUpdate(nextProps: Props, nextState: State) {
    if (nextState.matchedPeople.length) {
       // Navigate away from the screen
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

Top comments (0)