1. 程式人生 > >HTML5 手勢檢測原理和實現

HTML5 手勢檢測原理和實現

前言

隨著 Hybrid 應用的豐富,HTML5 工程師們已經不滿足於把桌面端體驗簡單移植到移動端,他們覬覦移動原生應用人性化的操作體驗,特別是原生應用與生俱來的豐富的手勢系統。HTML5 沒有提供開箱即用的手勢系統,但是提供了更底層一些的對 touch 事件的監聽。基於此,我們可以做出自己的手勢庫。

手勢

常用的 HTML5 手勢可以分為兩類,單點手勢和兩點手勢。單點手勢有 tap(單擊),double tap(雙擊),long tap(長按),swipe(揮),move(移動)。兩點手勢有 pinch(縮放),rotate(旋轉)。

接下來我們實現一個檢測這些手勢的 javaScript 庫,並利用這個手勢庫做出炫酷的互動效果。
這裡寫圖片描述

移動

關於移動手勢檢測我們這裡不再贅述。總結一下就是在每次touchmove事件發生時,把兩個位移點之間的座標位置相減,就可以了。

單擊(tap)

手勢檢測的關鍵是用 touchstart,touchmove,touchend 三個事件對手勢進行分解。

那麼怎麼分解單擊事件呢?

  1. 在 touchstart 發生時進入單擊檢測,只有一個接觸點。因為單擊事件限制為一個手指的動作。
  2. 沒有發生 touchmove 事件或者 touchmove 在一個很小的範圍(如下圖)。限制 touchmove 在一個很小範圍,是為了給使用者一定的冗餘空間,因為不能保證使用者手指在接觸螢幕的時候不發生輕微的位移。

這裡寫圖片描述

3.touchend 發生在 touchstart後的很短時間內(如下圖)。這個時間段的閾值是毫秒級,用來限制手指和螢幕接觸的時間。因為單擊事件從開始到結束是很快的。
這裡寫圖片描述

有了上面的流程,就可以開始實現 tap 事件監測了。

_getTime() {

  return new Date().getTime(); 

}

_onTouchStart(e) {

    //記錄touch開始的位置

    this.startX = e.touches[0].pageX;

    this.startY = e.touches[0].pageY;

    if(e.touches.length > 1
) { //多點監測 ... }else { //記錄touch開始的時間 this.startTime = this._getTime(); } } _onTouchMove(e) { ... //記錄手指移動的位置 this.moveX = e.touches[0].pageX; this.moveY = e.touches[0].pageY; ... } _onTouchEnd(e) { let timestamp = this._getTime(); if(this.moveX !== null && Math.abs(this.moveX - this.startX) > 10 || this.moveY !== null && Math.abs(this.moveY - this.startY) > 10) { ... }else { //手指移動的位移要小於10畫素並且手指和螢幕的接觸時間要短語500毫秒 if(timestamp - this.startTime < 500) { this._emitEvent('onTap') } } }

雙擊(double tap)

和單擊一樣,雙擊事件也需要我們對手勢進行量化分解。

雙擊事件是一個手指的行為。所以在 touchstart 時,我們要判斷此時螢幕有幾個接觸點。
雙擊事件中包含兩次獨立的單擊行為。理想情況下,這兩次點選應該落在螢幕上的同一個點上。為了給使用者一定的冗餘空間,將兩次點選的座標點距離限制在10個畫素以內。
這裡寫圖片描述

雙擊事件本質是兩次快速的單擊。也即是說,兩次點選的間隔時間很短。通過一定的測試量化後,我們把兩次單擊的時間間隔設為300毫秒。
這裡寫圖片描述

注意雙擊事件中我們檢測了相鄰兩個 touchstart 事件的位移和時間間隔。

_onTouchStart(e) {

  if(e.touches.length > 1) {

  ...

  } else {

    if(this.previousTouchPoint) {

      //兩次相鄰的touchstart之間距離要小於10,同時時間間隔小於300ms

      if( Math.abs(this.startX -this.previousTouchPoint.startX) < 10  &&

          Math.abs(this.startY - this.previousTouchPoint.startY) < 10 && 

          Math.abs(this.startTime - this.previousTouchTime) < 300) {

            this._emitEvent('onDoubleTap');

          }

    }

    //儲存上一次touchstart的時間和位置資訊

    this.previousTouchTime = this.startTime;

    this.previousTouchPoint = {

        startX : this.startX,

        startY : this.startY

     };

  }

}

長按(long press)

長按應該是最容易分解的手勢。我們可以這樣分解:在 touchstart 發生後的很長一段時間內,如果沒有發生 touchmove 或者 touchend 事件,那麼就觸發長按手勢。

長按是一個手指的行為,需要檢測螢幕上是否只有一個接觸點。
如果手指在空間上發生了移動,那麼長按事件取消。
如果手指在螢幕上停留的時間超過800ms,那麼觸發長按手勢。
如果手指在螢幕上停留的時間小於800ms,也即 touchend 在 touchstart 發生後的800ms內觸發,那麼長按事件取消。

這裡寫圖片描述

_onTouchStart(e) {

  clearTimeout(this.longPressTimeout);

  if(e.touches.length > 1) {

  }else {

    this.longPressTimeout = setTimeout(()=>{

      this._emitEvent('onLongPress');

    });

  }

}

_onTouchMove(e) {

  ...

  clearTimeout(this.longPressTimeout);

  ...

}

_onTouchEnd(e) {

  ...

  clearTimeout(this.longPressTimeout);

  ...

}

縮放(pinch)

縮放是一個非常有趣的手勢,還記得第一代iPhone雙指縮放圖片給你帶來的震撼嗎?雖然如此,縮放手勢的檢測卻相對簡單。

縮放是兩個手指的行為,需要檢測螢幕上是否有兩個接觸點。
縮放比例的量化,是通過兩次縮放行為之間的距離的比值得到,如下圖。

這裡寫圖片描述

//所以縮放的核心是獲取兩個接觸點之間的直線距離。

//勾股定理

_getDistance(xLen,yLen) {
   return Math.sqrt(xLen * xLen + yLen * yLen);
  }
//這裡的xLen是兩個接觸點x座標差的絕對值,yLen相應的就是y座標差的絕對值。

_onTouchStart(e) {

  if(e.touches.length > 1) {

    let point1 = e.touches[0];

    let point2 = e.touches[1];

    let xLen = Math.abs(point2.pageX - point1.pageX);

    let yLen = Math.abs(point2.pageY - point1.pageY);

    this.touchDistance = this._getDistance(xLen, yLen);

  } else {

    ...

  }

}
//在_onTouchStart函式中獲取並且儲存 touchstart 發生時兩個接觸點之間的距離。

_onTouchMove(e) {

  if(e.touches.length > 1) {

      let xLen = Math.abs(e.touches[0].pageX - e.touches[1].pageX);

      let yLen = Math.abs(e.touches[1].pageY - e.touches[1].pageY);

      let touchDistance = this._getDistance(xLen,yLen);

      if(this.touchDistance) {

        let pinchScale = touchDistance / this.touchDistance;

          this._emitEvent('onPinch',{scale:pinchScale - this.previousPinchScale});

          this.previousPinchScale = pinchScale;

      }

  }else {

    ...

  }

}

旋轉(rotate)

旋轉手勢需要檢測兩個比較重要的值,一是旋轉的角度,二是旋轉的方向(順時針或逆時針)。

其中旋轉角度和方向的計算需要通過向量的計算來獲取,本文不再展開。
這裡寫圖片描述

//首先,需要獲取向量的旋轉方向和角度。

//這兩個方法屬於向量計算,具體原理請閱讀本文最後的參考文獻

  _getRotateDirection(vector1,vector2) {

    return vector1.x * vector2.y - vector2.x * vector1.y;

  }  

  _getRotateAngle(vector1,vector2) {

    let direction = this._getRotateDirection(vector1,vector2);

    direction = direction > 0 ? -1 : 1;

    let len1 = this._getDistance(vector1.x,vector1.y);

    let len2 = this._getDistance(vector2.x,vector2.y);

    let mr = len1 * len2;

    if(mr === 0) return 0;

    let dot = vector1.x * vector2.x + vector1.y * vector2.y;

    let r = dot / mr;

    if(r > 1) r = 1;

    if(r < -1) r = -1;

    return Math.acos(r) * direction * 180 / Math.PI;

  }
//然後,我們在手指發生移動時,呼叫獲取旋轉方向和角度的方法。

_onTouchStart(e) {

  ...  

  if(e.touches.length > 1) {

    this.touchVector = {

       x: point2.pageX - this.startX,

       y: point2.pageY - this.startY

     };

  }

  ...

}

_onTouchMove(e) {

  ...

  if(this.touchVector) {

        let vector = {

          x: e.touches[1].pageX - e.touches[0].pageX,

          y: e.touches[1].pageY - e.touches[0].pageY

        };

        let angle = this._getRotateAngle(vector,this.touchVector);

        this._emitEvent('onRotate',{

          angle

        });

        this.touchVector.x = vector.x;

        this.touchVector.y = vector.y;

      }

  ...

}

實戰

好了,我們的手勢系統到這裡就完成了。接下來要在實戰中檢驗這套系統是否可靠,做一個簡單的圖片瀏覽器,支援圖片縮放,旋轉,移動,長按。

首先,做好DOM規劃,和“之前”一樣,我們的事件監聽機制並不直接作用在圖片上,而是作用在圖片的父元素上。
這裡寫圖片描述

//然後,可以開始使用上面的手勢檢測系統了。

render() {

    return (

      <Gestures onPinch={this.onPinch} onMove={this.onMove} onRotate={this.onRotate} onDoubleTap={this.onDoubleTap} onLongPress={this.onLongPress}>

        <div className="wrapper" >

          ![](http://upload-images.jianshu.io/upload_images/2362670-f8b44d4b9101e8d6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

        </div>

      </Gestures>

    );

  }
//由於我們的手勢系統檢測的增量,因此不能直接把增量應用在物件上,而是需要把這些增量累加。以旋轉為例:

onRotate(event) {

    //對增量進行累加

    this.angle += event.angle

    this.setState({

      angle:this.angle

    });

  }

相關推薦

HTML5 手勢檢測原理實現

前言 隨著 Hybrid 應用的豐富,HTML5 工程師們已經不滿足於把桌面端體驗簡單移植到移動端,他們覬覦移動原生應用人性化的操作體驗,特別是原生應用與生俱來的豐富的手勢系統。HTML5 沒有提供開箱即用的手勢系統,但是提供了更底層一些的對 touch 事件

手勢檢測GestureDetector的實現原理

public GestureDetector(Context context, OnGestureListener listener, Handler handler) { if (handler != null) { mHa

動態替換Linux核心函數的原理實現

c函數 路徑 pla ges sta images 語句 堆棧 mit 轉載:https://www.ibm.com/developerworks/cn/linux/l-knldebug/ 動態替換Linux核心函數的原理和實現 在調試Linux核心模塊時,有時需要

Linux時間子系統之六:高精度定時器(HRTIMER)的原理實現

3.4 size 屬於 running return repr 而是 復雜度 ctu 上一篇文章,我介紹了傳統的低分辨率定時器的實現原理。而隨著內核的不斷演進,大牛們已經對這種低分辨率定時器的精度不再滿足,而且,硬件也在不斷地發展,系統中的定時器硬件的精度也越來越高,這也給

API Hook基本原理實現

use 概率 缺省 後綴 origin gif object cati mov API Hook基本原理和實現 2009-03-14 20:09 windows系統下的編程,消息message的傳遞是貫穿其始終的。這個消息我們可以簡單理解為一個有特定

jsonp的原理實現

pty 方法 www 三方庫 .get 設定 部分 nbsp blog 什麽是JSONP? javascript高級程序設計中是這樣介紹jsonp的: jsonp是JSON with padding(填充式JSON或參數式JSON )的簡寫,是應用JSON的一種新方法,在

詳解PHP文件下載的原理實現

利用 ring php代碼 按鈕 功能 span 所有 編號 變量 通常文件下載過程是十分簡單的,建立一個鏈接指向到目標文件就可以了。例如下面的鏈接: XML/HTML代碼 <a href=http://www.xxx.com/xxx.rar>點擊下載文件&

慕課網 星級評分原理實現(上)

方法 click down cti 原理 als row rep ava 源碼下載 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8">

express 如何上傳文件的原理實現

rip .net 文件 note receiving 過濾 console 執行 sage express 上傳文件的原理和實現 原理 formidable multer COS 1.原理 1.1 要想了解express上傳 我們先看看 nodejs原生上傳是怎麽實現的

CSS 0.5px 細線邊框的原理實現方式

bottom back 先決條件 device min style ati 而且 origin   細線邊框的具體實現方法有:偽元素縮放或漸變,box-shadow模擬,svg畫線,border-image裁剪等。要實現小於1px的線條,有個先決條件:屏幕的分辨率要足夠高,

[NLP] TextCNN模型原理實現

puts 窗口 ima () weight ica alt fine NPU 1. 模型原理 1.1 論文 Yoon Kim在論文(2014 EMNLP) Convolutional Neural Networks for Sentence Classification

JAVA 動態代理原理實現

ror binary lose ole jdk 動態代理 參數 try lob rac 在 Java 中動態代理和代理都很常見,幾乎是所有主流框架都用到過的知識。在面試中也是經常被提到的話題,於是便總結了本文。 Java動態代理的基本原理為:被代理對象需要實現某個接口(這是

java中註解的原理實現機制

                                          &

登入許可權驗證之token驗證的原理實現

原理 後端不在儲存認證資訊,而是在使用者登入的時候生成一個token,然後返回給前端,前端進行儲存,在需要進行驗證的時候將token一併傳送到後端,後端進行驗證 加密的方式:對稱加密和非對稱加密,對稱加密指的是加密解密使用同一個金鑰,非對稱加密使用公鑰和私鑰,加密用私鑰加密,解密用公鑰解密

登入的許可權驗證session的原理實現

儲存方式原理: 登入成功後,儲存登入資訊到檔案/資料庫種,同時儲存建立時間和過期時間,下次驗證的時候取出來做驗證 使用express-session中介軟體來進行session的操作 1.安裝express-session npm install express-sess

詳解ROI Align的基本原理實現細節

ROI Align是在Mask-RNN這篇論文裡提出的一種區域特徵聚集方式,很好地解決了ROI Pooling操作中兩次量化造成的區域不匹配(mis-alignment)的問題。實驗顯示,在檢測任務中將ROI Pooling替換為ROI Align可以提升檢測模型的準確性。 1、ROI Pool

Taglib原理實現 第六章:標籤內常用方法總結

1。支援el表示式: import org.apache.taglibs.standard.lang.support.ExpressionEvaluatorManager; private Object value = null; this.valu

Taglib原理實現 第五章:再論支援El表示式jstl標籤

1。問題:你想和jstl共同工作。比如,在用自己的標籤處理一些邏輯之後,讓jstl處理餘下的工作。 2。看這個jsp例子: .... <% String name="diego"; request.setAttribute("name",name); %> <c:out&

Taglib 原理實現:第四章 迴圈的Tag

1。問題:在request裡的 People 物件,有個屬性叫 men ,men 是一個Collection ,有許多個man 。現在,把 collection裡的man的名字都顯示出來  顯然,這是一個巢狀T

Taglib 原理實現:第三章 tag之間的巢狀屬性讀取

1。問題:在request裡有一個 Man 物件,它有兩個屬性:name和age。現在,我們想用一個巢狀的tag,父tag取得物件,子tag取得name屬性並顯示在頁面上。例如,它的形式如下:  <diego:with object="${Man}"&g