JavaScript鼠标移动事件控制元素移动,实现60帧每秒的流畅效果(使用requestAnimationFrame)。

11

嗨!我有一个问题,就是如何使元素 #drag 平滑移动。

我看了这篇文章:http://www.html5rocks.com/en/tutorials/speed/animations/#debouncing-mouse-events

文章中提到:"当移动元素时,mousemove 事件的问题在于触发太频繁

因此,我尝试使用他们的方法:使用 requestAnimationFrame + boolean 检查

请查看此示例以实时查看效果:https://jsfiddle.net/5f181w9t/

HTML:

<div id="drag">this is draggable</div>

CSS:

#drag {width:100px; height:50px; background-color:red; transform:translate3d(0, 0, 0); }

JS :

JS:
var el               = document.getElementById("drag"),
    startPosition    = 0, // start position mousedown event
    currentPosition  = 0, // count current translateX value
    distancePosition = 0, // count distance between "down" & "move" event
    isMouseDown      = false; // check if mouse is down or not 

function mouseDown(e) {
    e.preventDefault(); // reset default behavior
    isMouseDown     = true;
    startPosition   = e.pageX; // get position X
    currentPosition = getTranslateX(); // get current translateX value
    requestAnimationFrame(update); // request 60fps animation
}    

function mouseMove(e) {
    e.preventDefault();
    distancePosition = (e.pageX - startPosition) + currentPosition; // count it!  
}

function mouseUp(e) {
    e.preventDefault();
    isMouseDown = false; // reset mouse is down boolean
}

function getTranslateX() {
   var translateX = parseInt(getComputedStyle(el, null).getPropertyValue("transform").split(",")[4]);

   return translateX; // get translateX value

}

function update() {
    if (isMouseDown) { // check if mouse is down
        requestAnimationFrame(update); // request 60 fps animation
    }
    el.style.transform = "translate3d(" + distancePosition + "px, 0, 0)";
  // move it!
}

el.addEventListener("mousedown", mouseDown);
document.addEventListener("mousemove", mouseMove);
document.addEventListener("mouseup", mouseUp);

这是完成它的正确方法吗?

我的代码有什么问题?

谢谢


是的,你的代码有什么问题吗? - Kaiido
感谢Kaiido的评论。真的吗?我觉得,它不是很流畅? - Ching Ching
在我的火狐浏览器上非常流畅,在谷歌浏览器上有一点闪烁,这是事实,但肯定是因为这个红色。 - Kaiido
2个回答

13
问题在于您在mouseDown事件监听器中使用了requestAnimationFrame()。您应该在mouseMove事件监听器中完成所有更新,因为您希望在鼠标移动而不是鼠标单击时更新显示。因此,您应该在update函数中的isMouseDown条件下更新所有变量。我建议按以下方式更正代码。

var el               = drag,
    startPosition    = 0, // start position mousedown event
    currentPosition  = 0, // count current translateX value
    distancePosition = 0, // count distance between "down" & "move" event
    isMouseDown      = false, // check if mouse is down or not
    needForRAF       = true;  // to prevent redundant rAF calls

function mouseDown(e) {
  e.preventDefault(); // reset default behavior
  isMouseDown     = true;
  currentPosition = getTranslateX(); // get current translateX value
  startPosition   = e.clientX; // get position X
}    

function mouseMove(e) {
  e.preventDefault();
  distancePosition = (e.clientX - startPosition) + currentPosition; // count it!  
  if (needForRAF && isMouseDown) {
    needForRAF = false;            // no need to call rAF up until next frame
    requestAnimationFrame(update); // request 60fps animation
  }
}

function mouseUp(e) {
  e.preventDefault();
  isMouseDown = false; // reset mouse is down boolean
}

function getTranslateX() {
  var translateX = parseInt(getComputedStyle(el, null).getPropertyValue("transform").split(",")[4]);
  return translateX; // get translateX value
}

function update() {
  needForRAF = true; // rAF now consumes the movement instruction so a new one can come
  el.style.transform = "translateX(" + distancePosition + "px)"; // move it!
}

el.addEventListener("mousedown", mouseDown);
document.addEventListener("mousemove", mouseMove);
document.addEventListener("mouseup", mouseUp);
#drag {
  width: 100px;
  height: 50px;
  background-color: red;
  transform: translateX(0);
}
<div id="drag">this is draggable</div>

请在这里检查。


1
如果mousemove()也调用animationframe,为什么要在update()中递归请求它?如果每帧有多个mousemove事件,那么之前的请求不需要被取消吗? - Okku
1
@Ilpo Oksanen,是的,你似乎是对的。在这种特定情况下,requestAnimationFrame 的伪递归喂养是多余的。这是我的一个疏忽,残留自我更新显示的标准方法。非常感谢你指出来。我会相应地进行更正。 - Redu
4
你仍在大量调用requestAnimationFrame,这些调用将同时执行。使用一个布尔值来检查是否已经在本帧中处理了事件,在调用rAF之前将其设置为true,并在rAF回调函数中将其设置为false。如果它已经设置为true,则不要调用rAF。 - Kaiido
@Kaiido感谢您的恰当评论和“负责任地编码”的警告。我已按建议修改了代码。 - Redu

8

您的代码应该已经正常工作了。然而,这里有另一种方法来实现:

您需要确保每帧只有一个requestAnimationFrame调用通过,否则在下一次repaint时将会多次调用update(),这可能会导致延迟并降低您的fps。为此,您需要保存请求的帧,并在每个mousemove事件中检查是否已经有一帧排队等候。如果有的话,您需要使用cancelAnimationFrame来取消它并发出新的请求。这样,update()就只会按照浏览器能够渲染更改的频率调用(在大多数浏览器中为60fps)。

function mouseDown(e) {
    e.preventDefault(); // cancel default behavior
    isMouseDown     = true;
    startPosition   = e.pageX; // get position X
    currentPosition = getTranslateX(); // get current translateX value
}

var lastUpdateCall=null;
function mouseMove(e){
    if(isMouseDown){ //check if mousedown here, so there aren't any unnecessary animation frame requests when the user isn't dragging
        e.preventDefault(); // You probably only want to preventDefault when the user is actually dragging

        if(lastUpdateCall) cancelAnimationFrame(lastUpdateCall); //if an animation frame was already requested after last repaint, cancel it in favour of the newer event

        lastUpdateCall=requestAnimationFrame(function(){ //save the requested frame so we can check next time if one was already requested
            distancePosition = (e.clientX - startPosition) + currentPosition; // Do the distance calculation inside the animation frame request also, so the browser doesn't have to do it more often than necessary 
            update(); //all the function that handles the request
            lastUpdateCall=null; // Since this frame didn't get cancelled, the lastUpdateCall should be reset so new frames can be called. 
        });
    }
}

function update(){
    el.style.transform = "translateX(" + distancePosition + "px)";// move it!
}

如果lastUpdateCall不是null的话,你也可以选择不再调用requestAnimationFrame函数,但这意味着每次event触发时都要计算距离,否则动画将会比你的鼠标慢最多20毫秒。我想两种方法都可以。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接