如何给拖拽交互添加缓动

1,006 阅读3分钟

前言

移动端的拖拽交互,在项目中经常使用。

实现拖拽也不复杂,通过绑定 touchstarttouchmove、和 touchend 事件,就可以完成 自定义滚轮游戏人物拖拽 等各种交互。

如果想让体验更好一些,添加一些缓动效果,是必不可少的。

没有添加缓动

以上图片是没有添加缓动的,可以看到拖拽起来画面相当灵活,但是随着手指离开屏幕,画面也就戛然而止了。

new.gif

以上图片是添加了缓动的,手指快速的滑动后离开,画面没有快速停止,而是经过一段惯性动作后停止。

添加缓动之后的效果,更符合现实中的惯性习惯。

实现思路

思路一

之前制作缓动,会在 touchmove 事件中通过多个参数来记录实时的拖拽速度,在 touchend 的时候,在通过一定的加速度来让速度逐步减到 0 ,完整代码较长,放在了 文末

这样实现的缺点很多,比如参数很多、逻辑复杂,不能满足快速开发的需求。

思路二

touchmove 阶段,不直接赋值,而是记录下来,通过 requestAnimationFrame 计算差值,逐步赋值给运动的元素,即可实现。

这样实现的优点就很明确了,参数少、逻辑清晰。

接下来详细介绍一下思路二:

代码详解

首先不考虑缓动,先完成基本的拖拽事件:

拖拽事件

let initX = 0;
let initY = 0;
let changeX = 0;
let changeY = 0;
let element = window.document;

element.addEventListener('touchstart',(e)=>{
    initX = e.touches[0].clientX;
    initY = e.touches[0].clientY;
});

element.addEventListener('touchmove',(e)=>{
    const { clientX: moveX, clientY: moveY } = e.touches[0];
    changeX += (moveX - initX);
    changeY += (moveY - initY);
    initX = moveX;
    initY = moveY;
});

通过以上简单的代码,我们就拿到了实时拖拽的 changeXchangeY ,然后赋值给元素即可。

添加缓动

按照之前的思路,我们在 touchmove 中不直接赋值,而是只记录值:

let targetX = 0;
let targetY = 0;

element.addEventListener('touchmove',(e)=>{
    const { clientX: moveX, clientY: moveY } = e.touches[0];
    targetX += (moveX - initX);
    targetY += (moveY - initY);
    initX = moveX;
    initY = moveY;
});

拿到在 touchmove 中记录的 targetXtargetY 之后,只需要思考如何缓慢的把 target 的值赋值给 change ,在这里只需要给 changeXchangeY 赋值的时候,添加一个减速倍率即可:

function touchAnimate(){
    requestAnimationFrame(touchAnimate);
    changeX += (targetX - changeX) * 0.1; // 此处减速倍率设置为 0.1
    changeY += (targetY - changeY) * 0.1;
}

至此,就完成了拖拽的缓动操作,完整代码查看 文末附录

总结

上述的简单代码就实现了拖拽的缓动,同时也是这个逻辑的核心代码。

实际开发中还要考虑添加 touchFlag 、手指移出规定的区域怎么处理等情况。因此我把该拖拽缓动功能制作成了一个组件,方便后续各种类似的项目使用。

组件使用

可以 查看源码 或者使用 npm 安装使用:

npm i drag-easing --save

基础示例

import DragEasing from 'drag-easing';

// 初始化
const de = new DragEasing({
    onDragging: (e)=>{
        // e.changeX 即为添加了缓动的拖拽 X 坐标
        // e.changeY 即为添加了缓动的拖拽 Y 坐标
    },
});

同时 组件 还提供了指定绑定元素、x y 方向的区间限制、以及手指移出交互区域的操作等,更多 API 可以在 点此 查看。

(完)

附录

思路一完整代码

tip: 此处使用到了 ts 语法,代码未处理,但只要阅读思路即可:

private timer:number = null;
private easeTimer:number = null;
private rotateNum:number = 359;
private startX:number = -1;
private moveX:number = -1;
private scaleNum:number = 750 / window.innerWidth;
private easeTime:number = -1;
private tempUnit:number = null;
private easeUnit:number = -1;
private easeDirection:string = '';
private rotateToTimer:number = null;
private rotateToNum:number = null;
private rotateToSpeed:number = null;
private touchStartCtrl(e){
    if(this.rotateToSpeed !== null) return;
    this.easeStop();
    this.startX = e.touches[0].clientX * this.scaleNum;
    this.animate();
}
private touchMoveCtrl(e){
    this.moveX = e.touches[0].clientX * this.scaleNum;
    if(this.moveX < 0 || this.moveX > 750){
        this.touchFinish(false);
    }
}
private touchEndCtrl(e){
    this.touchFinish(this.moveX !== -1);
}
private touchFinish(easeFlag:boolean = true){
    if(easeFlag) this.easing();
    this.startX = -1;
    this.moveX = -1;
    window.cancelAnimationFrame(this.timer);
}
private animate(){
    if(this.startX>-1&&this.moveX>-1){
        const changeNum:number = (this.moveX - this.startX) / 30;
        this.rotateNum = this.rotateNum += changeNum;
        this.tempUnit = 0.05 * (this.moveX-this.startX);
        this.startX = this.moveX;
    }
    this.timer = requestAnimationFrame(()=>{
        this.animate();
    })
}
private easing(){
    if(this.easeTime === -1){
        this.easeTime = 100;
        if(this.tempUnit === 0){
            this.easeStop();
            return;
        }
        this.easeDirection = this.tempUnit > 0 ? 'right' : 'left';
        const maxUnit = 1;
        this.easeUnit = this.tempUnit > 0 ? Math.max(maxUnit, this.tempUnit) : Math.min(maxUnit*-1, this.tempUnit);
    }
    if(this.easeTime > 0){
        this.easeTime = this.easeTime - 1;
        this.easeUnit -= this.easeUnit*0.1;
        this.rotateNum += this.easeUnit;
        this.easeTimer = requestAnimationFrame(()=>{
            this.easing();
        })
    }else{
        this.easeStop();
    }
}
private easeStop(){
    window.cancelAnimationFrame(this.easeTimer);
    this.easeTime = -1;
    this.easeUnit = -1;
}

思路二完整代码

let initX = 0;
let initY = 0;
let changeX = 0;
let changeY = 0;
let targetX = 0;
let targetY = 0;
let element = window.document;

element.addEventListener('touchstart',(e)=>{
    initX = e.touches[0].clientX;
    initY = e.touches[0].clientY;
});

element.addEventListener('touchmove',(e)=>{
    const { clientX: moveX, clientY: moveY } = e.touches[0];
    targetX += (moveX - initX);
    targetY += (moveY - initY);
    initX = moveX;
    initY = moveY;
});

function touchAnimate(){
    requestAnimationFrame(touchAnimate);
    changeX += (targetX - changeX) * 0.1;
    changeY += (targetY - changeY) * 0.1;
}
touchAnimate();