一、前言
這幾個月事情比較多,寫了一些博客都沒有來得及整理發布,今天剛好有一位同事在開發前端頁面的時候用到了手勢判斷。所以翻出了之前寫的 demo,順便整理一下作為記錄。
手勢判斷在各種應用中都十分常見,如 APP 中的手勢翻頁,前進后退等等,如微博做得就特別好,微信的話就不想吐槽了。不扯太遠,H5 開發中手勢判斷一般多用於一些交互比較靈活的場景,例如大轉盤抽獎游戲,旋轉菜單,酷跑,打磚塊游戲等等。今天不具體到這些小游戲的開發,我們重點講講實現的原理。其實比較基礎,大神請自動忽略。
二、實現原理
前提事件,所謂手勢,就是你的手對於屏幕觸摸的方向或者說軌跡。其實移動端只不過是用戶的手指代替了 PC 端的鼠標。所以如果你做過 PC 端頁面的鼠標軌跡判斷,那移動端的手勢判斷思路也就可以秒殺了。
上面我們提到,要對用戶手指對屏幕的操作進行捕捉,在 H5 中提供了這樣幾個關鍵的事件監聽 touchstart、touchmove、touchend。因為比較基礎我下面就簡單帶過了。
首先 touchstart 顧名思義 “觸摸-開始”,也就是當手指觸摸到屏幕的時候將觸發這個事件。其次 touchmove “觸摸-移動”。也就是當你的手指在屏幕上划動的時候該事件將不斷被觸發。最后 touchend “觸摸-結束” 當手指離開屏幕的那一瞬間觸發。
手指事件已經解決了,接下來就是想辦法判斷用戶的手指是怎樣划動的了。這里就需要引入一個十分基礎的概念坐標系。我們數學上的平面直角坐標系是這樣

而我們屏幕上的坐標系是這樣的:

注意 Y 軸的增長方向和平面直角坐標系是相反的。
到這里基本上思路就已經確定了,當手指觸摸到屏幕的一瞬間也就是 touchstart 時記錄手指觸摸的位置(xstart,ystart),當手指離開屏幕時也就是 touchend 時獲取手指離開屏幕的位置(xend,yend),然后進行判斷,如果 xend - xstart > 0 那么水平方向是向右,反之向左。如果 yend - ystart > 0 那么垂直方向是向下,反之向上。當然機器是十分精細的,就算移動一個像素上述判斷原則也將成立,人類是沒有機器那么敏感的,所以這里順便提一個東西,也就是靈敏度的問題,一般來說我們是不希望移動一個像素也算是觸發了某個手勢的,下面講到代碼時會有所體現。
手勢情況圖解:總共有八個方向

三、代碼實現
由於這次的項目已經使用了 zepto.js 這個框架,所以順手在此基礎上給它添加了自己的手勢拓展方法。這里我就不再累述給 zepto.js 添加拓展方法的知識點了。還不了解的朋友請先 Google(說到 Google 下篇文章記錄一下如何搭建自己的服務器利用 ss 科學上網吧)。我們先上完整代碼,在進行核心代碼分析
完整代碼如下:
$.fn.gesture = function(callback,sensibility){ if (!sensibility || isNaN(sensibility) || sensibility <= 0) sensibility = 130; var start_piont,end_point,delta_x,delta_y,distances; var direction = { horizontal : null, vertical : null } // var cond = 130;//靈敏度控制 this.bind('touchstart',function(e){ var touch = e.touches[0]; start_piont = { x: touch.pageX, y: touch.pageY } }).bind('touchmove',function(e){ /* var touch = e.touches[0]; end_point = { x: touch.pageX, y: touch.pageY }*/ }).bind('touchend', function(e){ var touch = e.changedTouches[0]; end_point = { x: touch.pageX, y: touch.pageY } delta_x = end_point.x - start_piont.x; delta_y = end_point.y - start_piont.y; if(delta_x < -sensibility) { // 向左划動 direction.horizontal='left'; } else if (delta_x > sensibility) { // 向右划動 direction.horizontal='right'; } if(delta_y > sensibility){ direction.vertical = 'down'; }else if(delta_y < (-sensibility)){ direction.vertical = 'up'; } if(typeof callback === 'function'){ callback(direction); } //還原狀態 direction = { horizontal : null, vertical : null } });
注意:基於 zepto.js 的拓展方法,所以注意一下依賴。記得先引入 zepto.js
分析:
首先看下下面這句代碼
$.fn.gesture = function(callback,sensibility = 130){...} 
        sensibility = 130 這個是 js 的默認參數值,這個在我之前的文章《開源原生JavaScript插件-CJPCD(省市區聯動)》文章中有提到過,后面有點紕漏是 IOS 設備並不兼容,所以這里還是采用了保險的做法:
if (!sensibility || isNaN(sensibility) || sensibility <= 0){ sensibility = 130; }
sensibility 就是上面提到的靈敏度的問題,筆者默認設置為 130 沒為什么。除此之外代碼中還出現了另外幾個關鍵的變量。
- start_piont: 手指開始觸摸的點(x,y)
 - end_piont: 手指j結束觸摸的點(x,y)
 - delta_x: X 軸的變量增量
 - delta_y: Y 軸的變量增量
 - direction: 方向對象手勢判斷結果的返回值,包括兩個成員 
          
- horizontal : 水平方向,取值為 
null、left或right - vertical : 垂直方向,取值為 
null、up或者down 
 - horizontal : 水平方向,取值為 
 
代碼值得注意的是:touchstart 和 touchmove 都可以通過 e.touches[0] 來獲取當前的手指坐標。但是 touchend 中的 touches (本質是 TouchList) 的長度為零 也就是說你無法通過這個來獲取到手指離開時那個點的坐標 如下圖

那這樣需求無法做了?答案是否定的,我們先來看方案一
既然這樣我們來 touchmove 事件中做處理,在我們手指在屏幕上划動的時候會不斷調用該方法,那么我們可以在該方法中不斷得給 end_point 刷新它的坐標值(x,y)那么當我們的手指離開屏幕,也就是不再觸發 touchmove 事件,但觸發 touchend 事件,此時在 touchend 事件中取得 end_point 的坐標即得到了手指離開屏幕的坐標。按此思路下 代碼應該修改如下:
... this.bind('touchstart',function(e){ var touch = e.touches[0]; start_piont = { x: touch.pageX, y: touch.pageY } }).bind('touchmove',function(e){ //在這里不斷刷新 var touch = e.touches[0]; end_point = { x: touch.pageX, y: touch.pageY } }).bind('touchend', function(e){ //在這里取值計算 delta_x = end_point.x - start_piont.x; delta_y = end_point.y - start_piont.y; if(delta_x < -sensibility) { // 向左划動 direction.horizontal='left'; } else if (delta_x > sensibility) { // 向右划動 direction.horizontal='right'; } if(delta_y > sensibility){ direction.vertical = 'down'; }else if(delta_y < (-sensibility)){ direction.vertical = 'up'; } if(typeof callback === 'function'){ callback(direction); } //還原狀態 direction = { horizontal : null, vertical : null } });
這樣做確實能解決上面我們提到的問題。但是從性能上來講在手指划動的時候不斷的賦值或者計算絕對不是實現這個需求的最佳方案,雖然在這里並不會有明顯的性能問題,但是原則上我們還是不要這樣做。所以我們采用下面的方案。 在引出下面方案之前我們先來介紹一下 changedTouches--‘觸發事件時改變的觸摸點的集合’,划動的點自然符合這個條件,也就是說我們可以通過它來獲取到手指離開屏幕時的那個點。不過這里要聲明一下,這篇文章不涉及多點觸摸的實現,所以我們以單點(一根手指)的觸摸為前提繼續下面的代碼改造:
this.bind('touchstart',function(e){ var touch = e.touches[0]; start_piont = { x: touch.pageX, y: touch.pageY } }).bind('touchmove',function(e){ // 請注意這里代碼清空了 }).bind('touchend', function(e){ //通過 changedTouches 獲取手指離開屏幕時的坐標 var touch = e.changedTouches[0]; end_point = { x: touch.pageX, y: touch.pageY } delta_x = end_point.x - start_piont.x; delta_y = end_point.y - start_piont.y; if(delta_x < -sensibility) { // 向左划動 direction.horizontal='left'; } else if (delta_x > sensibility) { // 向右划動 direction.horizontal='right'; } if(delta_y > sensibility){ direction.vertical = 'down'; }else if(delta_y < (-sensibility)){ direction.vertical = 'up'; } if(typeof callback === 'function'){ callback(direction); } //還原狀態 direction = { horizontal : null, vertical : null } });
到這里我們的組件也算基本編寫完成了。調用方式如下
<!--引入依賴--> <script src="http://cdn.bootcss.com/zepto/1.2.0/zepto.min.js"></script> <!--引入組件--> <script src="../plugins/cj-zepto-gesture.min.js"></script>
調用方法:
//使用案例,90 為靈敏度 $('.test-container').gesture(function(direction){ //回調函數處理 console.log('拓展方法',direction); },90);
test-container 為手勢操作容器,如下面的藍色 div、

限於筆者技術,文章觀點難免有不當之處,希望發現問題的朋友幫忙指正,筆者將會及時更新。也請轉載的朋友注明文章出處並附上原文鏈接,以便讀者能及時獲取到文章更新后的內容,以免誤導讀者。筆者力求避免寫些晦澀難懂的文章(雖然也有人說這樣顯得高逼格,專業),盡量使用簡單的用詞和例子來幫助理解。如果表達上有好的建議的話也希望朋友們在評論處指出。
本文為作者原創,轉載請注明出處! Cboyce
