當前位置:
首頁 > 科技 > 從移動端click到搖一搖

從移動端click到搖一搖

前言

7月末了,今日早讀文章由前端早讀課專欄作者@李銀城分享。

正文從這開始~

以前聽到前輩們說移動端盡量不要使用click,click會比較遲鈍,能用touchstart還是用touchstart。但是用touchstart會有一個問題,用戶在滑動頁面的時候要是不小心碰到了相關元素也會觸發touchstart,所以兩者都有缺點。那怎麼辦呢?

首先為什麼移動端的click會遲鈍呢?從谷歌的開發者文檔《300ms tap delay, gone away》可以找到答案:

For many years, mobile browsers applied a 300-350ms delay between touchend and click while they waited to see if this was going to be a double-tap or not, since double-tap was a gesture to zoom into text.

大意是說因為移動端要判斷是否是雙擊,所以單擊之後不能夠立刻觸發click,要等300ms,直到確認不是雙擊了才觸發click。所以就導致了click有延遲。

更為重要的是,文檔裡面還提到在2014年的Chrome 32版本已經把這個延遲去掉了,如果有一個meta 標籤:

即把viewport設置成設備的實際像素,那麼就不會有這300ms的延遲,並且這個舉動受到了IE/Firefox/Safari(IOS 9.3)的支持,也就是說現在的移動端開發可以不用顧慮click會比較遲鈍的問題。

如果設置initial-scale=1.0,在chrome上是可以生效,但是Safari 不會:

還有第三種辦法就是設置 CSS:

html{

touch-action:manipulation;

}

這樣也可以取消掉300ms的延遲,Chrome和Safari都可以生效。

click是在什麼時候觸發的呢?來研究一下click/touch事件的觸發先後順序。

1. click/touch觸發順序

用以下的html代碼來實驗:

hello,world

!function(){

vartarget=document.getElementById("target");

varbody=document.querySelector("body");

vartouchstartBeginTime=;

functionlog(event){

if(event.type==="touchstart")touchstartBeginTime=Date.now();

console.log(event.type,Date.now()-touchstartBeginTime);

}

target.onclick=log;

target.ontouchstart=log;

target.ontouchend=log;

target.ontouchmove=log;

target.onmouseover=log;

target.onmousedown=log;

target.onmouseup=log;

}();

用一台iPhone6 (IOS 10)的手機連接電腦的Safari做實驗,如下圖所示:

然後點擊灰色的target區域,用電腦的Safari進行檢查,可以看到輸出結果:

可以看到click事件是在最後觸發的,並且還看到300ms的延遲,實際的執行延遲要比這個大,因為瀏覽器的內核運行也需要消耗時間。現在加上viewport的meta標籤,再觀察結果,如下圖所示:

可以看到,300ms的延遲沒有了。

知道了click是在touchend之後觸發的,現在我們來嘗試一下實現一個tap事件。

2. tap事件的實現

雖然已經沒有太大的必要自行實現一個tap事件,但是我們還是很好奇可以怎麼實現一個能夠快速觸發的tap的事件?有兩個庫,一個是zepto,另一個是fastclick,它們都可以解決點擊延遲的問題。其中,zepto有一個自定義事件tap,它是一個沒有延遲的click事件。而fastclick是在touchend之後生成一個click事件,並立即觸發這個click,再取消原本的click事件。這兩者的原理都是一樣的,都是在touchend之後觸發,一個是觸發它自己定義的tap事件,一個是觸發原生click。

這裡有一個關鍵的問題,就是touchend之後不能夠每次都觸發tap,因為有可能用戶是在上下滑並不是在點擊,不然的話直接監聽touchstart就好了。所以怎麼判定用戶是點擊還是在上下滑呢?Zepto是用的位移偏差,即記錄下touchstart的時候的初始位移,然後用touchend的時候的位移減掉初始位移的偏差,如果這個差值在30以內,則認為用戶是點擊,大於30則認為是滑動。而fastclick是用的時間偏差,分別記錄touchstart和touchend的時間戳,如果它們的時間差大於700毫秒,則認為是滑動操作,否則是點擊操作。

Chrome又是怎麼判斷用戶是點擊還是滑動呢,筆者沒有去看安卓或者IOS Chrome的源碼,找了下Chromium的源碼,它裡面有一個resources的目錄,是Chrome自己頁面的代碼,如chrome://setting頁,它是用html寫的。在這個裡面有一個touch_handler.js,它裡面封裝了一些移動端的手勢實現如tap,tap是根據時間位移判斷是否要觸發tap,如下所示:

/**

* The time, in milliseconds, that a touch must be held to be considered

* long .

* @type

* @private

*/

TouchHandler.TIME_FOR_LONG_PRESS_=500;

定義的時間為長時間按壓long press的時間閾值為500ms,在touchstart裡面啟動一個計時器:

this.longPressTimeout_=window.setTimeout(

this.onLongPress_.bind(this),TouchHandler.TIME_FOR_LONG_PRESS_);

onLongPress_:function(){

this.disableTap_=true;

}

如果超過了閾值500ms,就把一個標誌位disableTap_設置為true,然後在touchend裡面,這個flag為true就不會觸發 tap:

if(!this.disableTap_)

this.dispatchEvent_(TouchHandler.EventType.TAP,touch);

相對於fastclick用兩個時間戳的方式,我感覺源碼的實現更為複雜,因為要啟動一個計時器。

現在我們來實現一個按位移偏差判斷的tap。

要實現一個自定義事件,有兩種方式,第一種是像jQuery/Zepto一樣,自己封裝一個事件機制,第二種是調用原生的document.createEvent,然後再執行div.dispatchEvent(event),這裡我們使用第一種。

為此先寫一個選擇器。如下代碼所示:

var$=function(selector){

vardom=null;

if(typeofselector==="string"){

dom=document.querySelectorAll(selector);

}elseif(selectorinstanceofHTMLElement){

dom=selector;

}

returnnew$Element(dom);

}

window.$=$;

選擇器的名稱用$,它是一個函數,傳進來的參數為選擇器或者dom元素,如果是字元串的選擇器,則調用querySelectorAll去獲取dom元素,如果它已經是一個dom則不用處理,最後返回一個$Element的封裝的實例,類似於jQuery對象。

現在來實現這個$Element的類,如下代碼所示:

class$Element{

constructor(_doms){

vardoms=_doms.constructor===Array||_doms.constructor===NodeList?

_doms:[_doms];

this.doms=doms;

this.init();

for(vari=;i

this[i]=doms[i];

if(!doms[i].listeners){

doms[i].listeners={};

}

}

}

}

$Element的構造函數裡面,先判斷參數的類型,如果它不是一個數組或者是用querySelectorAll返回的NodeList類型,則構造一個dom數組。然後給這些dom對象添加一個listeners的屬性,用來存放事件的回調函數。注意這裡不是一個好的實踐,因為一般不推薦給原生對象添加東西。但是從簡單考慮,這裡先用這樣的方法。

第8行代碼比較有趣,把this當作一個數組,dom元素當作這個數組的元素。這樣就可以通過索引獲取dom 元素:

varvalue=$("input")[].value;

但是它又不是一個數組,它沒有數組的sort/indexOf等函數,它是一個$Element實例,另一方面它又有length,可以通過index獲取元素,所以它是一個偽數組,這樣你就知道了arguments實例、jQuery對象這種偽數組是怎麼來的。

上面代碼還調了一個init,這個init函數用來添加tap 事件:

在說tap事件之前,需要提供事件綁定和觸發的api,如下所示:

上面的on函數會給dom的listeners屬性添加相應事件的回調,每種事件類型都用一個數組存儲。而觸發的代碼如下所示:

這段代碼也好理解,根據不同的事件類型去取回調函數的數組,依次執行。

現在重點來說一下怎麼添加一個tap事件,即上面的initTapEvent函數,如下代碼所示:

initTapEvent(dom){

varx1=,x2=,y1=,y2=;

dom.addEventListener("touchstart",function(event){

});

dom.addEventListener("touchmove",function(event){

});

dom.addEventListener("touchend",function(event){

});

}

思路是這樣的,在touchstart的時候記錄x1和y1 的位置:

dom.addEventListener("touchstart",function(event){

vartouch=event.touches[];

x1=touch.pageX;

y1=touch.pageY;

});

然後在touchmove的時候獲取到最新的移動位置:

dom.addEventListener("touchmove",function(event){

vartouch=event.touches[];

x2=touch.pageX;

y2=touch.pageY;

});

最後touchend的時候,比較位移偏差:

dom.addEventListener("touchend",function(event){

if(Math.abs(x2-x1)

$Element.dispatchEvent(dom,"tap",new$Event(x1,y1));

}

y2=x2=;

});

如果兩者的位移差小於10,則認為是tap事件,並觸發這個事件。這裡封裝了一個自定義事件:

class$Event{

constructor(pageX,pageY){

this.pageX=pageX;

this.pageY=pageY;

}

}

然後就可以使用這個tap事件了,如下代碼所示:

$("#target").on("tap",function(event){

console.log("tap",event.pageX,event.pageY);

});

接著在手機瀏覽器上運行,當點擊目標區域的時候就會執行tap回調,而上下滑動的時候則不會觸發,如下圖所示:

再比較一下tap和原生click的觸發時間的差別,需要給自定義事件添加一個click:

dom.addEventListener("click",function(event){

$Element.dispatchEvent(dom,"click",new$Event(event.pageX,event.pageY));

});

接著用一個tapTime記錄下時間:

vartapTime=;

$("div").on("tap",function(event){

console.log("tap",event.pageX,event.pageY);

tapTime=Date.now();

});

$("div").on("click",function(event){

console.log("time diff",Date.now()-tapTime);

});

點擊後,觀察控制台的輸出:

click會大概慢20ms,可能是因為它前面還要觸發mouse的事件。

這樣我們就實現了一個自定義tap事件,是自己封裝了一個事件機制,fastclick是使用原生的Event,如下fastclick的源碼,在touchend的回調函數裡面執行:

touch=event.changedTouches[];

// Synthesise a click event, with an extra attribute so it can be tracked

clickEvent=document.createEvent( MouseEvents );

clickEvent.initMouseEvent(this.determineEventType(targetElement),true,true,window,1,touch.screenX,touch.screenY,touch.clientX,touch.clientY,false,false,false,false,,null);

clickEvent.forwardedTouchEvent=true;

targetElement.dispatchEvent(clickEvent);

然後再調event.preventDefault禁掉原本的click事件的觸發。它裡面還做了其它一些的兼容性的處理。

這個時候如果要做一個放大的事件,你應該不難想到實現的方法。可以在touchstart裡面獲取event.touches兩根手指的初始位置,保存初始化手指的距離,然後在touchmove裡面再次獲取新位置,計算新的距離減掉老的距離,如果是正數則說明是放大,反之縮小,放大和縮小的尺度也是可以取到一個相對值。手機Safari有一個gesturestart/gesturechange/gestureend事件,在gesturechange的event裡面有一個放大比例scale的屬性。讀者可以自己嘗試實現一個放大和縮小的手勢事件。

當知道了怎麼實現一個自定義事件之後,現在來實現一個更為複雜的「搖一搖」事件。

3. 搖一搖事件

html5新增了一個devicemotion的事件,可以使用手機的重力感應。如下代碼所示:

window.ondevicemotion=function(event){

vargravity=event.accelerationIncludingGravity;

console.log(gravity.x,gravity.y,gravity.z);

}

x,y,z表示三個方向的重力加速度,如下圖所示:

x是手機短邊,y是長邊,z是和手機屏幕垂直的方向,當把手機平著放的時候,由於x、y和地平線平行,所以g(x) = g(y) = 0,而z和地平線垂直,所以g(z) = 9.8左右,同理當把手機豎著放的時候,g(x) = g(z) = 0,而g(y) = -9.8.

devicemotion事件會不斷地觸發,而且觸發得很快。

當我們把手機拿起來搖一搖的時候,這個場景應該是這樣的:

y軸和x軸的變化範圍從-45o到+45o,即這個區間是:

delta = 9.8 * sin(45o) * 2 = 13.8

即只要x軸和y軸的g值變化超過13.8,我們就認為發生了搖一搖事件。

根據上面的分析,不難寫出以下的代碼:

constEMPTY_VALUE=100;

constTHREAD_HOLD=13.8;

varminX=EMPTY_VALUE,

minY=EMPTY_VALUE;

window.ondevicemotion=function(event){

vargravity=event.accelerationIncludingGravity,

x=gravity.x,

y=gravity.y;

if(x

if(y

if(Math.abs(x-minX)>THREAD_HOLD&&

Math.abs(y-minY)>THREAD_HOLD){

console.log("shake");

varevent=newCustomEvent("shake");

window.dispatchEvent(event);

minX=minY=EMPTY_VALUE;

}

}

window.addEventListener("shake",function(){

console.log("window shake callback was called");

});

用一個minX和minY記錄最小的值,每次devicemotion觸發的時候就判斷當前的g值與最小值的差值是否超過了閾值,如果是的話就創建一個CustomEvent的實例,然後disatch給window,window上兼聽的onshake事件就會觸發了。

現在拿起手機搖一搖,控制台就會輸出:

這樣就實現了一個搖一搖shake事件。還有一個問題就是:這個shake會不會很容易觸發,即使不是搖一搖操作它也觸發了?根據實驗上面代碼如果不搖不容易觸發shake,同時搖的時候比較容易觸發。如果太難觸發可以把閾值改小點。

當然判斷是否搖一搖的演算法不止上面一個,你還可以想出其它更好的方法。

綜上,本文討論了怎麼去掉移動端click事件遲鈍的300ms延遲,怎麼實現一個快速響應的tap事件,怎麼封裝和觸發自定義事件,以及搖一搖的原理是怎麼樣的,怎麼實現一個搖一搖的shake事件。

相信閱讀了本文,你就知道了怎麼用一些基本事件進行組合觸發一些高級事件。通常把這些基本事件封裝起來,如上面用一個$Element的類,由它負責決定是否觸發tap,而高層的調用者不需要關心tap事件觸發的細節,這個$Element就相當於一個事件代理,或者也可以把tap當作一個門面。所以它是一個代理模式或者門面模式。更多設計模式可以查看本文《JS與面向對象》

關於本文

作者:@李銀城


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 前端早讀課 的精彩文章:

Nodejs cluster模塊深入探究
面向初學者的高階組件教程
構建高性能展開&收縮動畫
BATJ 前端面試的 5 大關鍵點,你 Get 到了嗎?

TAG:前端早讀課 |

您可能感興趣

Steam推出移動端APP「Steam Link」 手機平板串流玩Steam大作!
微軟公布新版Game Stack 可將XLive帶到移動端平台
Valve推出全新Steam移動端聊天應用
7年前的今天,Flash退出Android舞台,正式告別主流移動端
Valve已推出移動端Steam聊天軟體Steam Chat
PC、移動端VPN搭建Shadowsocks一鍵安裝腳本
Oculus在移動端App中為Rift提供遠程下載安裝功能
GoNetwork第一個移動端的乙太網基礎設施
移動端兼容問題:解決ios瀏覽器history.back頁面不刷新
如何用 iPad Pro 剪輯,移動端最強大的剪輯軟體 Lumafusion
進擊的V社:Valve推出移動端聊天軟體Steam Chat
CryptoCurve——一款移動端輕錢包App應用
Steam推出移動端APP 手機平板串流玩Steam大作!
Steam推出移動端APP 手機平板串流玩Steam大作!
V社推出移動端聊天軟體Steam Chat
Google Assistant 支持語音收付款,不過僅限於移動端
推出支持移動端的公有鏈IFMChain,「本能」認為移動是區塊鏈的未來
谷歌發布MobileNetV2:新一代移動端計算機視覺網路
Hulu中國:憑《使女的故事》「幹掉」Netflix,Hulu為什麼從移動端重返客廳?
GoNetwork—以太坊在移動端上高度可拓展的p2p網路