I’ve been working on a Tinder-like Web app, and one of the features of this type of app is to swipe left and right to determine if you like it or not. I made a simple Swipe animation myself and put it up for recording.

You can see that the card moves with the fingers and is slightly tilted depending on the starting contact. When the sliding degree is not enough, it will return to the original position, and there is a certain pop-up and return to the animation. Here is a simple simulation of the implementation:

The concrete realization idea is to realize by the combination processing of three different types of events of TouchStart, TouchMove and Thouchend of TouchEvent. The implementation is described below. (In addition to these three types, TouchEvent also has a TouchCancel type, which is not covered in this article.)

Specific function realization

1. The card moves with the finger

With the TouchStart event, the position of the initial contact is recorded. TargeTouches is a TouchList that contains objects of all touch points that are still in contact with the touch surface. Within which the event is triggered, it is the current target element. This is our card right here. The pageX (pageY) property is the X (Y) coordinate of the contact relative to the left edge of the HTML document

handleStart = ({ targetTouches }) = > {
        this.startPosition.x = targetTouches[0].pageX;
        this.startPosition.y = targetTouches[0].pageY;
         this.startTime = Date.now();
}
Copy the code

The touchMove event calculates the X and Y offsets of the current touch and the start touch, and uses the Transfrom attribute Translate to move the card position with the touch

handleMove = ({ target: el, targetTouches }) = > {
        const offX = targetTouches[0].pageX - this.startPosition.x;
        const offY = targetTouches[0].pageY - this.startPosition.y;

        el.style.transform = `translate(${offX}px, ${offY}px) )`}}Copy the code

2. Tilt the card according to different contacts

The previous section only implemented the card position to follow the touch, the card itself does not change, no soul. In order for the card to be tilted (or rotated) to follow your finger, you need to decide which way to tilt it. Based on the original GIF, you can see that the starting contact slides to the right from the top, or tilts clockwise and counterclockwise when sliding to the left from the bottom. This is a natural counterpoint to finger (thumb) movements. The next step is to use the isTop to record whether the contact is on top (I’m using the position relative to the screen here). IsRightWise records whether the card slides to the left or to the right. IsClockWise determines clockwise by the first two. Add the rotate field to the transfrom property

handleMove = ({ target: el, targetTouches }) = > {
        const offX = targetTouches[0].pageX - this.startPosition.x;
        const offY = targetTouches[0].pageY - this.startPosition.y;
        this.isRightWise = offX > 0 ? true : false;
        const isTop = this.startPosition.y < window.screen.height / 2 ? true : false;  // Determine the initial contact Y-axis position
        el.style.transformOrigin = `center ${isTop ? "bottom" : "top"}`;     // Perform different animations according to finger position

        if ((isTop && offX < 0) | | (! isTop && offX >0)) {
            this.isClockWise = false
            el.style.transform = `translate(${offX}px, ${offY}px) rotate(-The ${Math.abs(offX) / 10}deg)`
        }
        if((! isTop && offX <0) || (isTop && offX > 0)) {
            this.isClockWise = true;
            el.style.transform = `translate(${offX}px, ${offY}px) rotate(The ${Math.abs(offX) / 10}deg)`}}Copy the code

3. After releasing the card, remove it

Move the card beyond the threshold state checked, remove the card, and there will be an animation to remove the card from the screen. Note: Unlike the previous two events, we use changedTouches to create the card element.

The checkedThresholdX and checkedThresholdT thresholds are set, representing the distance traveled and the sliding time on the X axis, respectively. The first one is easy to understand, and the second one is also checked when fast swiping (after all, there are crazy right swipes)

handleEnd = ({ target: el, changedTouches }) = > {
        const isRightWise = this.isRightWise;
        const isClockWise = this.isClockWise;

        const offX = changedTouches[0].pageX - this.startPosition.x;
        const offTime = Date.now() - this.startTime;
        
        const checkedThresholdX = 100;
        const checkedThresholdT = 100;

        if (Math.abs(offX) > checkedThresholdX || offTime < checkedThresholdT) {
            //check card
            (function checkAnimation(offX) {
                const newOffX = offX > 0 ? offX + offVal : offX - offVal;
                el.style.transform = `translateX(${newOffX}px) rotate(${isClockWise ? Math.abs(newOffX) / 10 : -Math.abs(newOffX) / 10}deg)`;
                if (offX > -3000 && offX < 3000) {
                    // Judgment needs to be improved
                    window.requestAnimationFrame(() = > checkAnimation(newOffX));
                }
            })(offX)
        }
Copy the code

4. After releasing the card, the card is restored

When the slide distance is not enough, or the slide time is too long, the card will be put back, there will be a return to the original position, follow a certain distance, and then bounce back animation. The entire return animation can be divided into three parts:

  1. Finger off screen card returns to original position
  2. The card follows the return animation of the previous stage and pops up to a certain extent
  3. The card flipped back to its original position

Use isExtraAnimation and isReturnSAnimation to determine the animation stage

// Add part of the if condition
else {
            //return card

            let isExtraAnimation = false;
            let isReturnSAnimation = false;

            const reverseThreshold = 20;
            (function returnAnimation(offX) {
                let newOffX = null;
                let newOffDeg = null;
                if(! isReturnSAnimation) {console.log("111")
                    // Stage 1: Release and return to the original position, and beyond a certain degree
                    newOffX = isRightWise ? offX - offVal : offX + offVal;
                    // if(! newOffX) newOffX = offX > 0 ? 1: + 1;
                    if(! isExtraAnimation && ((isRightWise && newOffX <0) | | (! isRightWise && newOffX >0))) {
                        newOffX = 0;
                    }
                    if (newOffX === 0) {
                        console.log("222")
                        // The first stage is complete, the first stage is back to the original position, the next stage is second
                        isExtraAnimation = true;
                    }
                    newOffDeg = isClockWise ? Math.abs(newOffX) / 10 : -Math.abs(newOffX) / 10;
                    if (isExtraAnimation) {
                        newOffDeg = -newOffDeg;
                    }
                    if (isExtraAnimation && Math.abs(newOffX) > reverseThreshold) {
                        // Go to stage 3 next time
                        isReturnSAnimation = true;
                    }
                    el.style.transform = `translateX(${newOffX}px) rotate(${newOffDeg}deg)`;
                } else {
                    // Stage 3: the process of returning to the original position after the threshold is exceeded
                    newOffX = offX > 0 ? offX - offVal : offX + offVal;
                    if ((isRightWise && newOffX > 0) | | (! isRightWise && newOffX <0)) {
                        newOffX = 0;
                    }
                    newOffDeg = isClockWise ? -Math.abs(newOffX) / 10 : Math.abs(newOffX) / 10;
                    el.style.transform = `translateX(${newOffX}px) rotate(${newOffDeg}deg)`;
                }

                if(! (newOffX ===0 && isReturnSAnimation)) {
                    window.requestAnimationFrame(() = > returnAnimation(newOffX))
                }
            })(offX);

        }
Copy the code

5, prevent the influence between different cards

As a Swipe event handler class

export default class handleSwipe {
    constructor(cb) {
        this.cb = cb;
        this.startPosition = {
            x: 0.y: 0
        }
        this.startTime = null;
        this.isClockWise = null;
        this.isRightWise = null; // The direction in which card is being dragged
    }
    handleStart....
    handleMove...
    handleEnd...
    
}
Copy the code

Generate an instance for each card as a callback function (use React event and JSX syntax below)

const handler = new handleSwipe(checkCallback);
    return(
        <div className="card"
        onTouchStart={handler.handleStart}
        onTouchMove={handler.handleMove}
        onTouchEnd={handler.handleEnd}>
            <span>
            {props.id}
            </span>
        </div>
    )
Copy the code

conclusion

Swipe simulates the Swipe animation of Tinder cards in general, but many details (rotation Angle, animation speed, etc.) need to be improved. But what a bike!