阿里云主机折上折
  • 微信号
Current Site:Index > Touch events and gesture recognition

Touch events and gesture recognition

Author:Chuan Chen 阅读数:32754人阅读 分类: HTML

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 screen
  • touchmove: Triggered when a finger moves across the screen
  • touchend: Triggered when a finger leaves the screen
  • touchcancel: 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 viewport
  • pageX/pageY: Coordinates relative to the document
  • identifier: Unique identifier for the touch point
  • target: 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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.