仿Tinder,TouchEvent事件处理,实现Swipe动画

932 阅读2分钟

最近在做一个仿Tinder的web应用,这类应用一大特点就是左右滑动(swipe),来实现是否喜欢的判断。我自己做了个简单的Swipe动画实现,放上来记录一下。

IMB_jnbT8w.gif

可以看到卡片会随着的手指移动,并且根据起始触点的不同,有一定倾斜。滑动程度不够时会返回原位,并有一定弹出又返回的动画。下面是实现的一个简单模拟:

IMB_vs6JlX.gif

具体实现思路是通过TouchEvent的touchstart, touchmove,thouchend三种不同类型事件的组合处理来实现。下面会分别介绍具体实现 ( 除了这三个类型,TouchEvent还有一个touchcancel类型,不过本文并不会涉及。)

具体功能实现

1. 卡片跟随手指移动

通过touchstart事件,记录最开始触点的位置。targeTouches是个TouchList,包含仍与触摸面接触的所有触摸点的对象。事件触发在哪个内,它就是当前目标元素。在这里就是我们的卡片。pageX(pageY)属性是触点相对于HTML文档左边沿的的X(Y)坐标.
startTime时间戳,会在第三个功能处用到

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

通过touchmove事件,计算当前触点和touch开始触点在X,Y方向上的偏移量,使用transfrom属性translate使卡片位置跟随触点移动

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) )`
        }
    }

2. 根据不同触点倾斜卡片

上一部分仅仅实现了卡片位置跟随触点,卡牌本身没有变化,毫无灵魂。 为了让卡片跟随手指,发生一定倾斜(或者说旋转),需要先判断应该往哪倾斜。
根据最初的动图,可以看到起始触点位于上部向右滑,或位于下部向左滑时顺时针倾斜,逆时针正好相对。这样和手指(大拇指)动作相照应,比较自然。
下面通过 isTop 记录触点是否在上部(我这里用相对于屏幕的位置)。isRightWise记录卡片是往滑还是往左滑, isClockWise通过前两个判断是否顺时针。
transfrom属性增加rotate字段

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;  //初始触点y轴位置判断
        el.style.transformOrigin = `center ${isTop ? "bottom" : "top"}`;     //根据手指位置实现不同动画

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

3. 释放卡片后,移出卡片

移动卡牌距离,超出阈值后状态checked,将卡片移除,会有一个移出屏幕的动画。
注意: 和前面两个事件不同的是这里我们用changedTouches来获得卡片元素,(此时手指已经离开屏幕,不存在触点,也就不能用targetTouches来获得元素)

分别设置了 checkedThresholdX 和 checkedThresholdT 两个阈值,分别代表在x轴上的移动距离和滑动时间。第一个很好理解,第二个是当快速滑动是同样判定为check(毕竟会有疯狂右滑的老哥:)

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) {
                    //判定有待改善
                    window.requestAnimationFrame(() => checkAnimation(newOffX));
                }
            })(offX)
        }

4. 释放卡片后, 卡片归位

当滑动距离不够,或滑动时间过长时会将卡片归位,这里会有一个回归原位后顺着超出一定距离,再弹回的动画。可以将整个返回动画分三部分:

  1. 手指离开屏幕卡片返回原位
  2. 卡牌顺着上一阶段的返回动画,稍微弹出一定程度
  3. 卡牌反向弹回原位

通过 isExtraAnimationisReturnSAnimation来判断动画阶段

//接上一部分的if条件
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")
                    //阶段一:释放后归回原位,并超出一定程度
                    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")
                        //第一阶段完成, 第一次归原位,下次进入第二阶段
                        isExtraAnimation = true;
                    }
                    newOffDeg = isClockWise ? Math.abs(newOffX) / 10 : -Math.abs(newOffX) / 10;
                    if (isExtraAnimation) {
                        newOffDeg = -newOffDeg;
                    }
                    if (isExtraAnimation && Math.abs(newOffX) > reverseThreshold) {
                        //反向超出原位一定幅度完成, 下次进入第三阶段
                        isReturnSAnimation = true;
                    }
                    el.style.transform = `translateX(${newOffX}px) rotate(${newOffDeg}deg)`;
                } else {
                    //第三阶段:超出阈值后反向返回原位的过程
                    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);

        }

5,防止不同卡片之间的影响

作为一个Swipe事件处理类

export default class handleSwipe {
    constructor(cb) {
        this.cb = cb;
        this.startPosition = {
            x: 0,
            y: 0
        }
        this.startTime = null;
        this.isClockWise = null;
        this.isRightWise = null; //card被拖拽的方向
    }
    handleStart....
    handleMove...
    handleEnd...
    
}

为每个卡片生成一个实例作为回调函数(下面用的是React事件和JSX语法)

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

总结

大致上模拟了Tinder里卡片的Swipe动画,不过在很多细节上(比如旋转角度,动画速率等)还有待改善。不过,要啥自行车!