触摸事件

学习Canvas触摸事件处理,掌握触摸事件、手势识别和移动端交互开发方法。触摸事件是移动端交互的核心,正确处理触摸事件可以创建流畅的移动端体验。

触摸事件类型

事件列表

事件说明触发时机
touchstart触摸开始手指触摸屏幕
touchmove触摸移动手指在屏幕上移动
touchend触摸结束手指离开屏幕
touchcancel触摸取消触摸被中断(如来电)

基本监听

const canvas = document.getElementById('myCanvas')

canvas.addEventListener('touchstart', (e) => {
  e.preventDefault()
  console.log('触摸开始')
})

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault()
  console.log('触摸移动')
})

canvas.addEventListener('touchend', (e) => {
  console.log('触摸结束')
})

canvas.addEventListener('touchcancel', (e) => {
  console.log('触摸取消')
})

触摸对象

Touch对象属性

canvas.addEventListener('touchstart', (e) => {
  const touch = e.touches[0]
  
  console.log('触摸ID:', touch.identifier)
  console.log('相对于视口:', touch.clientX, touch.clientY)
  console.log('相对于页面:', touch.pageX, touch.pageY)
  console.log('相对于屏幕:', touch.screenX, touch.screenY)
})

触摸列表

canvas.addEventListener('touchstart', (e) => {
  console.log('当前触摸点数量:', e.touches.length)
  console.log('当前元素上的触摸点:', e.targetTouches.length)
  console.log('本次触摸变化的点:', e.changedTouches.length)
})

获取触摸坐标

function getTouchPos(canvas, e) {
  const rect = canvas.getBoundingClientRect()
  const touch = e.touches[0] || e.changedTouches[0]
  
  return {
    x: touch.clientX - rect.left,
    y: touch.clientY - rect.top
  }
}

canvas.addEventListener('touchstart', (e) => {
  e.preventDefault()
  const pos = getTouchPos(canvas, e)
  console.log('触摸位置:', pos.x, pos.y)
})

触摸事件演示

触摸事件演示(触摸或点击)

多点触控

追踪多个触摸点

const activeTouches = new Map()

canvas.addEventListener('touchstart', (e) => {
  e.preventDefault()
  
  for (const touch of e.changedTouches) {
    activeTouches.set(touch.identifier, {
      x: touch.clientX,
      y: touch.clientY,
      startX: touch.clientX,
      startY: touch.clientY
    })
  }
  
  console.log('活跃触摸点:', activeTouches.size)
})

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault()
  
  for (const touch of e.changedTouches) {
    const data = activeTouches.get(touch.identifier)
    if (data) {
      data.x = touch.clientX
      data.y = touch.clientY
    }
  }
})

canvas.addEventListener('touchend', (e) => {
  for (const touch of e.changedTouches) {
    activeTouches.delete(touch.identifier)
  }
})

双指缩放

let lastDistance = 0
let scale = 1

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault()
  
  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 distance = Math.sqrt(dx * dx + dy * dy)
    
    if (lastDistance > 0) {
      const delta = distance / lastDistance
      scale *= delta
      console.log('缩放:', scale)
    }
    
    lastDistance = distance
  }
})

canvas.addEventListener('touchend', (e) => {
  if (e.touches.length < 2) {
    lastDistance = 0
  }
})

双指旋转

let lastAngle = 0
let rotation = 0

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault()
  
  if (e.touches.length === 2) {
    const dx = e.touches[1].clientX - e.touches[0].clientX
    const dy = e.touches[1].clientY - e.touches[0].clientY
    const angle = Math.atan2(dy, dx)
    
    if (lastAngle !== 0) {
      const delta = angle - lastAngle
      rotation += delta
      console.log('旋转角度:', rotation * 180 / Math.PI)
    }
    
    lastAngle = angle
  }
})

canvas.addEventListener('touchend', (e) => {
  if (e.touches.length < 2) {
    lastAngle = 0
  }
})

手势识别

点击手势

class TapRecognizer {
  constructor(options = {}) {
    this.maxDuration = options.maxDuration || 300
    this.maxDistance = options.maxDistance || 10
    this.tapCount = options.tapCount || 1
    this.taps = []
  }
  
  recognize(e) {
    const touch = e.changedTouches[0]
    
    if (e.type === 'touchstart') {
      this.startTime = Date.now()
      this.startX = touch.clientX
      this.startY = touch.clientY
    }
    
    if (e.type === 'touchend') {
      const duration = Date.now() - this.startTime
      const dx = touch.clientX - this.startX
      const dy = touch.clientY - this.startY
      const distance = Math.sqrt(dx * dx + dy * dy)
      
      if (duration < this.maxDuration && distance < this.maxDistance) {
        this.taps.push(Date.now())
        
        if (this.taps.length >= this.tapCount) {
          const timeDiff = this.taps[this.taps.length - 1] - this.taps[0]
          if (timeDiff < this.maxDuration * this.tapCount) {
            return true
          }
          this.taps = [this.taps[this.taps.length - 1]]
        }
      }
    }
    
    return false
  }
}

const singleTap = new TapRecognizer({ tapCount: 1 })
const doubleTap = new TapRecognizer({ tapCount: 2 })

canvas.addEventListener('touchend', (e) => {
  if (singleTap.recognize(e)) {
    console.log('单击')
  }
  if (doubleTap.recognize(e)) {
    console.log('双击')
  }
})

滑动手势

class SwipeRecognizer {
  constructor(options = {}) {
    this.minDistance = options.minDistance || 50
    this.maxDuration = options.maxDuration || 500
    this.direction = options.direction || 'all'
  }
  
  recognize(e) {
    const touch = e.changedTouches[0]
    
    if (e.type === 'touchstart') {
      this.startTime = Date.now()
      this.startX = touch.clientX
      this.startY = touch.clientY
      return null
    }
    
    if (e.type === 'touchend') {
      const duration = Date.now() - this.startTime
      if (duration > this.maxDuration) return null
      
      const dx = touch.clientX - this.startX
      const dy = touch.clientY - this.startY
      const distance = Math.sqrt(dx * dx + dy * dy)
      
      if (distance < this.minDistance) return null
      
      const angle = Math.atan2(dy, dx) * 180 / Math.PI
      
      let direction
      if (angle >= -45 && angle < 45) direction = 'right'
      else if (angle >= 45 && angle < 135) direction = 'down'
      else if (angle >= -135 && angle < -45) direction = 'up'
      else direction = 'left'
      
      return { direction, distance, dx, dy }
    }
    
    return null
  }
}

const swiper = new SwipeRecognizer()

canvas.addEventListener('touchstart', (e) => swiper.recognize(e))
canvas.addEventListener('touchend', (e) => {
  const result = swiper.recognize(e)
  if (result) {
    console.log('滑动方向:', result.direction)
  }
})

长按手势

class LongPressRecognizer {
  constructor(options = {}) {
    this.minDuration = options.minDuration || 500
    this.maxDistance = options.maxDistance || 10
    this.timer = null
    this.recognized = false
  }
  
  start(e, callback) {
    const touch = e.touches[0]
    this.startX = touch.clientX
    this.startY = touch.clientY
    this.recognized = false
    
    this.timer = setTimeout(() => {
      this.recognized = true
      callback()
    }, this.minDuration)
  }
  
  move(e) {
    const touch = e.touches[0]
    const dx = touch.clientX - this.startX
    const dy = touch.clientY - this.startY
    const distance = Math.sqrt(dx * dx + dy * dy)
    
    if (distance > this.maxDistance) {
      this.cancel()
    }
  }
  
  cancel() {
    clearTimeout(this.timer)
    this.timer = null
  }
}

const longPress = new LongPressRecognizer()

canvas.addEventListener('touchstart', (e) => {
  longPress.start(e, () => {
    console.log('长按触发')
  })
})

canvas.addEventListener('touchmove', (e) => {
  longPress.move(e)
})

canvas.addEventListener('touchend', () => {
  longPress.cancel()
})

触摸与鼠标兼容

统一事件处理

class UnifiedPointer {
  constructor(canvas) {
    this.canvas = canvas
    this.handlers = {
      start: [],
      move: [],
      end: []
    }
    
    this.bindEvents()
  }
  
  bindEvents() {
    this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this))
    this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this))
    this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this))
    
    this.canvas.addEventListener('touchstart', this.onTouchStart.bind(this))
    this.canvas.addEventListener('touchmove', this.onTouchMove.bind(this))
    this.canvas.addEventListener('touchend', this.onTouchEnd.bind(this))
  }
  
  getPos(e) {
    const rect = this.canvas.getBoundingClientRect()
    if (e.touches) {
      return {
        x: e.touches[0].clientX - rect.left,
        y: e.touches[0].clientY - rect.top
      }
    }
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    }
  }
  
  onMouseDown(e) {
    this.trigger('start', this.getPos(e), e)
  }
  
  onMouseMove(e) {
    this.trigger('move', this.getPos(e), e)
  }
  
  onMouseUp(e) {
    this.trigger('end', this.getPos(e), e)
  }
  
  onTouchStart(e) {
    e.preventDefault()
    this.trigger('start', this.getPos(e), e)
  }
  
  onTouchMove(e) {
    e.preventDefault()
    this.trigger('move', this.getPos(e), e)
  }
  
  onTouchEnd(e) {
    this.trigger('end', null, e)
  }
  
  on(type, handler) {
    if (this.handlers[type]) {
      this.handlers[type].push(handler)
    }
  }
  
  trigger(type, pos, e) {
    this.handlers[type].forEach(handler => handler(pos, e))
  }
}

const pointer = new UnifiedPointer(canvas)

pointer.on('start', (pos) => {
  console.log('开始:', pos)
})

pointer.on('move', (pos) => {
  console.log('移动:', pos)
})

pointer.on('end', () => {
  console.log('结束')
})

触摸绘制

触摸绘制

性能优化

使用passive事件

canvas.addEventListener('touchstart', handler, { passive: true })
canvas.addEventListener('touchmove', handler, { passive: true })

减少重绘

let needsRedraw = false

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault()
  updateState(e)
  needsRedraw = true
})

function animationLoop() {
  if (needsRedraw) {
    redraw()
    needsRedraw = false
  }
  requestAnimationFrame(animationLoop)
}

animationLoop()