移动端图片缩放

图片缩放原理

  移动端图片缩放主要包含两个方面:1、用户手势检测;2、根据用户手势控制图片的变换(平移、缩放等)。

用户手势检测

  当用户浏览网页并用手指触摸手机屏幕时就会触发浏览器的touch事件。浏览器提供了三种touch事件用来响应用户手指触摸屏幕的状态:touchstart(手指刚接触屏幕时触发)、touchmove(手指在屏幕上移动时触发)和touchend(手指离开屏幕时触发)。利用上述三种事件可以检测用户的基本手势。

  1. 单击和双击。用户手指触摸屏幕触发touchstart事件后,在判断只有一个触摸点的情况下记录开始触摸时间和触摸点的位置信息。用户手指离开屏幕触发touchend事件后,首先判断当前时间与之前记录的开始触摸时间之差是否在设定的延时(如300ms,超过延时后为长按,不是单击)内以及当前变动的触摸点的坐标与之前记录的触摸点坐标是否在一定的误差(如±60px)内。如果上述条件满足,则当前用户手势预判为单击,如果在一定的延时(如300ms)内没有touchstart或者touchend事件触发,则判定用户此时是单击动作;如果在延时内再次触发了touchstart或者touchend事件且触摸点与上次触摸点的坐标在一定的误差(如±60px)内,则判定用户为双击动作。检测单击和双击手势的示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    // touchstart事件处理函数
    const handleTouchstart = function (evt) {
    const ctouches = evt.changedTouches;
    // 只有一个触摸点发生变化
    if (ctouches.length === 1) {
    // 更新startTouch,记录了触摸开始时变化的触摸点信息
    this.startTouch = ctouches[0];
    // 更新startTime,记录了触摸开始的时间
    this.startTime = Date.now();
    }
    };
    // touchend事件处理函数
    const handleTouchend = function (evt) {
    const ctouches = evt.changedTouches;
    if (ctouches.length === 1) {
    const now = Date.now();
    // 当前触摸点变化发生的时间与记录的上一次触摸点变化发生的时间差
    let delay = now - this.startTime;
    // 当前变化的触摸点与记录的上一次触摸开始时变化的触摸点在x轴方向的距离
    let deltaX = ctouches[0].pageX - this.startTouch.pageX;
    // 当前变化的触摸点与记录的上一次触摸开始时变化的触摸点在y轴方向的距离
    let deltaY = ctouches[0].pageY - this.startTouch.pageY;
    // 判断是否满足单击的条件
    if (delay < 300 && Math.abs(deltaX) < 60 && Math.abs(deltaY) < 60) {
    // 预判定为单击,设置延时精确判断
    clearTimeout(this.timerID);
    this.timerID = setTimeout(this.gesture = 'click', 300);
    }
    // 判断用户手势是否是双击
    // pevTouch记录了上一次触摸结束时变化的触摸点信息
    if (!this.prevTouch) {
    // 记录prevTouch
    this.prevTouch = ctouches[0];
    // 记录触摸结束时的时间
    this.endTime = now;
    } else {
    // 当前触摸结束的时间与上一次触摸结束的时间差
    delay = now - this.endTime;
    // 当前变化的触摸点与上一次触摸结束时变化的触摸点在x轴方向的距离
    deltaX = ctouches[0].pageX - this.prevTouch.pageX;
    // 当前变化的触摸点与上一次触摸结束时变化的触摸点在y轴方向的距离
    deltaY = ctouches[0].pageY - this.prevTouch.pageY;
    // 判断是否满足双击的条件
    if (delay < 300 && Math.abs(deltaX) < 60 && Math.abs(deltaY) < 60) {
    // 清除延时函数
    clearTimeout(this.timerID);
    this.gesture = 'doubleclick';
    }
    }
    }
    };
    // 绑定touchstart事件
    target.addEventListener('touchstart', handleTouchstart, false);
    // 绑定touchend事件
    target.addEventListener('touchend', handleTouchend, false);
  2. 滑动。用户手指在屏幕上滑动时会触发touchmove事件,通过tochmove事件提供的一些属性,开发者可以判断用户手指在屏幕上滑动的方向(水平(x轴)方向/垂直(y轴)方向)和距离。检测滑动的示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // touchstart事件处理函数
    const handleTouchstart = function (evt) {
    const ctouches = evt.changedTouches;
    // 只有一个触摸点发生变化
    if (ctouches.length === 1) {
    // 更新startTouch,记录了触摸开始时变化的触摸点信息
    this.startTouch = ctouches[0];
    }
    };
    // touchmove事件处理函数
    const handleTouchmove = function (evt) {
    const ctouches = evt.changedTouches;
    // 只有一个触摸点发生变化
    if (ctouches.length === 1) {
    // 触摸点在水平(x轴)方向的位移,正负代表方向,数值代表距离
    const deltaX = ctouches[0].pageX - this.startTouch.pageX;
    // 触摸点在垂直(y轴)方向的位移,正负代表方向,数值代表距离
    const deltaY = ctouches[0].pageY - this.startTouch.pageY;
    // 触摸点在屏幕上移动的直线距离
    const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
    }
    };
    // 绑定touchstart事件
    target.addEventListener('touchstart', handleTouchstart, false);
    // 绑定touchmove事件
    target.addEventListener('touchmove', handleTouchmove, false);
  3. 缩放。图片的缩放中心可以设定为两指初始接触屏幕时的中心点,通过用户两指当前在屏幕上的距离与初始距离之比可以计算出图片实时缩放比例。检测缩放的示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    // touchstart事件处理函数
    const handleTouchstart = function (evt) {
    const ctouches = evt.changedTouches;
    // 有两个触摸点发生变化
    if (ctouches.length === 2) {
    const deltaX = ctouches[0].pageX - ctouches[1].pageX;
    const deltaY = ctouches[0].pageY - ctouches[1].pageY;
    // 更新startDistance,记录了触摸开始时两个触摸点之间的直线距离
    this.startDistance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
    this.gesture = 'pinch';
    // 记录初始缩放比例
    this.initRatio = 1;
    // 缩放中心为初始两个触摸点的中心点
    this.pinchOrigin = {
    x: (ctouches[0].clientX + ctouches[1].clientX) / 2,
    y: (ctouches[0].clientY + ctouches[1].clientY) / 2
    }
    }
    };
    // touchmove事件处理函数
    const handleTouchmove = function (evt) {
    const ctouches = evt.changedTouches;
    if (this.gesture === 'pinch') {
    const deltaX = ctouches[0].pageX - ctouches[1].pageX;
    const deltaY = ctouches[0].pageY - ctouches[1].pageY;
    // 实施计算两指之间的直线距离
    const realDistance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
    // 计算实时缩放比例
    const realRatio = realDistance * this.initRatio / this.startDistance;
    }
    };
    // 绑定touchstart事件
    target.addEventListener('touchstart', handleTouchstart, false);
    // 绑定touchmove事件
    target.addEventListener('touchmove', handleTouchmove, false);

图片的线性变换

  图片的平移和缩放可以看成是在二维空间上的线性变换。线性变换的数学定义如下:

  设$U$,$V$是$K$上的线性空间,$K$为$R$(实数域)或$C$(复数域),$A$是$U$到$V$的映射,即对于任意$x \in U$,存在唯一的$z \in V$,使得$A(x)=z$。
  若$A$满足线性性质,即对于任意的$x,y \in U$及$\lambda,\mu \in K$均有

成立,则称$A$为线性空间$U$到$V$上的一个线性变换。

  线性变换通常可以用变换矩阵来表示,若$A$是$m$维实数域$R^m$到$n%$维实数域$R^n$的一个线性变换,$\vec x$是$m$维列向量且$\vec x \in R^m$,则$A$可以由一个$n \times m$维矩阵$C$表示成如下形式:

  式(1)中矩阵$C$就是线性变换$A$的变换矩阵。
  对于图片而言,其线性变换可以通过$2 \times 2$维矩阵表示。图片的缩放以矩阵的形式表示如下:

  式(2)$x$和$y$是原始坐标,$x’$和$y’$是变换后的坐标,$s_x$和$s_y$分别表示的是$x$轴和$y$轴的缩放系数。
  式(2)中的变换只是实现了图片的缩放,没有实现图片的平移,如果想要在图片进行缩放变换之后进行平移变换,需要引入仿射变换的概念。缩放变换和平移变换的复合变换可以使用仿射变换来表示:

  式(3)中$\Delta x$和$\Delta y$分别为$x$轴和$y$轴方向上的偏移。
  CSS transform属性的matrix函数可以快速实现图片的线性变换和复合变换。matrix函数接受的参数对应于仿射变换矩阵的各个元素。具体对应关系如下式所示:

  对于图片的3D变换,可以使用CSS transform属性的matrix3d函数。使用matrix3d函数时,浏览器使用GPU计算图片的变换,效果更流畅。考虑到matrix3d实现的效果更流畅,可以使用matrix3d函数实现matrix函数的功能,具体参数对应关系如下式所示:

图片缩放的完整代码

  根据图片缩放的原理,笔者编写了一个移动端图片缩放组件zoomer,并将代码集成到了自己的js武器库jslever中。关于zoomer组件的使用可以参考jslever项目仓库下的README文件。