内容简介:首先,需要了解 CSS3 的实际上,上面的写法可以算作 CSS 提供的语法糖。了解计算机图形学的同学可能知道,计算机完成图像变换实际上使用的实现是矩阵。
首先,需要了解 CSS3 的 transform ,用 transform 进行元素的变换,这是实现的关键。
transform 最常用的形式像这样:
// 放大 2 倍 transform: scale(2); // 向左平移 100px transform: translate(100px); // rotate,skew,perspective 等其他变换 复制代码
实际上,上面的写法可以算作 CSS 提供的语法糖。了解计算机图形学的同学可能知道,计算机完成图像变换实际上使用的实现是矩阵。
如果使用以下 JavaScript 代码更改并查询一个 div 元素的 CSS transform 属性:
document.querySelector('div').style.transform = 'scale(1)';
console.log(window.getComputedStyle(document.querySelector('div'), null).getPropertyValue('transform'));
// 输出 "matrix(1, 0, 0, 1, 0, 0)"
复制代码
可以看到此时 transform 的值并不是“scale(1)”,而是一个矩阵表示。此处 matrix 中的 6 个参数,对应了 2D 仿射变换矩阵中起作用的 6 个值(完整的是 3*3 矩阵,但是有 3 个参数是固定的)——不过这跟本文的实现没有太大关系。为了简单起见,只需知道在 matrix 用到的参数即可。
但这绝对不是 matrix 完整的正确用法
如果想要多了解一些关于变换矩阵的知识,请搜索“仿射变换”。
知乎上有一个很好的入门回答: 如何通俗地讲解「仿射变换」这个概念? - 马同学的回答 。
如果不愿意写矩阵形式,也可以将其等价地写成:
transform: translate(200px, 100px) scale(3); 复制代码
注意, 书写顺序决定了变换顺序,不可以将 scale 放置在 translate 之前 : Is a css transform matrix equivalent to a transform scale, skew, translate 。
Touch 事件
在进行实现之前,需要先了解一点触摸事件的处理。详见触摸事件。
这里简单介绍一下相关的事件:
touchstart :触摸事件开始,表示一个触摸点开始接触。可以通过传入对象获取 touches ,即一 个 TouchList 对象,里面含有当前所有的接触点,即touch 对象。下面 2 个事件传入参数相同。
touchmove :触摸点移动。
touchend :触摸事件结束,表示一个触摸点离开。
TouchList :是一种“类数组”对象,也就是和函数中拿到的 arguments 相似,不是数组,但是含有 length 属性,以及 0 、 1 这样的 key 值 ,可以通过 Array.prototype.slice 转为数组。也可以使用 touches['0'] 这样的语法直接从 touches 中取出触摸点对象。
需要了解
-
触摸事件传入的参数是组合对象,因此如果使用了 React 框架,最好不要向异步方法,如 setTimeout、Promise、async / await 区域中传递该参数。可以先使用变量获取需要使用的值,再进行传递。如果一定要传入,可以使用
e.persist()将对象持久化。 -
区别于点击事件,无法从触摸事件中直接获得 offsetX offsetY。因此需要自己计算这两个值。
拖拽的实现
网上找 DOM 元素拖拽,通常的做法是使用相对定位与 top、left 属性。但是结合缩放事件,本文将使用 transform 进行实现。
不过无论具体实现方式如何,移动元素的思想都是一致的:先计算两次 move 事件中的触摸位移,然后将这段位移应用到目标上。
HTML 部分:
<head>
<meta charset="UTF-8">
<!--一些方便实现的声明-->
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<title>Touch</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
// 禁用页面拖动刷新
overscroll-behavior: contain;
}
.board {
width: 100%;
height: 100%;
}
.board img {
width: 260px;
}
</style>
</head>
<body>
<div class="board">
<!--盗了少数派的图-->
<img src="https://cdn.sspai.com/article/86c69914-4545-bc1c-1310-2975d4fe8d6b.jpg?imageMogr2/quality/95/thumbnail/!700x233r/gravity/Center/crop/700x233" alt="">
</div>
</body>
复制代码
JavaScript 部分:
let img = document.querySelector('img');
// 查询 DOM 对象的 CSS 值
const getStyle = (target, style) => {
let styles = window.getComputedStyle(target, null);
return styles.getPropertyValue(style);
};
// 获取并解析元素当前的位移量
const getTranslate = (target) => {
let matrix = getStyle(target, 'transform');
let nums = matrix.substring(7, matrix.length - 1).split(', ');
let left = parseInt(nums[4]) || 0;
let top = parseInt(nums[5]) || 0;
return { left: left, top: top };
};
// 记录前一次触摸点的位置
let preTouchPosition = {};
const recordPreTouchPosition = (touch) => {
preTouchPosition = {
x: touch.clientX,
y: touch.clientY
};
};
// 应用样式变换
const setStyle = (key, value) => { img.style[key] = value; };
// 添加触摸移动的响应事件
img.addEventListener('touchmove', e => {
let touch = e.touches[0];
let translated = getTranslate(touch.target);
// 移动后的位置 = 当前位置 + (此刻触摸点位置 - 上一次触摸点位置)
let translateX = translated.left + (touch.clientX - preTouchPosition.x);
let translateY = translated.top + (touch.clientY - preTouchPosition.y);
let matrix = `matrix(1, 0, 0, 1, ${translateX}, ${translateY})`;
setStyle('transform', matrix);
// 完成一次移动后,要及时更新前一次触摸点的位置
recordPreTouchPosition(touch);
});
// 开始触摸时记录触摸点的位置
img.addEventListener('touchstart', e => { recordPreTouchPosition(e.touches['0']); });
复制代码
缩放的实现
初步
要进行缩放,就要知道缩放的倍数。进行缩放是双指的动作,有 2 个触摸点,而将触摸点之间的距离变化对应到缩放倍率的变化,就可以实现双指缩放的效果。要得知缩放的变化,思路跟移动一致,也是要记录上次的触摸点距离。然后就可以计算现在的缩放倍率。
let scaleRatio = 1;
// 从变量名就知道它的用途与用法
let preTouchesClientx1y1x2y2 = [];
img.addEventListener('touchmove', e => {
let touches = e.touches;
if (touches.length > 1) {
// 即便同时落下 10 个手指,我们只取前 2 个就好
let one = touches['0'];
let two = touches['1'];
const distance = (x1, y1, x2, y2) => {
let a = x1 - x2;
let b = y1 - y2;
return Math.sqrt(a * a + b * b);
};
// 新的缩放倍率 = (当前指间距离 ÷ 之前指间距离)× 之前缩放倍率
// 没有在 touchstart 中记录最初的双指位置,计算会得到 NaN,对结果直接取 1
scaleRatio = distance(one.clientX, one.clientY, two.clientX, two.clientY) / distance(...preTouchesClientx1y1x2y2) * scaleRatio || 1;
let matrix = `matrix(${scaleRatio}, 0, 0, ${scaleRatio}, ${translateX}, ${translateY})`;
setStyle('transform', matrix);
// 及时更新双指位置信息
preTouchesClientx1y1x2y2 = [one.clientX, one.clientY, two.clientX, two.clientY];
}
});
img.addEventListener('touchstart', e => {
let touches = e.touches;
// 双指同时落下也是有先后顺序的,当发现多指触摸时进行记录
if (touches.length > 1) {
let one = touches['0'];
let two = touches['1'];
preTouchesClientx1y1x2y2 = [one.clientX, one.clientY, two.clientX, two.clientY];
}
recordPreTouchPosition(touches['0']);
});
复制代码
现在已经实现了基本的缩放功能,但是好像哪里不太对……为什么感觉缩放效果不是从手指中传出的呢?似乎不管在哪里操作,都是从图片中心开始的。
transform-origin
简单介绍一个 CSS 属性: transform-origin ,详细介绍见MDN。
此属性规定元素基点,也就是是应用变换的原点。
// 元素基点设置为 (50px, 50px),是元素上的相对坐标 transform-origin: 50px 50px; 复制代码
当图形变换只有位移时,transform-origin 不会有什么影响。但是对于旋转和缩放属性来说,元素基点是重要的属性。
而默认的 transform-origin 值是 50% 50% ,也就是元素正中心。这也就是为什么每次进行缩放操作,都感觉缩放从图片中心点传来。
如果想要感受缩放效果从手指开始,就要将 transform-origin 设置在双指中间的位置;或者,通过位移的计算,模拟出 origin 的变化。本文采用前一种更直观的思路。
获取触摸的 offset
实际上,不管元素被变换成了什么形状,设置 origin 时都是采用相对元素变换前的偏移量。之前提到过 touch 事件中并没有触摸点相对于元素的 offset 值,因此需要自己来计算。
// 计算相对缩放前的偏移量,rect 为当前变换后元素的四周的位置
const relativeCoordinate = (x, y, rect) => {
let cx = (x - rect.left) / scaleRatio;
let cy = (y - rect.top) / scaleRatio;
return {
x: cx,
y: cy
};
};
复制代码
其实就是 (所选的屏幕位置 - 元素的屏幕位置) / 缩放比例 ,并不困难。rect 可以直接使用 getBoundingClientRect 函数获得。(之前误以为 getBoundingClientRect 获取的位置不正确,自己实现了一下 )
至于“所选的屏幕位置“,取双指中点的位置。这里选取 clientX 和 clientY 值计算,即距离浏览器的偏移量。
// 记录变换基点
let scaleOrigin = {};
img.addEventListener('touchmove', e => {
let touches = e.touches;
if (touches.length > 1) {
let one = touches['0'];
let two = touches['1'];
const distance = (x1, y1, x2, y2) => {
let a = x1 - x2;
let b = y1 - y2;
return Math.sqrt(a * a + b * b);
};
scaleRatio = distance(one.clientX, one.clientY, two.clientX, two.clientY) / distance(...preTouchesClientx1y1x2y2) * scaleRatio || 1;
// 移动基点
let origin = relativeCoordinate((one.clientX + two.clientX) / 2, (one.clientY + two.clientY) / 2, img.getBoundingClientRect());
scaleOrigin = origin;
setStyle('transform-origin', `${origin.x}px ${origin.y}px`);
let matrix = `matrix(${scaleRatio}, 0, 0, ${scaleRatio}, ${translateX}, ${translateY})`;
setStyle('transform', matrix);
preTouchesClientx1y1x2y2 = [one.clientX, one.clientY, two.clientX, two.clientY];
}
});
复制代码
似乎完成了?上手试一下。emmm……多操作一下就能发现,每次缩放离手后再次进行缩放,目标对象完全不受控制,甚至会瞬移。
修改 transform-origin 带来的问题
稍微思考,我们就能发现问题所在(不存在的,我 debug 好久):对于已经应用过缩放(或旋转)的元素,修改 origin 位置时,会产生位置的突然变化。
具体是怎么回事呢?其实这是一个高中数学就能够解释问题。
一点高中数学
以上是元素基点位于原点的情况。此时缩放倍率为 2,缩放前的点 A 坐标为 (3, 2),变换后 A' 为 (6, 4)。
那么,如果 origin 不在原点呢?将 origin 移动到 (1, 1) 时,情况如下:
可以看到,A' 点的坐标变为了 (5, 3)。其实现在从数值上已经可以看出一点端倪了,但是让我们来做一点抽象归纳。
首先,将基点 O 设为 。此时如果点 A 坐标为 ,缩放倍率为 s 。用向量来表示点 A 到基点的距离就是:
那么此时点 A' 到基点的距离正是 的 s 倍:
点 A' 的坐标即 的值加上点 O 的坐标:
如果我们移动基点 O,现在点 O 的坐标变为了: 。我们没有改变坐标系参考点,点 A 的坐标仍是 ,此时点 A 到基点 O 的距离为:
而新由点 A 变换得到的点 A'' 到基点 O 的距离( 的 s 倍)就变成了:
此时点的坐标, 加上点 O 的坐标:
也就是说,**将基点 O 从 移动到 ,记增量为 ,导致了点 A 的变换结果,从 ,变成了 **。计算一下 A 点因为元素基点而改变的值,也就是点 A'' 到点 A' 的距离:
可以带入上面的真实坐标值进行验证,结果是符合预期的。
消除修改 origin 位置带来的影响
这样就解释得通了,在 双指落下的一瞬间,origin 坐标变化了 ,而任一点 A 则变化了 。观察发现,这个值与点 A 自身的坐标 没有任何关系,是 origin 移动距离决定了的一个“定值”;也就是说,元素上的所有的点,同时产生了这个坐标位移效果。反映到界面上来,就是双指接触到元素的瞬间,元素立刻“瞬移”一下。而随着手指不断改变位置,origin 不断被重设,于是造成了缩放元素完全不受控制的局面。
要消除修改 origin 带来的负面影响,有 2 点需要做:
- 修改 origin 的同时修改位移,使得目标点的位移效果被抵消
- 减少 origin 的修改次数,可以减少不必要的计算量
进行修正
之前的计算中,我们得到了元素发生了 的平移,于是只需要在修改 origin 位置的同时,将位移量提前减去这个值即可。另外,我们将 origin 的修改频率从每个 touchmove 事件进行一次,减少到完整的一段缩放交互进行一次。
// 增加 originHaveSet 全局变量,每次设置 origin 位置后设为 true
img.addEventListener('touchmove', e => {
// ...
if (!originHaveSet) {
originHaveSet = true;
// 移动视线中心
let origin = relativeCoordinate((one.clientX + two.clientX) / 2, (one.clientY + two.clientY) / 2,
img.getBoundingClientRect());
// 修正视野变化带来的平移量,别忘了加上之前已有的位移值啊!
translateX = (scaleRatio - 1) * (origin.x - scaleOrigin.x) + translateX;
translateY = (scaleRatio - 1) * (origin.y - scaleOrigin.y) + translateY;
setStyle('transform-origin', `${origin.x}px ${origin.y}px`);
scaleOrigin = origin;
}
// ...
});
img.addEventListener('touchstart', e => {
let touches = e.touches;
if (touches.length > 1) {
// ... 开始缩放事件时,将标志置为 false
originHaveSet = false;
} //...
});
复制代码
这时再看一下效果,不禁流下了感动的泪水。终于能够正常缩放了,这完美的跟手效果,这顺滑的缩放体验……
稍等,缩放后拿开手指,为什么图片有时候还是会跳动啊。
一点尾巴
仔细检查一下代码,定位发现是单手 touchmove 的问题:双手缩放后移开,有时会触发单手的一个 touchmove 逻辑;而之前拖拽实现时用到的上一次接触点位置并没有及时更新,导致了计算出的图片移动距离与实际不符。
那么在 touchend 与 touchcancel 中加入相同的更新逻辑即可:
img.addEventListener('touchend', e => {
let touches = e.touches;
if (touches.length === 1) {
recordPreTouchPosition(touches['0']);
}
});
// touchcancel 一样
复制代码
最后,完整的代码可以在我的 github 上获得:html-drag-scale-demo。
以上所述就是小编给大家介绍的《一步步实现网页图片的手势拖拽与缩放》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- [译] 为什么要做特征缩放,怎么做特征缩放,什么时候做特征缩放?特征缩放三连了解一下
- TensorFlow 实现手势识别
- Flutter 手势密码控件
- golang使用nfnt缩放图片
- 特征工程之特征缩放 & 特征编码
- Flutter学习指南:交互、手势和动画
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Django 1.0 Template Development
Scott Newman / Packt / 2008 / 24.99
Django is a high-level Python web application framework designed to support the rapid development of dynamic websites, web applications, and web services. Getting the most out of its template system a......一起来看看 《Django 1.0 Template Development》 这本书的介绍吧!