Touch events and gesture recognition
Basics of Touch Events
HTML5 provides a rich set of touch event interfaces for mobile devices, enabling developers to create responsive touch-based interactive experiences. Touch events differ from traditional mouse events by allowing simultaneous tracking of multiple touch points, facilitating complex gesture operations.
// Basic touch event listener example
const element = document.getElementById('touchArea');
element.addEventListener('touchstart', (e) => {
console.log('Touch started', e.touches);
});
element.addEventListener('touchmove', (e) => {
e.preventDefault(); // Prevent default scrolling behavior
console.log('Touch moved', e.touches);
});
element.addEventListener('touchend', (e) => {
console.log('Touch ended', e.changedTouches);
});
Main touch events include:
touchstart
: Triggered when a finger touches the screentouchmove
: Triggered when a finger moves across the screentouchend
: Triggered when a finger leaves the screentouchcancel
: Triggered when the system cancels the touch (e.g., due to an incoming call)
Multi-Touch Handling
Multi-touch is a key feature of mobile devices. The touches
property of the TouchEvent
object provides information about all current touch points.
element.addEventListener('touchmove', (e) => {
const touches = e.touches;
if (touches.length === 2) {
// Calculate distance between two points
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
const distance = Math.sqrt(dx * dx + dy * dy);
console.log('Distance between points:', distance);
}
});
Each touch point includes these common properties:
clientX/clientY
: Coordinates relative to the viewportpageX/pageY
: Coordinates relative to the documentidentifier
: Unique identifier for the touch pointtarget
: The DOM element initially touched
Common Gesture Recognition
Tap and Long Press
let touchTimer;
let isLongPress = false;
element.addEventListener('touchstart', () => {
touchTimer = setTimeout(() => {
isLongPress = true;
console.log('Long press event');
}, 500); // 500ms threshold for long press
});
element.addEventListener('touchend', (e) => {
clearTimeout(touchTimer);
if (!isLongPress) {
console.log('Tap event');
}
isLongPress = false;
});
Swipe Recognition
let startX, startY;
element.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
});
element.addEventListener('touchmove', (e) => {
const deltaX = e.touches[0].clientX - startX;
const deltaY = e.touches[0].clientY - startY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
if (deltaX > 10) {
console.log('Swipe right');
} else if (deltaX < -10) {
console.log('Swipe left');
}
} else {
if (deltaY > 10) {
console.log('Swipe down');
} else if (deltaY < -10) {
console.log('Swipe up');
}
}
});
Pinch Gesture
let initialDistance = 0;
element.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
initialDistance = Math.sqrt(dx * dx + dy * dy);
}
});
element.addEventListener('touchmove', (e) => {
if (e.touches.length === 2) {
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const currentDistance = Math.sqrt(dx * dx + dy * dy);
if (initialDistance > 0) {
const scale = currentDistance / initialDistance;
console.log('Scale ratio:', scale.toFixed(2));
}
}
});
Performance Optimization and Best Practices
Event Throttling
let lastMoveTime = 0;
const moveThreshold = 16; // Approximately 60fps
element.addEventListener('touchmove', (e) => {
const now = Date.now();
if (now - lastMoveTime >= moveThreshold) {
// Perform operation
updatePosition(e.touches[0].clientX, e.touches[0].clientY);
lastMoveTime = now;
}
});
Passive Event Listeners
// Improve scrolling performance
element.addEventListener('touchmove', (e) => {
// Lightweight operation
}, { passive: true });
CSS Hardware Acceleration
.touch-element {
will-change: transform; /* Hint for potential changes */
transform: translateZ(0); /* Trigger hardware acceleration */
}
Using Gesture Libraries
While manual gesture implementation is possible, professional libraries improve development efficiency and stability. Hammer.js is a popular choice:
import Hammer from 'hammerjs';
const mc = new Hammer(element);
mc.get('pan').set({ direction: Hammer.DIRECTION_ALL });
mc.on('pan', (e) => {
console.log('Pan:', e.deltaX, e.deltaY);
});
mc.on('pinch', (e) => {
console.log('Pinch:', e.scale);
});
mc.on('rotate', (e) => {
console.log('Rotate:', e.rotation);
});
Cross-Platform Compatibility
Different devices and browsers have varying touch event support, requiring compatibility handling:
function getSupportedEvents() {
if ('ontouchstart' in window) {
return {
start: 'touchstart',
move: 'touchmove',
end: 'touchend',
cancel: 'touchcancel'
};
} else if ('onpointerdown' in window) {
return {
start: 'pointerdown',
move: 'pointermove',
end: 'pointerup',
cancel: 'pointercancel'
};
} else {
return {
start: 'mousedown',
move: 'mousemove',
end: 'mouseup',
cancel: null
};
}
}
const events = getSupportedEvents();
element.addEventListener(events.start, handleStart);
Advanced Gesture Combinations
Complex gestures often require state management:
class GestureRecognizer {
constructor(element) {
this.state = 'idle';
this.element = element;
this.setupEvents();
}
setupEvents() {
this.element.addEventListener('touchstart', this.handleStart.bind(this));
this.element.addEventListener('touchmove', this.handleMove.bind(this));
this.element.addEventListener('touchend', this.handleEnd.bind(this));
}
handleStart(e) {
if (e.touches.length === 2) {
this.state = 'potentialZoom';
this.initialDistance = this.getTouchDistance(e.touches);
} else {
this.state = 'potentialTap';
this.startTime = Date.now();
}
}
handleMove(e) {
if (this.state === 'potentialZoom' && e.touches.length === 2) {
const currentDistance = this.getTouchDistance(e.touches);
const scale = currentDistance / this.initialDistance;
this.dispatchEvent('zoom', { scale });
}
}
handleEnd(e) {
if (this.state === 'potentialTap' && Date.now() - this.startTime < 200) {
this.dispatchEvent('tap');
}
this.state = 'idle';
}
getTouchDistance(touches) {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
}
dispatchEvent(type, detail) {
const event = new CustomEvent(`gesture:${type}`, { detail });
this.element.dispatchEvent(event);
}
}
Practical Application Scenarios
Image Viewer
class ImageViewer {
constructor(container) {
this.container = container;
this.image = container.querySelector('img');
this.scale = 1;
this.position = { x: 0, y: 0 };
this.setupGestures();
}
setupGestures() {
const mc = new Hammer(this.container);
// Double tap to reset
mc.on('doubletap', () => {
this.resetTransform();
});
// Pan
mc.on('pan', (e) => {
this.position.x += e.deltaX;
this.position.y += e.deltaY;
this.updateTransform();
});
// Pinch
mc.on('pinch', (e) => {
this.scale *= e.scale;
this.updateTransform();
});
}
updateTransform() {
this.image.style.transform = `
translate(${this.position.x}px, ${this.position.y}px)
scale(${this.scale})
`;
}
resetTransform() {
this.scale = 1;
this.position = { x: 0, y: 0 };
this.updateTransform();
}
}
Slide Menu
class SlideMenu {
constructor(menuElement) {
this.menu = menuElement;
this.startX = 0;
this.currentX = 0;
this.threshold = 100;
this.setupTouchEvents();
}
setupTouchEvents() {
this.menu.addEventListener('touchstart', (e) => {
this.startX = e.touches[0].clientX;
});
this.menu.addEventListener('touchmove', (e) => {
this.currentX = e.touches[0].clientX - this.startX;
// Limit right swipe
if (this.currentX > 0) {
this.menu.style.transform = `translateX(${this.currentX}px)`;
}
});
this.menu.addEventListener('touchend', () => {
if (this.currentX > this.threshold) {
this.openMenu();
} else {
this.closeMenu();
}
});
}
openMenu() {
this.menu.style.transform = 'translateX(250px)';
}
closeMenu() {
this.menu.style.transform = 'translateX(0)';
}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn