相信对于很多前端的同学来说,拖动并不陌生,网上随随便便搜一下能搜出很多关于拖动的文章,github上也有很多关于拖动的库可以用。最近接到一个需求,需要修改线上一个拖动窗口的功能。类似下图:
看似简单的问题背后却隐藏着很多坑,让我不得不写这么一篇文章梳理一下。
我们之前的拖动代码也许就是某位同学从网上找的一篇拖动的文章写的,存在的问题如下:
1. 第一个问题:拖动不流畅,卡顿
2. 第二个问题:背景是个Flash游戏,窗口经常被Flash盖住
对于第一个问题,拖动不流畅,卡顿。理所当然我必须看下代码
// 鼠标按下div1时
obj.onmousedown = function(e) {
var evnt = e || event; // 得到鼠标事件
disX = evnt.clientX - obj.offsetLeft; // 鼠标横坐标 - div1的left
disY = evnt.clientY - obj.offsetTop; // 鼠标纵坐标 - div1的top
// 鼠标移动时
document.onmousemove = function(e) {
var evnt = e || event;
var x = evnt.clientX - disX;
var y = evnt.clientY - disY;
var window_width = document.documentElement.clientWidth - obj.offsetWidth;
var window_height = document.documentElement.clientHeight - obj.offsetHeight;
x = ( x < 0 ) ? 0 : x;
x = ( x > window_width ) ? window_width : x;
y = ( y < 0 ) ? 0 : y;
y = ( y > window_height ) ? window_height : y;
}
// 鼠标抬起时
document.onmouseup = function() {
document.onmousemove =null;
document.onmouseup = null;
}
return false;
};
我觉得这种写法不好,而且有bug,所以我改写了个方法。
var ox = 0;//原始坐标
var oy = 0;//原始坐标
var moveObj = $('move-div')
moveObj.on('mousedown', function(event) {
ox = parseInt(event.pageX);
oy = parseInt(event.pageY);
that.target = that.videoBox;
});
$(document).on('mouseup', function(event) {
that.target = null;
ox = 0;
oy = 0;
});
$(document).on('mousemove', function(event) {
if (that.target) {
var nX = parseInt(event.pageX);
var nY = parseInt(event.pageY);
var newX = nX - ox;
var newY = nY - oy;
var realX = targteOffsetObj.left + newX;
var realY = targteOffsetObj.top + newY;
that.target.css({left: realX, top: realY});
}
});
我只需要将绑定范围扩大到document下,这样就不会丢失焦点。然后每次点击的时候设定target,然后计算移动的相对位置,修改下css就搞定移动的问题。
理想很丰满,现实很骨感。
这样做依然存在问题,那就是移动快的时候,鼠标移动速度太快移到了背景的Flash上面,瞬间就会失去event对象,这种情况下还是会出现卡顿现象。
我第一个方法是绑定个mouseout,希望通过监听mouseout时实时fix一下坐标。但是这样并不理想,mouseout只能监听到刚移出几像素的时候,移动范围大了还是会丢失,还是会非常卡。所以,我后来想了个方案,在点击的时候给移动的对象加一个class,class里面是给宽高各加了200px。这样,即使移动很快,只要不超过这200px的范围,那么就一直可以监听到事件。后来试了下效果确实还不错。
第一个问题算是解决了,那么就开始搞第二个问题。
对于Flash背景来说,即使扩大了宽高,只要进入Flash的范围,就会被其抢走焦点。所以我第一个方案是将绑定改成draggable的方式,因为drag毕竟是HTML5的新属性,没准能抢过Flash。带着这种希望,我实现了第二种拖动方法。
moveObj.attr('draggable', 'true');
moveObj.on('dragstart', function(event) {
ox = parseInt(event.originalEvent.x);
oy = parseInt(event.originalEvent.y);
that.target = that.videoBox;
moveObj.addClass('choose');
//让火狐浏览器支持drag事件
event.originalEvent.dataTransfer.setData("text", event.target.innerHTML);
});
moveObj.on('drag', function(event) {
if (that.target) {
//松开那一下修改
if (event.originalEvent.x == 0 && event.originalEvent.y == 0) {
return false;
}
var nX = parseInt(event.originalEvent.x);
var nY = parseInt(event.originalEvent.y);
var newX = nX - ox;
var newY = nY - oy;
var realX = targteOffsetObj.left + newX;
var realY = targteOffsetObj.top + newY;
that.move(realX, realY);
}
});
moveObj.on('dragend', function(event) {
that.target = null;
ox = 0;
oy = 0;
moveObj.removeClass('choose');
callback();
//防止个别恶心浏览器下进行搜索
event.preventDefault();
});
写完试了一下,效果不错,而且毕竟是HTML5原生支持的,效率会比自己写的js效率高。但是这里还是遇到了两个坑,第一个是火狐下draggable不执行,解决方法是在dragstart的时候hack一下。
event.originalEvent.dataTransfer.setData("text", event.target.innerHTML);
但是第二个坑是即使hack成功,他的对象里没有可用来计算相对位移的属性,所以最终火狐下还是要屏蔽这种方法。
当然还有一个最关键的问题,draggable的元素并不能抢过Flash,焦点还是让Flash无情的抢走了。
所以,我们又回归到了问题的核心:
到底什么东西能够抢过Flash,焦点不会被Flash抢走呢?
后来我灵光一现,既然Flash这么牛逼,为什么我不用Flash来做我的容器,这样就不会被Flash抢走焦点了。于是,就有了第三个拖动的方法。
//通过swfobject创建flash,替换掉原来的move-object
window.onload = function () {
that.createFlash();
};
//生成了flash需要需要时间,所以不能直接绑定在move-object上面
$(document).on('mousedown', '#move-object', function(event) {
ox = parseInt(event.pageX);
oy = parseInt(event.pageY);
that.target = $('#video_content');
moveObj.addClass('choose');
event.preventDefault();
event.stopPropagation();
});
$(document).on('mouseup', '#move-object', function(event) {
that.target = null;
ox = 0;
oy = 0;
moveObj.removeClass('choose');
event.preventDefault();
event.stopPropagation();
});
$(document).on('mousemove', function(event) {
//document.onmousemove = function(event) {
if (that.target) {
var nX = parseInt(event.pageX);
var nY = parseInt(event.pageY);
var newX = nX - ox;
var newY = nY - oy;
var realX = targteOffsetObj.left + newX;
var realY = targteOffsetObj.top + newY;
that.move(realX, realY);
event.preventDefault();
event.stopPropagation();
}
});
这个Flash是请我同事帮我写的空的Flash,什么操作都没有,它只是做一件事,不让背景Flash抢去焦点。事实证明,确实做到了。即使鼠标范围移出到Flash背景上,只要鼠标不松开,事件就不会消失。而且不管我鼠标移动多快,焦点都在这个Flash元素上。
所以,困扰已久的Flash背景下拖动的问题算是解决了。
最终我的程序会自动选择适合拖动方式,支持Flash的优先使用Flash,如果不支持Flash优先使用drag,如果不支持drag就用最初的js拖动。
但是,特殊浏览器下比如IE,窗口游戏中被Flash覆盖的问题却始终还未解决。我试了很多方案,最终针对游戏中被Flash覆盖,我用了重新设置宽高的方式强制调用浏览器重绘机制,可以解决被覆盖的问题,窗口不会再被盖掉。
this.videoBox.css('width', this.videoBox.width());//调用重绘机制
(这里需要注意的是,网上很多说修改zoom属性,我这里并没有效果,所以用的只是宽。)
实际测试中,又发现了一个新的问题,就是窗口虽然因为重绘显示了,但是只显示了一部分,还有一部分不显示。
最终,我们用Iframe作为垫片,做一个等大的Iframe在我的窗口下,然后移动窗口的时候同步移动这个垫片。但是美中不足的是这个Iframe不能透明,是一个黑色的背景色,设置透明也会透出Flash本身的黑底。
这点美中不足暂时还没办法解决。但是这个拖动问题已经算解决了。
最后,说一下忙了这一通总结出的两点结论:
1.只有Flash可以抢过Flash的焦点
2.只有Iframe可以盖住Flash
我知道这个问题、这个场景可能很奇葩,很多同学并不会遇到。而我因为要解决这个问题,去查了很多资料,从一个拖动,引发出很多不同的技术,不一样的思维,学到了很多其他的知识。
所以,我这里想说的是,作为前端,一定要有发散的思维,要从不同的维度考虑问题。拖动的问题可以从Flash上解决,那么其他的问题呢?
希望我写的这篇文章大家看了有那么一点点收获,获得一些不一样的灵感。