本文來自火狐瀏覽器官方文檔 https://developer.mozilla.org/zh-CN/docs/Web/API/Touch_events
觸摸事件(touch event)可響應用戶手指(或觸控筆)對屏幕或者觸控板操作,給基於觸控的用戶界面提供了可靠支持。
觸摸事件接口是較為底層的 API,可為特定程序提供多點觸控交互(比如雙指手勢)的支持。多點觸控交互開始於一個手指(或觸控筆)開始接觸設備平面的時刻。隨后其他手指也可觸摸設備表面,並隨意進行划動。當所有手指離開設備平面時,交互結束。整個交互期間,程序接收開始、移動、結束三個階段的觸摸事件。
觸摸事件與鼠標事件類似,不同的是觸摸事件還提供同一表面不同位置的同步觸摸。TouchEvent
接口將當前所有活動的觸摸點封裝起來。Touch
接口表示單獨一個觸摸點,其中包含參考瀏覽器視角的相對坐標。
定義節
- 表面(surface)
- 可感知觸摸的平面,可以是屏幕或觸控板。
- 觸摸點(touch point)
- 表面上的一個接觸點.。有可能是手指 (或者胳膊肘、耳朵、鼻子都行。但一般是手指) 或者觸摸筆.
接口節
-
TouchEvent
- 表示位於表面上的一個觸摸點的某個狀態發生改變時產生的事件。
-
Touch
- 表示用戶與觸摸表面間的一個單獨的接觸點。
-
TouchList
- 表示一組 Touch,用於多點觸控的情況。
示例節
本示例通過對多個觸控點進行同步跟蹤,讓用戶通過多點觸控的方式在 <canvas>
元素上用兩個(或以上)手指同時畫圖。本示例只在支持觸摸事件的瀏覽器下生效。
創建畫布節
<canvas id="canvas" width="600" height="600" style="border:solid black 1px;"> 你的瀏覽器不支持 canvas 元素。 </canvas> <br> 日志:<pre id="log" style="border: 1px solid #ccc;"></pre>
設置事件處理器節
當頁面加載時,下文的 startup()
函數由 window.onload
屬性觸發。
window.onload = function startup() { var el = document.getElementsByTagName("canvas")[0]; el.addEventListener("touchstart", handleStart, false); el.addEventListener("touchend", handleEnd, false); el.addEventListener("touchmove", handleMove, false); log("初始化成功。") }
該函數為 <canvas>
元素設置了所有相關的事件監聽器,使事件在觸發時能夠得到處理。
跟蹤新觸摸
我們將跟蹤當前存在的所有觸摸點。
var ongoingTouches = [];
當 touchstart
事件觸發時,平面上即出現一個新的觸摸點,繼而調用 handleStart()
:
function handleStart(evt) { evt.preventDefault(); console.log("觸摸開始。"); var el = document.getElementsByTagName("canvas")[0]; var ctx = el.getContext("2d"); var touches = evt.changedTouches; for (var i = 0; i < touches.length; i++) { console.log("開始第 " + i + " 個觸摸 ..."); ongoingTouches.push(copyTouch(touches[i])); var color = colorForTouch(touches[i]); ctx.beginPath(); ctx.arc(touches[i].pageX, touches[i].pageY, 4, 0, 2 * Math.PI, false); // 在起點畫一個圓。 ctx.fillStyle = color; ctx.fill(); console.log("第 " + i + " 個觸摸已開始。"); } }
event.preventDefault()
阻止了瀏覽器繼續處理觸摸(和鼠標)事件。 然后我們取得上下文,從事件的 TouchEvent.changedTouches
屬性中獲得已改變的觸摸點列表。
上述列表中所有的 Touch
對象即為當前所有活動的觸摸點,把它們置於一個數組中,然后為每個觸摸繪制起點。我們設置線條寬度為四像素,所以恰好會繪制一個半徑為 4 像素的圓。
當觸摸移動時繪制
在觸摸平面上移動一根或者幾根手指會觸發 touchmove
事件,從而將調用handleMove()
函數。本例中這個函數用於更新觸摸點信息,並為每個觸摸點從之前位置到當前位置之間繪制直線。
function handleMove(evt) { evt.preventDefault(); var el = document.getElementsByTagName("canvas")[0]; var ctx = el.getContext("2d"); var touches = evt.changedTouches; for (var i = 0; i < touches.length; i++) { var color = colorForTouch(touches[i]); var idx = ongoingTouchIndexById(touches[i].identifier); if (idx >= 0) { log("繼續第 " + idx + "個觸摸。"); ctx.beginPath(); log("ctx.moveTo(" + ongoingTouches[idx].pageX + ", " + ongoingTouches[idx].pageY + ");"); ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY); log("ctx.lineTo(" + touches[i].pageX + ", " + touches[i].pageY + ");"); ctx.lineTo(touches[i].pageX, touches[i].pageY); ctx.lineWidth = 4; ctx.strokeStyle = color; ctx.stroke(); ongoingTouches.splice(idx, 1, copyTouch(touches[i])); // 切換觸摸信息 console.log("."); } else { log("無法確定下一個觸摸點。"); } } }
這個函數不僅對所有已改變的觸摸點進行了遍歷,還從緩存的觸摸信息數組中得到了每個觸摸點要繪制的新線段的起點。這是通過讀取每個觸摸點的 Touch.identifier
屬性實現的。對每個觸摸點而言,該屬性是個唯一的整數,且手指接觸表面的整個過程中,這個屬性保持不變。
這樣我們就可以取得每個觸摸點之前位置的坐標,並且使用恰當的上下文方法繪制片段,從而將新舊兩個位置連接起來。
當片段繪制完畢后,我們調用 Array.splice()
將 ongoingTouches
數組中觸摸點之前的信息替換為當前信息。
觸摸結束處理
用戶的手指從表面抬起時將觸發 touchend
事件。我們通過調用下面的 handleEnd()
函數來處理此類事件。 這個函數的工作就是為每個結束的觸摸點繪制最后一個片段,然后將觸摸點從 ongoingTouches
數組中移除。
function handleEnd(evt) { evt.preventDefault(); log("觸摸結束。"); var el = document.getElementsByTagName("canvas")[0]; var ctx = el.getContext("2d"); var touches = evt.changedTouches; for (var i = 0; i < touches.length; i++) { var color = colorForTouch(touches[i]); var idx = ongoingTouchIndexById(touches[i].identifier); if (idx >= 0) { ctx.lineWidth = 4; ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY); ctx.lineTo(touches[i].pageX, touches[i].pageY); ctx.fillRect(touches[i].pageX - 4, touches[i].pageY - 4, 8, 8); // 在終點畫一個正方形 ongoingTouches.splice(idx, 1); // 用完后移除 } else { log("無法確定要結束哪個觸摸點。"); } } }
這個函數與上一個很相像,唯一的實質性區別就是在調用 Array.splice()
時, 我們把用過的觸摸實體從 ongoingTouches
數組中直接移除了,不再添加更新信息。對這個觸摸點的跟蹤隨之終止。
觸摸取消處理
如果用戶的手指不小心滑入瀏覽器界面,或者觸摸需要取消時,會觸發 touchcancel
事件。從而會調用下面的 handleCancel()
函數:
function handleCancel(evt) { evt.preventDefault(); console.log("觸摸取消。"); var touches = evt.changedTouches; for (var i = 0; i < touches.length; i++) { var idx = ongoingTouchIndexById(touches[i].identifier); ongoingTouches.splice(idx, 1); // 用完后移除 } }
這里的想法是立刻結束觸摸,所以我們直接從 ongoingTouches
數組中將其移除,而不去繪制最后的片段。
便捷函數節
本例中使用了幾個便捷函數,有必要簡單了解一下,對理解其它代碼很有幫助。
為每個觸摸點選擇一個顏色
為了區分每個觸摸點繪制的內容,我們引入 colorForTouch()
函數,根據每個觸摸點所獨有的標記設定一個顏色。 這個標記在這里可能是一個無意義的數字,但我們至少可以利用它“對於每個觸摸點的值都不同”這一特性。
function colorForTouch(touch) { var r = touch.identifier % 16; var g = Math.floor(touch.identifier / 3) % 16; var b = Math.floor(touch.identifier / 7) % 16; r = r.toString(16); // 轉換為十六進制字符串 g = g.toString(16); // 轉換為十六進制字符串 b = b.toString(16); // 轉換為十六進制字符串 var color = "#" + r + g + b; log("identifier " + touch.identifier + " 觸摸的顏色為:" + color); return color; }
這個函數返回一個表示顏色的字符串,可以在調用 <canvas>
的函數時使用。比如,若 Touch.identifier
的值為 10, 則返回的字符串為 "#aaa"。
譯注:這里的 colorForTouch()
不是一個好主意。Touch.identifier
是一個實驗性屬性,存在兼容性問題,不同瀏覽器之間實現方法不同,因此會得到不同的結果。
拷貝觸摸對象
有些瀏覽器(比如 mobile Safari)會在不同事件之間復用觸摸點對象,因此只復制所需的部分,要優於引用整個對象。
function copyTouch(touch) { return { identifier: touch.identifier, pageX: touch.pageX, pageY: touch.pageY }; }
查找觸摸點
ongoingTouchIndexById()
函數通過遍歷 ongoingTouches
數組來尋找與給定標記相匹配的觸摸點,返回該觸摸點在數組中的下標。
function ongoingTouchIndexById(idToFind) { for (var i=0; i<ongoingTouches.length; i++) { var id = ongoingTouches[i].identifier; if (id == idToFind) { return i; } } return -1; // 未找到 }
顯示后台操作記錄
function log(msg) {
var p = document.getElementById('log');
p.innerHTML = msg + "\n" + p.innerHTML;
}
如果你的瀏覽器支持觸摸,就可以 在線試用。
附加信息節
本節提供了在 web 應用中處理觸摸事件的其它信息。
處理點擊節
由於在 touchstart
(或系列 touchmove
事件里的首個)中調用 preventDefault()
也會阻止相應的鼠標事件的觸發,因此一般情況下我們在touchmove
而不是 touchstart
中調用它,這樣,鼠標事件仍可正常觸發,鏈接等部件也可繼續工作。有些框架采取了一個替代方案,使用觸摸事件代替鼠標事件來達到相同目的。 (下面的示例過於簡單,可能產生奇怪的行為。這里僅僅作為一個引導。)
function onTouch(evt) { evt.preventDefault(); if (evt.touches.length > 1 || (evt.type == "touchend" && evt.touches.length > 0)) return; var newEvt = document.createEvent("MouseEvents"); var type = null; var touch = null; switch (evt.type) { case "touchstart": type = "mousedown"; touch = evt.changedTouches[0]; break; case "touchmove": type = "mousemove"; touch = evt.changedTouches[0]; break; case "touchend": type = "mouseup"; touch = evt.changedTouches[0]; break; } newEvt.initMouseEvent(type, true, true, evt.originalTarget.ownerDocument.defaultView, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, evt.ctrlKey, evt.altKey, evt.shiftKey, evt.metaKey, 0, null); evt.originalTarget.dispatchEvent(newEvt); }
只在第二次觸摸時調用 preventDefault()節
為了阻止頁面產生類似 pinchZoom 的行為,可以通過“在系列觸摸點的第二個調用 preventDefault()
”技術來實現。但是該技術的行為並沒有在觸摸事件的標准中做出完整定義,並且在不同瀏覽器中會產生不同行為(比如,iOS將阻止縮放,但允許二指平移;Android 允許縮放但阻止平移;Opera 和 Firefox 目前會阻止所有縮放和平移操作)。目前,對於此類情況最好依靠 meta viewport
來阻止縮放,而不要依賴於上述行為。
標准節
標准 | 狀態 | 備注 |
---|---|---|
Touch Events – Level 2 Touch |
Draft | 添加 radiusX 、radiusY 、rotationAngle 和 force 屬性 |
Touch Events Touch |
Recommendation | 首次引入 |
瀏覽器兼容性節
Touch
節
Desktop | Mobile | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Basic support | Full support22 | Full supportYes | Full support52 | No supportNo | No supportNo | No supportNo | Full supportYes | Full supportYes | Full supportYes | Full support6 | Full supportYes | Full supportYes | ? |
Touch() constructor
|
Full support48 | No supportNo | Full support46 | No supportNo | Full support35 | ? | Full support48 | Full support48 | Full supportYes | Full support6 | Full supportYes | Full supportYes | Full supportYes |
clientX |
Full support22 | Full supportYes | Full support52 | No supportNo | No supportNo | No supportNo | Full supportYes | Full supportYes | Full supportYes | Full support6 | Full supportYes | Full supportYes | ? |
clientY |
Full support22 | Full supportYes | Full support52 | No supportNo | No supportNo | No supportNo | Full supportYes | Full supportYes | Full supportYes | Full support6 | Full supportYes | Full supportYes | ? |
force
|
Full supportYes | ? | Full supportYes | ? | ? | ? | Full supportYes | Full supportYes | ? | Full supportYes | ? | Full supportYes | ? |
identifier |
Full support22 | Full supportYes | Full support52 | No supportNo | No supportNo | No supportNo | Full supportYes | Full supportYes | Full supportYes | Full support6 | Full supportYes | Full supportYes | ? |
pageX |
Full support22 | Full supportYes | Full support52 | No supportNo | No supportNo | No supportNo | Full supportYes | Full supportYes | Full supportYes | Full support6 | Full supportYes | Full supportYes | ? |
pageY |
Full support22 | Full supportYes | Full support52 | No supportNo | No supportNo | No supportNo | Full supportYes | Full supportYes | Full supportYes | Full support6 | Full supportYes | Full supportYes | ? |
radiusX
|
Full support43 | ? | ? | ? | ? | ? | Full support43 | Full support43 | ? | ? | ? | Full supportYes | ? |
radiusY
|
Full support43 | ? | ? | ? | ? | ? | Full support43 | Full support43 | ? | ? | ? | Full supportYes | ? |
rotationAngle
|
Full support43 | ? | ? | ? | ? | ? | Full support43 | Full support43 | ? | ? | ? | Full supportYes | ? |
screenX |
Full support22 | Full supportYes | Full support52 | No supportNo | No supportNo | No supportNo | Full supportYes | Full supportYes | Full supportYes | Full support6 | Full supportYes | Full supportYes | ? |
screenY |
Full support22 | Full supportYes | Full support52 | No supportNo | No supportNo | No supportNo | Full supportYes | Full supportYes | Full supportYes | Full support6 | Full supportYes | Full supportYes | ? |
target |
Full support22 | Full supportYes | Full support52 | No supportNo | No supportNo | No supportNo | Full supportYes | Full supportYes | Full supportYes | Full support6 | Full supportYes | Full supportYes | ? |
Legend
- Full support
- Full support
- No support
- No support
- Compatibility unknown
- Compatibility unknown
- Experimental. Expect behavior to change in the future.
- Experimental. Expect behavior to change in the future.
- See implementation notes.
- See implementation notes.
Firefox,觸摸事件以及多進程(e10s)節
在 Firefox 中,觸摸事件隨 e10s(electrolysis 即 多進程 Firefox)的禁用而禁用。e10s 在 Firefox 中默認為可用,但可以在某些特定情形下關閉它,比如在安裝一些要求禁用 e10s 的工具或擴展時。這意味着即使在支持觸屏的桌面或便攜設備上,觸摸事件也可能失效。
你可以使用 about:support
查看“應用程序概要”部分中“多進程窗口”一欄來確定 e10s 是否啟用。1/1 表示啟用,0/1 表示禁用。
如果你想強制性的開啟 e10s(來顯式重新啟用觸摸事件支持),你需要使用 about:config 創建一個布爾類型的設置 browser.tabs.remote.force-enable
,將它設置為 true
,重啟瀏覽器,e10s 將始終啟用而不受其他設置的影響。