MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 触摸事件处理与手势识别

2022-02-134.4k 阅读

React 触摸事件基础

在 React 应用中,触摸事件是与移动设备交互的重要组成部分。React 提供了一系列与触摸相关的事件,这些事件与传统的鼠标事件有一些相似之处,但也存在一些差异,因为触摸交互是基于手指与屏幕的直接接触。

触摸事件类型

  1. touchstart:当手指触摸屏幕时触发。这个事件可以用于初始化手势识别的过程,例如开始一个拖动操作或者识别一个点击动作的起始点。
import React, { Component } from'react';

class TouchExample extends Component {
  handleTouchStart = (event) => {
    console.log('Touch started:', event.touches[0].clientX, event.touches[0].clientY);
  }

  render() {
    return (
      <div onTouchStart={this.handleTouchStart}>
        Touch me
      </div>
    );
  }
}

export default TouchExample;

在上述代码中,handleTouchStart 函数在 touchstart 事件触发时被调用,通过 event.touches[0] 可以获取到第一个触摸点的坐标信息。

  1. touchmove:当手指在屏幕上移动时持续触发。这对于实现拖动、缩放等手势操作至关重要。通过监听 touchmove 事件,我们可以实时获取触摸点的位置变化。
import React, { Component } from'react';

class TouchExample extends Component {
  handleTouchMove = (event) => {
    event.preventDefault();
    const touch = event.touches[0];
    console.log('Touch moved:', touch.clientX, touch.clientY);
  }

  render() {
    return (
      <div onTouchMove={this.handleTouchMove}>
        Drag me
      </div>
    );
  }
}

export default TouchExample;

这里调用 event.preventDefault() 是为了阻止浏览器的默认滚动行为,避免在触摸移动时页面发生不必要的滚动。

  1. touchend:当手指离开屏幕时触发。这通常用于结束一个手势操作,例如完成一次拖动或者确定一次点击动作的结束。
import React, { Component } from'react';

class TouchExample extends Component {
  handleTouchEnd = (event) => {
    console.log('Touch ended');
  }

  render() {
    return (
      <div onTouchEnd={this.handleTouchEnd}>
        Release me
      </div>
    );
  }
}

export default TouchExample;
  1. touchcancel:当触摸操作被系统取消时触发,比如设备突然接收到一个电话,导致当前触摸操作被打断。
import React, { Component } from'react';

class TouchExample extends Component {
  handleTouchCancel = (event) => {
    console.log('Touch cancelled');
  }

  render() {
    return (
      <div onTouchCancel={this.handleTouchCancel}>
        Operation might be cancelled
      </div>
    );
  }
}

export default TouchExample;

事件对象属性

触摸事件的 event 对象包含了丰富的信息,用于处理触摸相关的逻辑。

  1. touches:这是一个 TouchList 对象,包含了当前屏幕上所有触摸点的信息。每个触摸点都是一个 Touch 对象,具有 clientXclientY 等属性,分别表示触摸点在视口内的横坐标和纵坐标。
  2. targetTouches:同样是一个 TouchList 对象,但它只包含了当前事件目标元素上的触摸点信息。这在处理复杂布局中的触摸事件时非常有用,比如在一个包含多个子元素的父元素上监听触摸事件,通过 targetTouches 可以准确获取作用在特定子元素上的触摸点。
  3. changedTouches:该 TouchList 对象记录了在本次事件中状态发生改变的触摸点。例如,在 touchstart 事件中,changedTouches 包含了新开始触摸的点;在 touchmove 事件中,它包含了移动的点;在 touchend 事件中,它包含了离开屏幕的点。

手势识别原理

手势识别是在触摸事件的基础上,通过分析触摸点的位置、数量、移动轨迹等信息,来判断用户执行了何种手势操作,如点击、长按、拖动、缩放、旋转等。

点击手势识别

点击手势可以通过 touchstarttouchend 事件的组合来识别。基本思路是在 touchstart 事件触发时记录触摸点的位置和时间,然后在 touchend 事件触发时,检查触摸点的位置是否在一个较小的范围内(以排除误操作),并且时间间隔是否在一个合理的阈值内(通常认为较短的时间间隔为点击操作)。

import React, { Component } from'react';

class TapGesture extends Component {
  constructor(props) {
    super(props);
    this.state = {
      touchStartX: null,
      touchStartY: null,
      startTime: null
    };
  }

  handleTouchStart = (event) => {
    const touch = event.touches[0];
    this.setState({
      touchStartX: touch.clientX,
      touchStartY: touch.clientY,
      startTime: new Date().getTime()
    });
  }

  handleTouchEnd = (event) => {
    const touch = event.changedTouches[0];
    const { touchStartX, touchStartY, startTime } = this.state;
    const currentTime = new Date().getTime();
    const distance = Math.sqrt((touch.clientX - touchStartX) ** 2 + (touch.clientY - touchStartY) ** 2);
    if (distance < 20 && currentTime - startTime < 300) {
      console.log('Tap gesture detected');
    }
    this.setState({
      touchStartX: null,
      touchStartY: null,
      startTime: null
    });
  }

  render() {
    return (
      <div
        onTouchStart={this.handleTouchStart}
        onTouchEnd={this.handleTouchEnd}
      >
        Detect tap
      </div>
    );
  }
}

export default TapGesture;

长按手势识别

长按手势识别需要在 touchstart 事件触发时启动一个定时器,当 touchend 事件触发时清除定时器。如果定时器在 touchend 事件触发前达到了设定的长按时间阈值,则认为触发了长按手势。

import React, { Component } from'react';

class LongPressGesture extends Component {
  constructor(props) {
    super(props);
    this.state = {
      longPressTimer: null
    };
  }

  handleTouchStart = (event) => {
    const longPressDuration = 1000; // 1 second
    const timer = setTimeout(() => {
      console.log('Long press gesture detected');
    }, longPressDuration);
    this.setState({
      longPressTimer: timer
    });
  }

  handleTouchEnd = (event) => {
    const { longPressTimer } = this.state;
    if (longPressTimer) {
      clearTimeout(longPressTimer);
      this.setState({
        longPressTimer: null
      });
    }
  }

  render() {
    return (
      <div
        onTouchStart={this.handleTouchStart}
        onTouchEnd={this.handleTouchEnd}
      >
        Detect long press
      </div>
    );
  }
}

export default LongPressGesture;

拖动手势识别

拖动手势主要依赖 touchmove 事件。在 touchstart 事件中记录起始位置,然后在 touchmove 事件中根据触摸点的移动距离更新元素的位置。

import React, { Component } from'react';

class DragGesture extends Component {
  constructor(props) {
    super(props);
    this.state = {
      x: 0,
      y: 0,
      startX: null,
      startY: null
    };
  }

  handleTouchStart = (event) => {
    const touch = event.touches[0];
    this.setState({
      startX: touch.clientX,
      startY: touch.clientY
    });
  }

  handleTouchMove = (event) => {
    event.preventDefault();
    const touch = event.touches[0];
    const { startX, startY } = this.state;
    if (startX!== null && startY!== null) {
      const dx = touch.clientX - startX;
      const dy = touch.clientY - startY;
      this.setState((prevState) => ({
        x: prevState.x + dx,
        y: prevState.y + dy,
        startX: touch.clientX,
        startY: touch.clientY
      }));
    }
  }

  handleTouchEnd = (event) => {
    this.setState({
      startX: null,
      startY: null
    });
  }

  render() {
    const { x, y } = this.state;
    return (
      <div
        onTouchStart={this.handleTouchStart}
        onTouchMove={this.handleTouchMove}
        onTouchEnd={this.handleTouchEnd}
        style={{
          position: 'absolute',
          left: x,
          top: y,
          width: 100,
          height: 100,
          backgroundColor: 'lightblue'
        }}
      >
        Drag me
      </div>
    );
  }
}

export default DragGesture;

缩放手势识别

缩放手势通常涉及到两个触摸点。通过在 touchstart 事件中记录两个触摸点之间的初始距离,然后在 touchmove 事件中对比当前两个触摸点的距离,从而计算出缩放比例。

import React, { Component } from'react';

class ZoomGesture extends Component {
  constructor(props) {
    super(props);
    this.state = {
      scale: 1,
      initialDistance: null
    };
  }

  handleTouchStart = (event) => {
    if (event.touches.length === 2) {
      const touch1 = event.touches[0];
      const touch2 = event.touches[1];
      const dx = touch2.clientX - touch1.clientX;
      const dy = touch2.clientY - touch1.clientY;
      const distance = Math.sqrt(dx ** 2 + dy ** 2);
      this.setState({
        initialDistance: distance
      });
    }
  }

  handleTouchMove = (event) => {
    if (event.touches.length === 2 && this.state.initialDistance!== null) {
      const touch1 = event.touches[0];
      const touch2 = event.touches[1];
      const dx = touch2.clientX - touch1.clientX;
      const dy = touch2.clientY - touch1.clientY;
      const currentDistance = Math.sqrt(dx ** 2 + dy ** 2);
      const newScale = currentDistance / this.state.initialDistance;
      this.setState((prevState) => ({
        scale: prevState.scale * newScale,
        initialDistance: currentDistance
      }));
    }
  }

  handleTouchEnd = (event) => {
    this.setState({
      initialDistance: null
    });
  }

  render() {
    const { scale } = this.state;
    return (
      <div
        onTouchStart={this.handleTouchStart}
        onTouchMove={this.handleTouchMove}
        onTouchEnd={this.handleTouchEnd}
        style={{
          transform: `scale(${scale})`,
          width: 200,
          height: 200,
          backgroundColor: 'lightgreen'
        }}
      >
        Zoom me
      </div>
    );
  }
}

export default ZoomGesture;

旋转手势识别

旋转手势也基于两个触摸点。在 touchstart 事件中记录两个触摸点形成的向量的初始角度,在 touchmove 事件中计算当前向量的角度,通过对比角度变化来确定旋转的方向和角度。

import React, { Component } from'react';

class RotateGesture extends Component {
  constructor(props) {
    super(props);
    this.state = {
      rotation: 0,
      initialAngle: null
    };
  }

  getAngle = (x1, y1, x2, y2) => {
    return Math.atan2(y2 - y1, x2 - x1);
  }

  handleTouchStart = (event) => {
    if (event.touches.length === 2) {
      const touch1 = event.touches[0];
      const touch2 = event.touches[1];
      const angle = this.getAngle(touch1.clientX, touch1.clientY, touch2.clientX, touch2.clientY);
      this.setState({
        initialAngle: angle
      });
    }
  }

  handleTouchMove = (event) => {
    if (event.touches.length === 2 && this.state.initialAngle!== null) {
      const touch1 = event.touches[0];
      const touch2 = event.touches[1];
      const currentAngle = this.getAngle(touch1.clientX, touch1.clientY, touch2.clientX, touch2.clientY);
      const rotation = currentAngle - this.state.initialAngle;
      this.setState((prevState) => ({
        rotation: prevState.rotation + rotation,
        initialAngle: currentAngle
      }));
    }
  }

  handleTouchEnd = (event) => {
    this.setState({
      initialAngle: null
    });
  }

  render() {
    const { rotation } = this.state;
    return (
      <div
        onTouchStart={this.handleTouchStart}
        onTouchMove={this.handleTouchMove}
        onTouchEnd={this.handleTouchEnd}
        style={{
          transform: `rotate(${rotation}rad)`,
          width: 200,
          height: 200,
          backgroundColor: 'lightpink'
        }}
      >
        Rotate me
      </div>
    );
  }
}

export default RotateGesture;

第三方库助力手势识别

虽然可以通过原生的触摸事件来实现手势识别,但手动编写复杂手势逻辑可能会变得繁琐且容易出错。因此,使用第三方库可以大大简化这个过程。

React - Gesture - Handler

  1. 安装与基本使用React - Gesture - Handler 是一个高性能的手势处理库,支持多种手势,如点击、拖动、缩放、旋转等。首先,通过 npm 安装该库:npm install react - gesture - handler。 在 React 应用中,需要将应用包裹在 GestureHandlerRootView 组件中(对于 React Native 应用有不同的使用方式,这里以 React Web 为例)。
import React from'react';
import { GestureHandlerRootView, TapGestureHandler, PanGestureHandler } from'react - gesture - handler';

const MyComponent = () => {
  const onTap = () => {
    console.log('Tapped');
  };

  const onPan = (event) => {
    console.log('Panning:', event.nativeEvent.translationX, event.nativeEvent.translationY);
  };

  return (
    <GestureHandlerRootView>
      <TapGestureHandler onPress={onTap}>
        <div style={{ width: 200, height: 100, backgroundColor: 'lightblue' }}>
          Tap me
        </div>
      </TapGestureHandler>
      <PanGestureHandler onGestureEvent={onPan}>
        <div style={{ width: 200, height: 100, backgroundColor: 'lightgreen', marginTop: 20 }}>
          Drag me
        </div>
      </PanGestureHandler>
    </GestureHandlerRootView>
  );
};

export default MyComponent;
  1. 高级手势组合React - Gesture - Handler 允许组合多个手势,并且可以定义手势之间的优先级。例如,在一个组件上同时识别点击和长按手势,并且长按手势优先于点击手势。
import React from'react';
import { GestureHandlerRootView, TapGestureHandler, LongPressGestureHandler } from'react - gesture - handler';

const MyComponent = () => {
  const onTap = () => {
    console.log('Tapped');
  };

  const onLongPress = () => {
    console.log('Long pressed');
  };

  return (
    <GestureHandlerRootView>
      <LongPressGestureHandler onLongPress={onLongPress}>
        <TapGestureHandler shouldCancelWhenOutside={false} onPress={onTap}>
          <div style={{ width: 200, height: 100, backgroundColor: 'lightblue' }}>
            Tap or long press me
          </div>
        </TapGestureHandler>
      </LongPressGestureHandler>
    </GestureHandlerRootView>
  );
};

export default MyComponent;

在上述代码中,LongPressGestureHandler 包裹了 TapGestureHandler,表示长按手势优先。shouldCancelWhenOutside={false} 确保在组件外松开手指也能触发点击事件。

Hammer.js

  1. 引入与初始化Hammer.js 是另一个流行的手势识别库,它可以在浏览器环境中识别各种手势。首先,通过 npm 安装:npm install hammerjs。 在 React 组件中,需要引入 Hammer 并进行初始化。
import React, { useEffect } from'react';
import Hammer from 'hammerjs';

const MyComponent = () => {
  useEffect(() => {
    const element = document.getElementById('my - element');
    const hammer = new Hammer(element);

    hammer.on('tap', () => {
      console.log('Tapped');
    });

    hammer.on('pan', (event) => {
      console.log('Panning:', event.deltaX, event.deltaY);
    });

    return () => {
      hammer.destroy();
    };
  }, []);

  return (
    <div id="my - element" style={{ width: 200, height: 100, backgroundColor: 'lightblue' }}>
      Interact with me
    </div>
  );
};

export default MyComponent;
  1. 自定义手势与配置Hammer.js 允许自定义手势的配置参数,例如设置点击的阈值、长按的时间等。同时,也可以定义一些复杂的自定义手势。
import React, { useEffect } from'react';
import Hammer from 'hammerjs';

const MyComponent = () => {
  useEffect(() => {
    const element = document.getElementById('my - element');
    const hammer = new Hammer(element);

    const customGesture = new Hammer.Pan({
      threshold: 10, // 触发平移的最小移动距离
      pointers: 1, // 只允许单指操作
      direction: Hammer.DIRECTION_ALL
    });

    hammer.add(customGesture);

    hammer.on('customPan', (event) => {
      console.log('Custom pan gesture:', event.deltaX, event.deltaY);
    });

    return () => {
      hammer.destroy();
    };
  }, []);

  return (
    <div id="my - element" style={{ width: 200, height: 100, backgroundColor: 'lightblue' }}>
      Try custom gesture
    </div>
  );
};

export default MyComponent;

在上述代码中,我们定义了一个名为 customPan 的自定义平移手势,并设置了一些参数,然后监听该手势的触发。

触摸事件与手势识别的性能优化

在处理触摸事件和手势识别时,性能优化至关重要,尤其是在移动设备上,因为资源相对有限。

减少事件处理函数中的计算量

在触摸事件处理函数中,应尽量避免复杂的计算。例如,在 touchmove 事件中频繁进行大量的数学运算或者 DOM 操作会导致卡顿。如果需要进行复杂计算,可以考虑在 requestAnimationFrame 中进行,这样可以将计算分散到每一帧,提高动画的流畅性。

import React, { Component } from'react';

class PerformanceOptimizedDrag extends Component {
  constructor(props) {
    super(props);
    this.state = {
      x: 0,
      y: 0,
      startX: null,
      startY: null
    };
  }

  handleTouchStart = (event) => {
    const touch = event.touches[0];
    this.setState({
      startX: touch.clientX,
      startY: touch.clientY
    });
  }

  handleTouchMove = (event) => {
    event.preventDefault();
    const touch = event.touches[0];
    const { startX, startY } = this.state;
    if (startX!== null && startY!== null) {
      requestAnimationFrame(() => {
        const dx = touch.clientX - startX;
        const dy = touch.clientY - startY;
        this.setState((prevState) => ({
          x: prevState.x + dx,
          y: prevState.y + dy,
          startX: touch.clientX,
          startY: touch.clientY
        }));
      });
    }
  }

  handleTouchEnd = (event) => {
    this.setState({
      startX: null,
      startY: null
    });
  }

  render() {
    const { x, y } = this.state;
    return (
      <div
        onTouchStart={this.handleTouchStart}
        onTouchMove={this.handleTouchMove}
        onTouchEnd={this.handleTouchEnd}
        style={{
          position: 'absolute',
          left: x,
          top: y,
          width: 100,
          height: 100,
          backgroundColor: 'lightblue'
        }}
      >
        Drag me with better performance
      </div>
    );
  }
}

export default PerformanceOptimizedDrag;

合理使用被动事件

touchmove 事件中,浏览器默认会执行一些滚动等操作。通过将 touchmove 事件标记为被动事件,可以提高性能,因为浏览器不需要等待 JavaScript 事件处理函数执行完毕再进行默认行为。在 React 中,可以通过以下方式设置:

import React, { Component } from'react';

class PassiveTouchExample extends Component {
  handleTouchMove = (event) => {
    console.log('Touch moved:', event.touches[0].clientX, event.touches[0].clientY);
  }

  render() {
    return (
      <div
        onTouchMove={(e) => {
          this.handleTouchMove(e);
        }}
        style={{ touchAction: 'pan - y' }}
      >
        Touch me with passive event
      </div>
    );
  }
}

export default PassiveTouchExample;

这里通过 style={{ touchAction: 'pan - y' }} 告诉浏览器只允许垂直方向的默认滚动行为,同时在 onTouchMove 事件处理中可以更高效地处理触摸移动逻辑。

手势防抖与节流

对于一些高频触发的手势事件,如 touchmove,可以使用防抖(Debounce)和节流(Throttle)技术。防抖是指在一定时间内如果事件再次触发,则重置定时器,只有在指定时间内没有再次触发事件时,才执行事件处理函数。节流则是指在一定时间间隔内,无论事件触发多少次,只执行一次事件处理函数。

  1. 防抖:可以通过自定义防抖函数来实现。
const debounce = (func, delay) => {
  let timer;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
};

class DebounceTouchExample extends Component {
  constructor(props) {
    super(props);
    this.handleTouchMoveDebounced = debounce(this.handleTouchMove, 200);
  }

  handleTouchMove = (event) => {
    console.log('Touch moved:', event.touches[0].clientX, event.touches[0].clientY);
  }

  render() {
    return (
      <div
        onTouchMove={this.handleTouchMoveDebounced}
      >
        Touch me with debounce
      </div>
    );
  }
}
  1. 节流:同样可以自定义节流函数。
const throttle = (func, delay) => {
  let lastTime = 0;
  return function() {
    const context = this;
    const args = arguments;
    const currentTime = new Date().getTime();
    if (currentTime - lastTime >= delay) {
      func.apply(context, args);
      lastTime = currentTime;
    }
  };
};

class ThrottleTouchExample extends Component {
  constructor(props) {
    super(props);
    this.handleTouchMoveThrottled = throttle(this.handleTouchMove, 200);
  }

  handleTouchMove = (event) => {
    console.log('Touch moved:', event.touches[0].clientX, event.touches[0].clientY);
  }

  render() {
    return (
      <div
        onTouchMove={this.handleTouchMoveThrottled}
      >
        Touch me with throttle
      </div>
    );
  }
}

通过防抖和节流,可以有效减少高频事件触发时的计算量,提升应用性能。

跨平台考虑

在开发 React 应用时,需要考虑触摸事件和手势识别在不同平台上的兼容性。

Web 与移动端浏览器差异

虽然大部分触摸事件在现代 Web 浏览器和移动端浏览器中表现相似,但仍存在一些差异。例如,一些移动端浏览器可能对手势有原生的处理,这可能与我们在 JavaScript 中实现的手势识别产生冲突。为了避免这种情况,需要合理设置 touchAction CSS 属性,明确告诉浏览器如何处理触摸操作。

.element {
  touch - action: pan - x pan - y;
}

上述代码表示允许元素在水平和垂直方向上进行平移操作,浏览器会根据这个设置来调整默认的触摸行为,避免与自定义的手势识别逻辑冲突。

React Native 与 React Web

如果项目同时涉及 React Native 和 React Web,手势识别的实现方式会有所不同。在 React Native 中,react - gesture - handler 库有专门针对原生平台的优化,并且事件处理与 React Web 略有不同。例如,在 React Native 中使用 PanResponder 来处理触摸事件,它提供了一套统一的接口来处理各种触摸相关的交互。

import React, { Component } from'react';
import { PanResponder, StyleSheet, View } from'react - native';

class RNPanExample extends Component {
  constructor(props) {
    super(props);
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onPanResponderMove: (evt, gestureState) => {
        console.log('Moving:', gestureState.dx, gestureState.dy);
      },
      onPanResponderRelease: (evt, gestureState) => {
        console.log('Released:', gestureState.dx, gestureState.dy);
      }
    });
  }

  render() {
    return (
      <View
        {...this._panResponder.panHandlers}
        style={styles.container}
      >
        Drag me in React Native
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF'
  }
});

export default RNPanExample;

而在 React Web 中,我们主要通过 HTML 元素的触摸事件来实现手势识别,如前文所述。因此,在跨平台开发时,需要根据不同的平台选择合适的实现方式,或者使用一些跨平台库来统一手势识别的逻辑。

通过深入理解 React 中的触摸事件处理和手势识别原理,合理使用第三方库,进行性能优化以及考虑跨平台兼容性,开发者可以打造出流畅、交互性强的 React 应用,满足不同用户在各种设备上的操作需求。无论是简单的点击操作还是复杂的多手势交互,都能够通过合适的技术手段实现并优化。