多点触控交互
触摸事件接口支持应用程序特定的单点和多点触控交互。然而,对于程序员来说,这些接口可能有点棘手,因为触摸事件与鼠标事件等其他 DOM 输入事件非常不同。本指南中描述的应用程序展示了如何使用触摸事件进行简单的单点和多点触控交互,这是构建应用程序特定手势所需的基础。
示例
本示例演示了如何使用 touchstart、touchmove、touchcancel 和 touchend) 触摸事件来处理以下手势:单点触控、两点(同时)触控、多于两点同时触控、单指滑动以及两指移动/捏合/滑动。
定义触摸目标
该应用程序使用 <div> 元素来表示四个触摸区域。
div {
margin: 0em;
padding: 2em;
}
#target1 {
background: white;
border: 1px solid black;
}
#target2 {
background: white;
border: 1px solid black;
}
#target3 {
background: white;
border: 1px solid black;
}
#target4 {
background: white;
border: 1px solid black;
}
全局状态
tpCache 用于在触摸点触发的事件之外缓存触摸点以供处理。
// Log events flag
let logEvents = false;
// Touch Point cache
const tpCache = [];
注册事件处理程序
为所有四种触摸事件类型注册了事件处理程序。touchend 和 touchcancel 事件类型使用相同的处理程序。
function setHandlers(name) {
// Install event handlers for the given element
const el = document.getElementById(name);
el.addEventListener("touchstart", startHandler);
el.addEventListener("touchmove", moveHandler);
// Use same handler for touchcancel and touchend
el.addEventListener("touchcancel", endHandler);
el.addEventListener("touchend", endHandler);
}
function init() {
setHandlers("target1");
setHandlers("target2");
setHandlers("target3");
setHandlers("target4");
}
移动/捏合/缩放处理程序
此函数提供了对两点水平移动/捏合/缩放处理非常基础的支持。代码不包含错误处理或垂直移动。请注意,捏合和缩放移动检测的阈值是应用程序特定的(且依赖于设备)的。
// This is a very basic 2-touch move/pinch/zoom handler that does not include
// error handling, only handles horizontal moves, etc.
function handlePinchZoom(ev) {
if (ev.targetTouches.length === 2 && ev.changedTouches.length === 2) {
// Check if the two target touches are the same ones that started
// the 2-touch
const point1 = tpCache.findLastIndex(
(tp) => tp.identifier === ev.targetTouches[0].identifier,
);
const point2 = tpCache.findLastIndex(
(tp) => tp.identifier === ev.targetTouches[1].identifier,
);
if (point1 >= 0 && point2 >= 0) {
// Calculate the difference between the start and move coordinates
const diff1 = Math.abs(
tpCache[point1].clientX - ev.targetTouches[0].clientX,
);
const diff2 = Math.abs(
tpCache[point2].clientX - ev.targetTouches[1].clientX,
);
// This threshold is device dependent as well as application specific
const PINCH_THRESHOLD = ev.target.clientWidth / 10;
if (diff1 >= PINCH_THRESHOLD && diff2 >= PINCH_THRESHOLD)
ev.target.style.background = "green";
} else {
// empty tpCache
tpCache.length = 0;
}
}
}
触摸开始处理程序
touchstart 事件处理程序会缓存触摸点以支持两点手势。它还调用 preventDefault() 来阻止浏览器应用进一步的事件处理(例如,鼠标事件模拟)。
function startHandler(ev) {
// If the user makes simultaneous touches, the browser will fire a
// separate touchstart event for each touch point. Thus if there are
// three simultaneous touches, the first touchstart event will have
// targetTouches length of one, the second event will have a length
// of two, and so on.
ev.preventDefault();
// Cache the touch points for later processing of 2-touch pinch/zoom
if (ev.targetTouches.length === 2) {
for (const touch of ev.targetTouches) {
tpCache.push(touch);
}
}
if (logEvents) log("touchStart", ev, true);
updateBackground(ev);
}
触摸移动处理程序
touchmove 处理程序出于与上述相同的原因调用 preventDefault(),并调用捏合/缩放处理程序。
function moveHandler(ev) {
// Note: if the user makes more than one "simultaneous" touches, most browsers
// fire at least one touchmove event and some will fire several touch moves.
// Consequently, an application might want to "ignore" some touch moves.
//
// This function sets the target element's border to "dashed" to visually
// indicate the target received a move event.
//
ev.preventDefault();
if (logEvents) log("touchMove", ev, false);
// To avoid too much color flashing many touchmove events are started,
// don't update the background if two touch points are active
if (!(ev.touches.length === 2 && ev.targetTouches.length === 2))
updateBackground(ev);
// Set the target element's border to dashed to give a clear visual
// indication the element received a move event.
ev.target.style.border = "dashed";
// Check this event for 2-touch Move/Pinch/Zoom gesture
handlePinchZoom(ev);
}
触摸结束处理程序
touchend 处理程序将事件目标的背景颜色恢复为其原始颜色。
function endHandler(ev) {
ev.preventDefault();
if (logEvents) log(ev.type, ev, false);
if (ev.targetTouches.length === 0) {
// Restore background and border to original values
ev.target.style.background = "white";
ev.target.style.border = "1px solid black";
}
}
应用程序 UI
该应用程序使用 <div> 元素作为触摸区域,并提供按钮来启用日志记录和清除日志。
<div id="target1">Tap, Hold or Swipe me 1</div>
<div id="target2">Tap, Hold or Swipe me 2</div>
<div id="target3">Tap, Hold or Swipe me 3</div>
<div id="target4">Tap, Hold or Swipe me 4</div>
<!-- UI for logging/debugging -->
<button id="toggle-log">Start/Stop event logging</button>
<button id="clear-log">Clear the log</button>
<output id="output"></output>
杂项函数
这些函数支持该应用程序,但与事件流没有直接关系。
更新背景颜色
触摸区域的背景颜色将按如下方式更改:无触摸时为 white;单点触控时为 yellow;两点同时触控时为 pink;三点或更多点同时触控时为 lightblue。有关检测到两指移动/捏合/缩放时背景颜色变化的详细信息,请参阅触摸移动处理程序。
function updateBackground(ev) {
// Change background color based on the number simultaneous touches
// in the event's targetTouches list:
// yellow - one tap (or hold)
// pink - two taps
// lightblue - more than two taps
switch (ev.targetTouches.length) {
case 1:
// Single tap`
ev.target.style.background = "yellow";
break;
case 2:
// Two simultaneous touches
ev.target.style.background = "pink";
break;
default:
// More than two simultaneous touches
ev.target.style.background = "lightblue";
}
}
事件日志记录
这些函数用于将事件活动记录到应用程序窗口,以支持调试和了解事件流。
const output = document.getElementById("output");
function toggleLog(ev) {
logEvents = !logEvents;
}
document.getElementById("toggle-log").addEventListener("click", toggleLog);
function log(name, ev, printTargetIds) {
let s =
`${name}: touches = ${ev.touches.length} ; ` +
`targetTouches = ${ev.targetTouches.length} ; ` +
`changedTouches = ${ev.changedTouches.length}`;
output.innerText += `${s}\n`;
if (printTargetIds) {
s = "";
for (const touch of ev.targetTouches) {
s += `... id = ${touch.identifier}\n`;
}
output.innerText += s;
}
}
function clearLog(event) {
output.textContent = "";
}
document.getElementById("clear-log").addEventListener("click", clearLog);