检测夹点的最简单方法
Posted
技术标签:
【中文标题】检测夹点的最简单方法【英文标题】:Simplest way to detect a pinch 【发布时间】:2012-06-26 08:16:35 【问题描述】:这是一个WEB APP,不是原生应用。请不要使用 Objective-C NS 命令。
所以我需要在 ios 上检测“捏”事件。问题是我看到的用于执行手势或多点触控事件的每个插件或方法,(通常)都与 jQuery 一起使用,并且是阳光下每个手势的完整附加插件。我的应用程序很大,而且我对代码中的枯木非常敏感。我所需要的只是检测夹点,而使用 jGesture 之类的东西只是为了满足我的简单需求。
此外,我对如何手动检测夹点了解有限。我可以得到两个手指的位置,似乎无法正确混合以检测到这一点。有没有人有一个简单的 sn-p 只检测捏?
【问题讨论】:
我找到的最佳解决方案是在 Mozilla 的文档中。 Pinch Zoom Gesture 文章很好的描述并解决了这个问题。 【参考方案1】:想想pinch
事件是什么:两个手指放在一个元素上,彼此靠近或远离。
据我所知,手势事件是一个相当新的标准,因此最安全的方法可能是使用触摸事件,如下所示:
(ontouchstart
事件)
if (e.touches.length === 2)
scaling = true;
pinchStart(e);
(ontouchmove
事件)
if (scaling)
pinchMove(e);
(ontouchend
事件)
if (scaling)
pinchEnd(e);
scaling = false;
要获取两个手指之间的距离,请使用hypot
函数:
var dist = Math.hypot(
e.touches[0].pageX - e.touches[1].pageX,
e.touches[0].pageY - e.touches[1].pageY);
【讨论】:
为什么要编写自己的捏合检测?这是 iOS webkit 中的原生功能。这也不是一个好的实现,因为它无法区分两指滑动和捏合之间的区别。不是好建议。 @mmaclaurin 因为 webkit 并不总是具有捏合检测功能(如果我错了请纠正我),并非所有触摸屏都使用 webkit,有时不需要检测滑动事件。 OP 想要一个没有枯木库函数的简单解决方案。 OP 确实提到了 iOS,但在考虑其他平台时,这是最好的答案。除非您将平方根部分排除在距离计算之外。我把它放进去。 @BrianMortenson 这是故意的;sqrt
可能很昂贵,您通常只需要知道您的手指向内或向外移动了一定幅度。但是.. 我确实说过勾股定理,但我并没有在技术上使用它;)
@mmaclaurin 只需检查 (deltaX * deltaY
【参考方案2】:
您想使用gesturestart
, gesturechange
, and gestureend
events。只要 2 个或更多手指触摸屏幕,就会触发这些。
根据您需要对捏合手势执行的操作,需要调整您的方法。可以检查scale
乘数以确定用户的捏合手势的戏剧性程度。有关scale
属性的行为方式的详细信息,请参阅Apple's TouchEvent documentation。
node.addEventListener('gestureend', function(e)
if (e.scale < 1.0)
// User moved fingers closer together
else if (e.scale > 1.0)
// User moved fingers further apart
, false);
您还可以拦截 gesturechange
事件以检测捏合,如果您需要它以使您的应用感觉更灵敏。
【讨论】:
我知道这个问题是专门针对 iOS 的,但问题的标题是一般的“检测夹点的最简单方法”。手势启动、手势更改和手势结束事件是 iOS 特定的,不能跨平台工作。它们不会在 android 或任何其他触摸浏览器上触发。要执行此跨平台操作,请使用 touchstart、touchmove 和 touchend 事件,如此答案***.com/a/11183333/375690。 @phil 如果您正在寻找支持所有移动浏览器的最简单方法,最好使用hammer.js 我使用了 jQuery$(selector).on('gestureend',...)
,但必须使用 e.originalEvent.scale
而不是 e.scale
。
@ChadvonNau 那是因为 jQuery 的事件对象是一个“规范化的 W3C 事件对象”。 W3C Event object 不包括 scale
属性。这是供应商特定的属性。虽然我的回答包括使用 vanilla JS 完成任务的最简单方法,但如果您已经在使用 JS 框架,那么最好使用hammer.js,因为它会为您提供更好的 API。
@superuberduper IE8/9 根本无法检测到夹点。 Touch API 直到 IE10 才添加到 IE。最初的问题专门询问了 iOS,但要跨浏览器处理这个问题,您应该使用hammer.js 框架,该框架抽象出跨浏览器的差异。【参考方案3】:
Hammer.js 一路走好!它处理“变换”(捏)。 http://eightmedia.github.com/hammer.js/
但如果你想自己实现它,我认为 Jeffrey 的回答非常可靠。
【讨论】:
在看到 Dan 的回答之前,我实际上刚刚找到了hammer.js 并实施了它。锤子很酷。 看起来很酷,但演示并不是那么顺利。放大然后尝试平移感觉真的很笨拙。 值得注意的是,Hammer 有大量未解决的错误,其中一些在撰写本文时非常严重(尤其是 Android)。只是值得考虑。 这里一样,有问题。尝试过 Hammer,最终使用了 Jeffrey 的解决方案。【参考方案4】:不幸的是,跨浏览器检测捏合手势并不像人们希望的那么简单,但 HammerJS 让它变得容易多了!
查看Pinch Zoom and Pan with HammerJS demo。此示例已在 Android、iOS 和 Windows Phone 上进行了测试。
您可以在Pinch Zoom and Pan with HammerJS找到源代码。
为方便起见,这里是源代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport"
content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<title>Pinch Zoom</title>
</head>
<body>
<div>
<div style="height:150px;background-color:#eeeeee">
Ignore this area. Space is needed to test on the iPhone simulator as pinch simulation on the
iPhone simulator requires the target to be near the middle of the screen and we only respect
touch events in the image area. This space is not needed in production.
</div>
<style>
.pinch-zoom-container
overflow: hidden;
height: 300px;
.pinch-zoom-image
width: 100%;
</style>
<script src="https://hammerjs.github.io/dist/hammer.js"></script>
<script>
var MIN_SCALE = 1; // 1=scaling when first loaded
var MAX_SCALE = 64;
// HammerJS fires "pinch" and "pan" events that are cumulative in nature and not
// deltas. Therefore, we need to store the "last" values of scale, x and y so that we can
// adjust the UI accordingly. It isn't until the "pinchend" and "panend" events are received
// that we can set the "last" values.
// Our "raw" coordinates are not scaled. This allows us to only have to modify our stored
// coordinates when the UI is updated. It also simplifies our calculations as these
// coordinates are without respect to the current scale.
var imgWidth = null;
var imgHeight = null;
var viewportWidth = null;
var viewportHeight = null;
var scale = null;
var lastScale = null;
var container = null;
var img = null;
var x = 0;
var lastX = 0;
var y = 0;
var lastY = 0;
var pinchCenter = null;
// We need to disable the following event handlers so that the browser doesn't try to
// automatically handle our image drag gestures.
var disableImgEventHandlers = function ()
var events = ['onclick', 'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover',
'onmouseup', 'ondblclick', 'onfocus', 'onblur'];
events.forEach(function (event)
img[event] = function ()
return false;
;
);
;
// Traverse the DOM to calculate the absolute position of an element
var absolutePosition = function (el)
var x = 0,
y = 0;
while (el !== null)
x += el.offsetLeft;
y += el.offsetTop;
el = el.offsetParent;
return x: x, y: y ;
;
var restrictScale = function (scale)
if (scale < MIN_SCALE)
scale = MIN_SCALE;
else if (scale > MAX_SCALE)
scale = MAX_SCALE;
return scale;
;
var restrictRawPos = function (pos, viewportDim, imgDim)
if (pos < viewportDim/scale - imgDim) // too far left/up?
pos = viewportDim/scale - imgDim;
else if (pos > 0) // too far right/down?
pos = 0;
return pos;
;
var updateLastPos = function (deltaX, deltaY)
lastX = x;
lastY = y;
;
var translate = function (deltaX, deltaY)
// We restrict to the min of the viewport width/height or current width/height as the
// current width/height may be smaller than the viewport width/height
var newX = restrictRawPos(lastX + deltaX/scale,
Math.min(viewportWidth, curWidth), imgWidth);
x = newX;
img.style.marginLeft = Math.ceil(newX*scale) + 'px';
var newY = restrictRawPos(lastY + deltaY/scale,
Math.min(viewportHeight, curHeight), imgHeight);
y = newY;
img.style.marginTop = Math.ceil(newY*scale) + 'px';
;
var zoom = function (scaleBy)
scale = restrictScale(lastScale*scaleBy);
curWidth = imgWidth*scale;
curHeight = imgHeight*scale;
img.style.width = Math.ceil(curWidth) + 'px';
img.style.height = Math.ceil(curHeight) + 'px';
// Adjust margins to make sure that we aren't out of bounds
translate(0, 0);
;
var rawCenter = function (e)
var pos = absolutePosition(container);
// We need to account for the scroll position
var scrollLeft = window.pageXOffset ? window.pageXOffset : document.body.scrollLeft;
var scrollTop = window.pageYOffset ? window.pageYOffset : document.body.scrollTop;
var zoomX = -x + (e.center.x - pos.x + scrollLeft)/scale;
var zoomY = -y + (e.center.y - pos.y + scrollTop)/scale;
return x: zoomX, y: zoomY ;
;
var updateLastScale = function ()
lastScale = scale;
;
var zoomAround = function (scaleBy, rawZoomX, rawZoomY, doNotUpdateLast)
// Zoom
zoom(scaleBy);
// New raw center of viewport
var rawCenterX = -x + Math.min(viewportWidth, curWidth)/2/scale;
var rawCenterY = -y + Math.min(viewportHeight, curHeight)/2/scale;
// Delta
var deltaX = (rawCenterX - rawZoomX)*scale;
var deltaY = (rawCenterY - rawZoomY)*scale;
// Translate back to zoom center
translate(deltaX, deltaY);
if (!doNotUpdateLast)
updateLastScale();
updateLastPos();
;
var zoomCenter = function (scaleBy)
// Center of viewport
var zoomX = -x + Math.min(viewportWidth, curWidth)/2/scale;
var zoomY = -y + Math.min(viewportHeight, curHeight)/2/scale;
zoomAround(scaleBy, zoomX, zoomY);
;
var zoomIn = function ()
zoomCenter(2);
;
var zoomOut = function ()
zoomCenter(1/2);
;
var onLoad = function ()
img = document.getElementById('pinch-zoom-image-id');
container = img.parentElement;
disableImgEventHandlers();
imgWidth = img.width;
imgHeight = img.height;
viewportWidth = img.offsetWidth;
scale = viewportWidth/imgWidth;
lastScale = scale;
viewportHeight = img.parentElement.offsetHeight;
curWidth = imgWidth*scale;
curHeight = imgHeight*scale;
var hammer = new Hammer(container,
domEvents: true
);
hammer.get('pinch').set(
enable: true
);
hammer.on('pan', function (e)
translate(e.deltaX, e.deltaY);
);
hammer.on('panend', function (e)
updateLastPos();
);
hammer.on('pinch', function (e)
// We only calculate the pinch center on the first pinch event as we want the center to
// stay consistent during the entire pinch
if (pinchCenter === null)
pinchCenter = rawCenter(e);
var offsetX = pinchCenter.x*scale - (-x*scale + Math.min(viewportWidth, curWidth)/2);
var offsetY = pinchCenter.y*scale - (-y*scale + Math.min(viewportHeight, curHeight)/2);
pinchCenterOffset = x: offsetX, y: offsetY ;
// When the user pinch zooms, she/he expects the pinch center to remain in the same
// relative location of the screen. To achieve this, the raw zoom center is calculated by
// first storing the pinch center and the scaled offset to the current center of the
// image. The new scale is then used to calculate the zoom center. This has the effect of
// actually translating the zoom center on each pinch zoom event.
var newScale = restrictScale(scale*e.scale);
var zoomX = pinchCenter.x*newScale - pinchCenterOffset.x;
var zoomY = pinchCenter.y*newScale - pinchCenterOffset.y;
var zoomCenter = x: zoomX/newScale, y: zoomY/newScale ;
zoomAround(e.scale, zoomCenter.x, zoomCenter.y, true);
);
hammer.on('pinchend', function (e)
updateLastScale();
updateLastPos();
pinchCenter = null;
);
hammer.on('doubletap', function (e)
var c = rawCenter(e);
zoomAround(2, c.x, c.y);
);
;
</script>
<button onclick="zoomIn()">Zoom In</button>
<button onclick="zoomOut()">Zoom Out</button>
<div class="pinch-zoom-container">
<img id="pinch-zoom-image-id" class="pinch-zoom-image" onload="onLoad()"
src="https://hammerjs.github.io/assets/img/pano-1.jpg">
</div>
</div>
</body>
</html>
【讨论】:
【参考方案5】:检测任何元素上的两个手指捏缩放,使用 Hammer.js 等 3rd 方库轻松且无麻烦(注意,hammer 有滚动问题!)
function onScale(el, callback)
let hypo = undefined;
el.addEventListener('touchmove', function(event)
if (event.targetTouches.length === 2)
let hypo1 = Math.hypot((event.targetTouches[0].pageX - event.targetTouches[1].pageX),
(event.targetTouches[0].pageY - event.targetTouches[1].pageY));
if (hypo === undefined)
hypo = hypo1;
callback(hypo1/hypo);
, false);
el.addEventListener('touchend', function(event)
hypo = undefined;
, false);
【讨论】:
似乎使用event.touches
比使用event.targetTouches
更好。【参考方案6】:
这些答案都没有达到我想要的,所以我最终自己写了一些东西。我想使用我的 MacBookPro 触控板在我的网站上捏缩放图像。以下代码(需要 jQuery)似乎至少可以在 Chrome 和 Edge 中运行。也许这对其他人有用。
function setupImageEnlargement(el)
// "el" represents the image element, such as the results of document.getElementByd('image-id')
var img = $(el);
$(window, 'html', 'body').bind('scroll touchmove mousewheel', function(e)
//TODO: need to limit this to when the mouse is over the image in question
//TODO: behavior not the same in Safari and FF, but seems to work in Edge and Chrome
if (typeof e.originalEvent != 'undefined' && e.originalEvent != null
&& e.originalEvent.wheelDelta != 'undefined' && e.originalEvent.wheelDelta != null)
e.preventDefault();
e.stopPropagation();
console.log(e);
if (e.originalEvent.wheelDelta > 0)
// zooming
var newW = 1.1 * parseFloat(img.width());
var newH = 1.1 * parseFloat(img.height());
if (newW < el.naturalWidth && newH < el.naturalHeight)
// Go ahead and zoom the image
//console.log('zooming the image');
img.css(
"width": newW + 'px',
"height": newH + 'px',
"max-width": newW + 'px',
"max-height": newH + 'px'
);
else
// Make image as big as it gets
//console.log('making it as big as it gets');
img.css(
"width": el.naturalWidth + 'px',
"height": el.naturalHeight + 'px',
"max-width": el.naturalWidth + 'px',
"max-height": el.naturalHeight + 'px'
);
else if (e.originalEvent.wheelDelta < 0)
// shrinking
var newW = 0.9 * parseFloat(img.width());
var newH = 0.9 * parseFloat(img.height());
//TODO: I had added these data-attributes to the image onload.
// They represent the original width and height of the image on the screen.
// If your image is normally 100% width, you may need to change these values on resize.
var origW = parseFloat(img.attr('data-startwidth'));
var origH = parseFloat(img.attr('data-startheight'));
if (newW > origW && newH > origH)
// Go ahead and shrink the image
//console.log('shrinking the image');
img.css(
"width": newW + 'px',
"height": newH + 'px',
"max-width": newW + 'px',
"max-height": newH + 'px'
);
else
// Make image as small as it gets
//console.log('making it as small as it gets');
// This restores the image to its original size. You may want
//to do this differently, like by removing the css instead of defining it.
img.css(
"width": origW + 'px',
"height": origH + 'px',
"max-width": origW + 'px',
"max-height": origH + 'px'
);
);
【讨论】:
【参考方案7】:我的回答受到 Jeffrey 的回答的启发。在该答案给出了更抽象的解决方案的地方,我尝试提供有关如何实现它的更具体的步骤。这只是一个指南,可以更优雅地实现。有关更详细的示例,请查看 MDN 网络文档的 tutorial。
HTML:
<div id="zoom_here">....</div>
JS
<script>
var dist1=0;
function start(ev)
if (ev.targetTouches.length == 2) //check if two fingers touched screen
dist1 = Math.hypot( //get rough estimate of distance between two fingers
ev.touches[0].pageX - ev.touches[1].pageX,
ev.touches[0].pageY - ev.touches[1].pageY);
function move(ev)
if (ev.targetTouches.length == 2 && ev.changedTouches.length == 2)
// Check if the two target touches are the same ones that started
var dist2 = Math.hypot(//get rough estimate of new distance between fingers
ev.touches[0].pageX - ev.touches[1].pageX,
ev.touches[0].pageY - ev.touches[1].pageY);
//alert(dist);
if(dist1>dist2) //if fingers are closer now than when they first touched screen, they are pinching
alert('zoom out');
if(dist1<dist2) //if fingers are further apart than when they first touched the screen, they are making the zoomin gesture
alert('zoom in');
document.getElementById ('zoom_here').addEventListener ('touchstart', start, false);
document.getElementById('zoom_here').addEventListener('touchmove', move, false);
</script>
【讨论】:
添加 Zoom In 和 Zoom Out 逻辑也会很有帮助。【参考方案8】:最简单的方法是响应 'wheel' 事件。
您需要调用ev.preventDefault()
以防止浏览器进行全屏缩放。
浏览器会为触控板上的捏合合成“滚轮”事件,作为奖励,您还可以处理鼠标滚轮事件。这是地图应用程序处理它的方式。
更多细节在我的例子中:
let element = document.getElementById('el');
let scale = 1.0;
element.addEventListener('wheel', (ev) =>
// This is crucial. Without it, the browser will do a full page zoom
ev.preventDefault();
// This is an empirically determined heuristic.
// Unfortunately I don't know of any way to do this better.
// Typical deltaY values from a trackpad pinch are under 1.0
// Typical deltaY values from a mouse wheel are more than 100.
let isPinch = Math.abs(ev.deltaY) < 50;
if (isPinch)
// This is a pinch on a trackpad
let factor = 1 - 0.01 * ev.deltaY;
scale *= factor;
element.innerText = `Pinch: scale is $scale`;
else
// This is a mouse wheel
let strength = 1.4;
let factor = ev.deltaY < 0 ? strength : 1.0 / strength;
scale *= factor;
element.innerText = `Mouse: scale is $scale`;
);
<div id='el' style='width:400px; height:300px; background:#ffa'>
Scale: 1.0
</div>
【讨论】:
感谢 Ed_ 的 abs()以上是关于检测夹点的最简单方法的主要内容,如果未能解决你的问题,请参考以下文章