天天看點

移動Web觸控事件總結

移動web風風火火幾多年,讓我這個在pc端漂流的前端er不免心生仰慕,的确入行幾多年,也該是時候進軍移動web了。移動web中踩到的第一個坑就是事件問題,是以在吸取衆大神的經驗後,特作總結以示後來者。

首先pc端那一堆非常happy的滑鼠事件沒了,<code>mousedown</code>, <code>mouseup</code>, <code>mousemove</code>, <code>mouseover</code>, <code>mouseout</code>, <code>mouseenter</code>, <code>mouseleave</code>全都沒了,<code>click</code>也與之前有所差别。取而代之的是幾個原始的事件。

-touchstart

-touchmove

-touchend

-touchcancel

同樣事件處理函數中的<code>event</code>也與pc端有着極大的差别,最典型的是增加了三個與觸摸相關的屬性:

-touches

-changedtouches

-targettouches

在pc端一台機器隻會有一個滑鼠,是以與滑鼠相關的屬性都可以放到一個event對象上,但是移動端裝置大多支援多點觸控,這就意味着一個事件可能與多個觸控點相關,每個觸控點都需要記錄自己單獨的屬性。是以event對象中與touch相關的三個屬性都是<code>touchlist</code>類型,與觸控位置、目标元素、全都放到了touch對象上。

touch對象主要屬性如下:

-clientx / clienty:觸摸點相對浏覽器視窗的位置

-pagex / pagey:觸摸點相對于頁面的位置

-screenx / screeny:觸摸點相對于螢幕的位置

-identifier:touch對象的id

-target:目前的dom元素

現在反過來看看幾個touch相關事件,并與pc端事件做一下對比:

-<code>touchstart</code>: 觸控最開始時發生,類似于pc端的mousedown事件

-<code>touchmove</code>: 觸控點在螢幕上移動時觸發,類似于mousemove。但是在當在移動裝置上,觸控點從一個元素移動到另一個元素上時,并不會像pc端一樣觸發類似<code>mouseover</code>/<code>mouseout</code> <code>mouseenter</code>/<code>mouseleave</code>的事件。

-<code>touchend</code>: 在觸摸結束時觸發,類似mouseup

-<code>touchcancel</code>: 當一些更進階别的事情發生時,浏覽器會觸發該事件。比如突然來了一個電話,這時候會觸發touchcanel事件。如果是在遊戲中,就要在touchcancel時儲存目前遊戲的狀态資訊。

-<code>click</code>: 移動端的click事件雖然存在,但與pc端有着明顯的差異。這也就是著名的300ms問題,以及為了解決300ms延遲帶來的點透問題。

這幾個事件的事件對象的target屬性永遠是觸控事件最先發生的那個元素

先把click的問題放一下,我們先考慮以下能否在移動端模拟pc事件呢?答案是可以的。首先我們需要定義一下标準事件:

press -&gt; mousedown release -&gt; mouseup

move -&gt; mousemove

cancel -&gt; mouseleave

over -&gt; mouseover

out -&gt; mouseout

enter -&gt; mouseenter

leave -&gt; mouseleave

總體看來如下圖所示:

移動Web觸控事件總結

在我們定義好标準時候就要考慮如何去實作,值得慶幸的是,事件的傳播階段并沒有變化,這裡要感謝微軟不來添亂。盜一張圖:

移動Web觸控事件總結

我們先來看<code>toucmove</code>,單看名字容易讓人想當然的認為它與mousemove對應,然後上文說過了,當觸控點在不同元素上移動時,并不會觸發<code>mouseover</code>/<code>mouseout</code> <code>mouseenter</code>/<code>mouseleave</code>等事件,為了實作上面所說的<code>over</code>, <code>out</code>, <code>enter</code>, <code>leave</code>我們首先要能夠在<code>touchmove</code>中拿到目前位置的dom元素。

浏覽器為我們提供了<code>elementfrompoint</code>方法,這個函數根據<code>clientx</code>/<code>clienty</code>來選中最上層的dom元素,這為我們在touchmove中實時擷取最近的dom元素提供了保障。當觸控點從一個元素移動到另一個元素上時,對移出元素觸發<code>mytouchout</code>事件對移入元素觸發<code>mytouchover</code>事件,同時對與觸摸元素當觸控點在其上移動時觸發<code>mytouchmove</code>事件。

移動Web觸控事件總結

關于自定義事件,當然是使用createevent, initevent, dispatchevent三個函數,這三個函數并不是本文重點,請大家自行查閱《javascript進階程式設計第三版》13章中關于自定義事件的内容。

如此一來,我們的move、over、out等事件就有了着落,而press也非常簡單,隻需要綁定touchstart即可,同樣cancel也隻需要綁定touchcancel即可。

對于release我們不能簡單的綁定touchend。因為上文已經說過,touchend中touch的target屬性對應的是最初觸控的元素,并不會随着觸控點位置而改變。即是最終在元素b上拿開手指,touchend仍然會發生在元素a上。是以我們需要在touchend時,利用<code>elementfrompoint</code>擷取最後觸摸元素,在它身上觸發<code>mytouchend</code>事件來模拟release。

根據事件傳播的三個階段,最适合做這些事的階段應位于冒泡階段,代碼如下:

首先定義事件綁定與發射函數:

然後模拟mouse事件,分别在document上添加<code>touchstart</code>, <code>touchmove</code>, <code>touchend</code>的事件處理:

到目前為止标準化事件基本完成,剩下的就是enter與leave事件。這兩個事件與over、out類似,差別就是enter與leave在touch進入或者離開子元素時并不冒泡到父元素上,而over與out會冒泡到父元素。是以我們隻要在over與out上稍加變通即可:如果<code>evt.relatedtarget</code>是子元素則父元素不觸發事件,核心函數如下:

綜上,我們的标準化事件過程就全部完成了:

在最初移動web剛出現時,使用者輕按兩下時網頁會自動放大,是以為了區分輕按兩下縮放與click事件,浏覽器設定了一個間隔時間300ms。如果300ms内連續點選2次則認為是輕按兩下縮放,否則是單擊click,浏覽器内部實作原理如下所示

移動Web觸控事件總結

在實際應用中發現,300ms并不是絕對發生,當使用者設定了viewport并禁止縮放時,大部分浏覽器會禁止300ms延遲,但在低版本安卓以及微信、qq等應用的内嵌webview中仍然會發生300ms延遲問題。

在現今分秒必争的移動端,如果網頁在100ms之内沒有反應就會給使用者遲鈍的感覺,更何況300ms,根據上文我們可以簡單的使用<code>press</code>事件來解決問題。與click相比,press的間隔時間明顯縮短。但這也帶來了移動端另一個經典問題:點透!

點透的經典例子是:在遮罩層下有一個button或者文本框,在遮罩層上綁定press事件,當press發生時,事件函數中清除遮罩層。這樣業務場景下,當press時,遮罩層會消失,這是正常的,但是300ms後,遮罩層下方的元素觸發了click事件。

發生這件事的原因在于,press發生後遮罩層被清除,300ms後,浏覽器找到目前最上層元素,觸發click事件,過程原理如下:

如果我們全部依賴press而不去綁定click事件,是否可行呢?答案是否定的,因為press隻對應<code>touchstart</code>,如果使用者一直按住不放,或者先按住在滑到别的元素上,這不能認為是一次click事件。那麼我們是否可以像自定義<code>mytouch*</code>等事件那樣來定義自己的<code>click</code>事件呢?答案是可行的!

我們可以認為當觸控點選開始并且在結束時所經過的事件不超過300ms而且移動位置不超過4px,則這次事件就是一次完整的click事件。

這個過程涉及touchstart、touchmove和touchend三個事件,首先綁定document的touchstart事件:

整個過程核心邏輯在于dofastclick函數中:

現在我們添加了自定義的click事件,那麼問題來了在我們的自定義click中不會存在300ms延遲,但是現在浏覽器存在兩個click事件,一個是我們定義的,一個是原生的click事件。原生的click事件仍然會在300ms後執行,當你對一個元素綁定click事件時,一次click通常會觸發兩次click事件,這也是另一個經典的<code>鬼點選問題</code>。是以我們需要将原生的click事件徹底禁止掉。根據事件的三個處理階段,最合适的處理地方在于捕獲階段,阻止原生click的繼續傳播和預設行為。

現在鬼點選的問題解決了,但是實踐發現

移動浏覽器仍然保留<code>mousedown</code>與<code>mouseup</code>事件,這兩個事件仍然存在300ms延遲的問題!!!當遮罩層的下方是一個文本框時,300ms後mousedown發生,鍵盤就是在<code>mousedown</code>的時候彈出的!是以我們需要把mousedown事件一起禁掉。

那麼事情結束了麼?然并卵,如果将mousedown禁掉,你的input文本框永遠不會再彈出鍵盤!!!是以我們需要做一下判斷,如果是文本框不能<code>preventdefault</code>:

總結一下,目前我還沒有發現完美的解決方案,也就是說如果你的移動浏覽器沒有禁用300ms延遲,如果你的遮罩層下方是個文本框,如果你的業務剛好滿足點透的業務場景。。。貌似沒有完美的方式阻止鍵盤彈出。或者可以使用緩動動畫,過渡300ms。

繼續閱讀