使用 Hammer.js 捏缩放
Posted
技术标签:
【中文标题】使用 Hammer.js 捏缩放【英文标题】:Pinch to zoom using Hammer.js 【发布时间】:2013-08-03 09:10:13 【问题描述】:我正在尝试使用hammer.js 实现捏缩放
这是我的 html-
<script src="//cdnjs.cloudflare.com/ajax/libs/hammer.js/1.0.5/hammer.min.js"></script>
<div id="pinchzoom">
<div>
<img id="rect" src="http://blog.gettyimages.com/wp-content/uploads/2013/01/Siberian-Tiger-Running-Through-Snow-Tom-Brakefield-Getty-Images-200353826-001.jpg" ondragstart="return false" />
</div>
</div>
这是我的脚本
var hammertime = Hammer(document.getElementById('pinchzoom'),
transform_always_block: true,
transform_min_scale: 1,
drag_block_horizontal: false,
drag_block_vertical: false,
drag_min_distance: 0
);
var rect = document.getElementById('rect');
var posX=0, posY=0,
scale=1, last_scale,
rotation= 1, last_rotation;
hammertime.on('touch drag transform', function(ev)
switch(ev.type)
case 'touch':
last_scale = scale;
last_rotation = rotation;
break;
case 'drag':
posX = ev.gesture.deltaX;
posY = ev.gesture.deltaY;
break;
case 'transform':
rotation = last_rotation + ev.gesture.rotation;
scale = Math.max(1, Math.min(last_scale * ev.gesture.scale, 10));
break;
// transform!
var transform =
//"translate3d("+posX+"px,"+posY+"px, 0) " +
"scale3d("+scale+","+scale+", 0) ";
rect.style.transform = transform;
rect.style.oTransform = transform;
rect.style.msTransform = transform;
rect.style.mozTransform = transform;
rect.style.webkitTransform = transform;
);
它工作正常,但我无法滚动图像。在取消注释 transform3d 时,它可以工作,但图像在拖动时会失去其位置。我不能使用 jQuery。
【问题讨论】:
【参考方案1】:对于hammer.js 2.0+,我采用了@DGS 答案并对其进行了更改以适应我对cordova 和intel-xdk 所做的事情,所以它是纯JS 和hammer.js 2.0 对于webkit。
我的实现允许您同时缩放和拖动,而不是像上面那样相互独立,并提供更原生的感觉。它还实现了双击放大(和缩小)。我将它设置为在.999
和4
之间缩放,但您可以随意更改这些值。因此,如果您只是复制并粘贴它,它可能会按照您的预期进行(webkit)。
感谢八媒体和@DGS 让我开始!随意改进它。
function hammerIt(elm)
hammertime = new Hammer(elm, );
hammertime.get('pinch').set(
enable: true
);
var posX = 0,
posY = 0,
scale = 1,
last_scale = 1,
last_posX = 0,
last_posY = 0,
max_pos_x = 0,
max_pos_y = 0,
transform = "",
el = elm;
hammertime.on('doubletap pan pinch panend pinchend', function(ev)
if (ev.type == "doubletap")
transform =
"translate3d(0, 0, 0) " +
"scale3d(2, 2, 1) ";
scale = 2;
last_scale = 2;
try
if (window.getComputedStyle(el, null).getPropertyValue('-webkit-transform').toString() != "matrix(1, 0, 0, 1, 0, 0)")
transform =
"translate3d(0, 0, 0) " +
"scale3d(1, 1, 1) ";
scale = 1;
last_scale = 1;
catch (err)
el.style.webkitTransform = transform;
transform = "";
//pan
if (scale != 1)
posX = last_posX + ev.deltaX;
posY = last_posY + ev.deltaY;
max_pos_x = Math.ceil((scale - 1) * el.clientWidth / 2);
max_pos_y = Math.ceil((scale - 1) * el.clientHeight / 2);
if (posX > max_pos_x)
posX = max_pos_x;
if (posX < -max_pos_x)
posX = -max_pos_x;
if (posY > max_pos_y)
posY = max_pos_y;
if (posY < -max_pos_y)
posY = -max_pos_y;
//pinch
if (ev.type == "pinch")
scale = Math.max(.999, Math.min(last_scale * (ev.scale), 4));
if(ev.type == "pinchend")last_scale = scale;
//panend
if(ev.type == "panend")
last_posX = posX < max_pos_x ? posX : max_pos_x;
last_posY = posY < max_pos_y ? posY : max_pos_y;
if (scale != 1)
transform =
"translate3d(" + posX + "px," + posY + "px, 0) " +
"scale3d(" + scale + ", " + scale + ", 1)";
if (transform)
el.style.webkitTransform = transform;
);
要实现只需用hammerIt(document.getElementById("imagid"));元素加载后。您可以在任意数量的元素上调用它。
干杯!
【讨论】:
这很好,但我对“规模!= 1”的测试有点困惑。除非我开始捏合,否则我的比例始终为 1,这意味着这些手势会被跳过。 @JasonFarnsworth 您无法平移/拖动尚未放大的图像,因此除非缩放 > 1,否则运行该代码是没有意义的。 啊,明白了。我使用了不同的方法——我让用户平移,然后如果他们超出范围,我会转换回合理的状态。 谢谢,缩放工作正常 :),但平移无法正常工作 :( 为什么使用 0.999 作为最大值而不是 1?使用0.999,经过一次放大和总缩小后,比例达到0.999,此时应为1,作为原始值。【参考方案2】:添加变量last_posX
和last_posY
以说明翻译后的位置变化。在 dragend 上更新它们,以便下次捕获拖动事件时会考虑上次拖动结束的位置
var posX=0, posY=0,
scale=1, last_scale,
last_posX=0, last_posY=0,
max_pos_x=0, max_pos_y=0;
hammertime.on('touch drag transform dragend', function(ev)
switch(ev.type)
case 'touch':
last_scale = scale;
break;
case 'drag':
if(scale != 1)
posX = last_posX + ev.gesture.deltaX;
posY = last_posY + ev.gesture.deltaY;
if(posX > max_pos_x)
posX = max_pos_x;
if(posX < -max_pos_x)
posX = -max_pos_x;
if(posY > max_pos_y)
posY = max_pos_y;
if(posY < -max_pos_y)
posY = -max_pos_y;
else
posX = 0;
posY = 0;
saved_posX = 0;
saved_posY = 0;
break;
case 'transform':
scale = Math.max(1, Math.min(last_scale * ev.gesture.scale, 10));
max_pos_x = Math.ceil((scale - 1) * rect.clientWidth / 2);
max_pos_y = Math.ceil((scale - 1) * rect.clientHeight / 2);
if(posX > max_pos_x)
posX = max_pos_x;
if(posX < -max_pos_x)
posX = -max_pos_x;
if(posY > max_pos_y)
posY = max_pos_y;
if(posY < -max_pos_y)
posY = -max_pos_y;
break;
case 'dragend':
last_posX = posX < max_pos_x ? posX: max_pos_x;
last_posY = posY < max_pos_y ? posY: max_pos_y;
break;
// transform!
var transform =
"translate3d(0, 0, 0) " +
"scale3d(1, 1, 0) ";
if(scale != 1)
transform =
"translate3d("+posX+"px,"+posY+"px, 0) " +
"scale3d("+scale+","+scale+", 0) ";
rect.style.transform = transform;
rect.style.oTransform = transform;
rect.style.msTransform = transform;
rect.style.mozTransform = transform;
rect.style.webkitTransform = transform;
);
【讨论】:
@DGS- 我想保留原始图像大小,即当比例为 1 或更少时,图像应该在收缩时达到原始大小和位置。 @Nickalchemist 更新了答案。现在如果比例为 1 或更小,css 转换和比例将重置为默认值。 @DGS- 位置仍未固定。我仍然能够以 0 比例拖动图像。 @DGS- 当我缩放和拖动时,我仍然能够看到空白。这不是典型的缩放缩放工作方式。不应有空格。 所以你不想滚动超出图像的限制就是你说的?【参考方案3】:我对解决方案并不满意,所以我也创建了一个。
你可以在https://bl.ocks.org/stephanbogner/06c3e0d3a1c8fcca61b5345e1af80798玩它
代码更复杂,但它的行为类似于 iOS(在 iPad 和 iPhone 上测试):
双击图像,它会放大到该确切位置 捏合时缩放中心在您的手指之间,您可以在捏合时拖动<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HammerJS Pinch Zoom and Drag</title>
<style type="text/css">
body
margin: 0;
padding: 0;
#dragWrapper
margin: 40px;
background: whitesmoke;
width: calc(100vw - 80px);
height: calc(100vh - 80px);
position: relative;
#drag
touch-action: auto;
height: 300px;
width: 200px;
position: absolute;
left: 0;
top: 0;
background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Bergelut_dengan_asap_nan_beracun.jpg/318px-Bergelut_dengan_asap_nan_beracun.jpg');
background-size: cover;
</style>
</head>
<body>
<div id="dragWrapper">
<div id="drag"></div>
</div>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
<script type="text/javascript">
var element = document.getElementById('drag')
var hammertime = new Hammer(element, );
hammertime.get('pinch').set( enable: true );
hammertime.get('pan').set( threshold: 0 );
var fixHammerjsDeltaIssue = undefined;
var pinchStart = x: undefined, y: undefined
var lastEvent = undefined;
var originalSize =
width: 200,
height: 300
var current =
x: 0,
y: 0,
z: 1,
zooming: false,
width: originalSize.width * 1,
height: originalSize.height * 1,
var last =
x: current.x,
y: current.y,
z: current.z
function getRelativePosition(element, point, originalSize, scale)
var domCoords = getCoords(element);
var elementX = point.x - domCoords.x;
var elementY = point.y - domCoords.y;
var relativeX = elementX / (originalSize.width * scale / 2) - 1;
var relativeY = elementY / (originalSize.height * scale / 2) - 1;
return x: relativeX, y: relativeY
function getCoords(elem) // crossbrowser version
var box = elem.getBoundingClientRect();
var body = document.body;
var docEl = document.documentElement;
var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
var clientTop = docEl.clientTop || body.clientTop || 0;
var clientLeft = docEl.clientLeft || body.clientLeft || 0;
var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;
return x: Math.round(left), y: Math.round(top) ;
function scaleFrom(zoomOrigin, currentScale, newScale)
var currentShift = getCoordinateShiftDueToScale(originalSize, currentScale);
var newShift = getCoordinateShiftDueToScale(originalSize, newScale)
var zoomDistance = newScale - currentScale
var shift =
x: currentShift.x - newShift.x,
y: currentShift.y - newShift.y,
var output =
x: zoomOrigin.x * shift.x,
y: zoomOrigin.y * shift.y,
z: zoomDistance
return output
function getCoordinateShiftDueToScale(size, scale)
var newWidth = scale * size.width;
var newHeight = scale * size.height;
var dx = (newWidth - size.width) / 2
var dy = (newHeight - size.height) / 2
return
x: dx,
y: dy
hammertime.on('doubletap', function(e)
var scaleFactor = 1;
if (current.zooming === false)
current.zooming = true;
else
current.zooming = false;
scaleFactor = -scaleFactor;
element.style.transition = "0.3s";
setTimeout(function()
element.style.transition = "none";
, 300)
var zoomOrigin = getRelativePosition(element, x: e.center.x, y: e.center.y , originalSize, current.z);
var d = scaleFrom(zoomOrigin, current.z, current.z + scaleFactor)
current.x += d.x;
current.y += d.y;
current.z += d.z;
last.x = current.x;
last.y = current.y;
last.z = current.z;
update();
)
hammertime.on('pan', function(e)
if (lastEvent !== 'pan')
fixHammerjsDeltaIssue =
x: e.deltaX,
y: e.deltaY
current.x = last.x + e.deltaX - fixHammerjsDeltaIssue.x;
current.y = last.y + e.deltaY - fixHammerjsDeltaIssue.y;
lastEvent = 'pan';
update();
)
hammertime.on('pinch', function(e)
var d = scaleFrom(pinchZoomOrigin, last.z, last.z * e.scale)
current.x = d.x + last.x + e.deltaX;
current.y = d.y + last.y + e.deltaY;
current.z = d.z + last.z;
lastEvent = 'pinch';
update();
)
var pinchZoomOrigin = undefined;
hammertime.on('pinchstart', function(e)
pinchStart.x = e.center.x;
pinchStart.y = e.center.y;
pinchZoomOrigin = getRelativePosition(element, x: pinchStart.x, y: pinchStart.y , originalSize, current.z);
lastEvent = 'pinchstart';
)
hammertime.on('panend', function(e)
last.x = current.x;
last.y = current.y;
lastEvent = 'panend';
)
hammertime.on('pinchend', function(e)
last.x = current.x;
last.y = current.y;
last.z = current.z;
lastEvent = 'pinchend';
)
function update()
current.height = originalSize.height * current.z;
current.width = originalSize.width * current.z;
element.style.transform = "translate3d(" + current.x + "px, " + current.y + "px, 0) scale(" + current.z + ")";
</script>
</body>
</html>
【讨论】:
【参考方案4】:查看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>
(顺便说一句,上面有很多很好的信息,但我无法让任何示例真正起作用,并且我想实现一些微妙的细节,例如考虑缩放中心)。
【讨论】:
onLoad() 函数有一个小错误。 “viewportWidth = img.offsetWidth;”应该是 viewportWidth = img.parentElement offsetWidth;"【参考方案5】:这是一个使用 paper.js (codepen) 的相对紧凑的解决方案:
function initPinchZoom()
const canvasElement = paper.view.element;
const box = canvasElement.getBoundingClientRect();
const offset = new paper.Point(box.left, box.top);
const hammer = new Hammer(canvasElement, );
hammer.get('pinch').set( enable: true );
let startMatrix, startMatrixInverted, p0ProjectCoords;
hammer.on('pinchstart', e =>
startMatrix = paper.view.matrix.clone();
startMatrixInverted = startMatrix.inverted();
const p0 = getCenterPoint(e);
p0ProjectCoords = paper.view.viewToProject(p0);
);
hammer.on('pinch', e =>
// Translate and scale view using pinch event's 'center' and 'scale' properties.
// Translation computes center's distance from initial center (considering current scale).
const p = getCenterPoint(e);
const pProject0 = p.transform(startMatrixInverted);
const delta = pProject0.subtract(p0ProjectCoords).divide(e.scale);
paper.view.matrix = startMatrix.clone().scale(e.scale, p0ProjectCoords).translate(delta);
);
function getCenterPoint(e)
return new paper.Point(e.center.x, e.center.y).subtract(offset);
【讨论】:
以上是关于使用 Hammer.js 捏缩放的主要内容,如果未能解决你的问题,请参考以下文章
使用 Android 内置的手势监听器和缩放监听器实现捏缩放和拖动