React 触摸事件处理与手势识别
React 触摸事件基础
在 React 应用中,触摸事件是与移动设备交互的重要组成部分。React 提供了一系列与触摸相关的事件,这些事件与传统的鼠标事件有一些相似之处,但也存在一些差异,因为触摸交互是基于手指与屏幕的直接接触。
触摸事件类型
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]
可以获取到第一个触摸点的坐标信息。
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()
是为了阻止浏览器的默认滚动行为,避免在触摸移动时页面发生不必要的滚动。
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;
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
对象包含了丰富的信息,用于处理触摸相关的逻辑。
touches
:这是一个TouchList
对象,包含了当前屏幕上所有触摸点的信息。每个触摸点都是一个Touch
对象,具有clientX
、clientY
等属性,分别表示触摸点在视口内的横坐标和纵坐标。targetTouches
:同样是一个TouchList
对象,但它只包含了当前事件目标元素上的触摸点信息。这在处理复杂布局中的触摸事件时非常有用,比如在一个包含多个子元素的父元素上监听触摸事件,通过targetTouches
可以准确获取作用在特定子元素上的触摸点。changedTouches
:该TouchList
对象记录了在本次事件中状态发生改变的触摸点。例如,在touchstart
事件中,changedTouches
包含了新开始触摸的点;在touchmove
事件中,它包含了移动的点;在touchend
事件中,它包含了离开屏幕的点。
手势识别原理
手势识别是在触摸事件的基础上,通过分析触摸点的位置、数量、移动轨迹等信息,来判断用户执行了何种手势操作,如点击、长按、拖动、缩放、旋转等。
点击手势识别
点击手势可以通过 touchstart
和 touchend
事件的组合来识别。基本思路是在 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
- 安装与基本使用:
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;
- 高级手势组合:
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
- 引入与初始化:
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;
- 自定义手势与配置:
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)技术。防抖是指在一定时间内如果事件再次触发,则重置定时器,只有在指定时间内没有再次触发事件时,才执行事件处理函数。节流则是指在一定时间间隔内,无论事件触发多少次,只执行一次事件处理函数。
- 防抖:可以通过自定义防抖函数来实现。
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>
);
}
}
- 节流:同样可以自定义节流函数。
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 应用,满足不同用户在各种设备上的操作需求。无论是简单的点击操作还是复杂的多手势交互,都能够通过合适的技术手段实现并优化。