最近在做一个仿Tinder的web应用,这类应用一大特点就是左右滑动(swipe),来实现是否喜欢的判断。我自己做了个简单的Swipe动画实现,放上来记录一下。
可以看到卡片会随着的手指移动,并且根据起始触点的不同,有一定倾斜。滑动程度不够时会返回原位,并有一定弹出又返回的动画。下面是实现的一个简单模拟:
具体实现思路是通过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. 释放卡片后, 卡片归位
当滑动距离不够,或滑动时间过长时会将卡片归位,这里会有一个回归原位后顺着超出一定距离,再弹回的动画。可以将整个返回动画分三部分:
- 手指离开屏幕卡片返回原位
- 卡牌顺着上一阶段的返回动画,稍微弹出一定程度
- 卡牌反向弹回原位
通过 isExtraAnimation 和 isReturnSAnimation来判断动画阶段
//接上一部分的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动画,不过在很多细节上(比如旋转角度,动画速率等)还有待改善。不过,要啥自行车!