【HarmonyOS】React Native of HarmonyOS实战:手势组合与协同

🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

- [【HarmonyOS】React Native of HarmonyOS实战:手势组合与协同](#【HarmonyOS】React Native of HarmonyOS实战:手势组合与协同)
-
- 摘要
- 一、手势组合模式体系
-
- [1.1 组合模式分类](#1.1 组合模式分类)
- [1.2 模式特性对比](#1.2 模式特性对比)
- 二、手势组合框架
-
- [2.1 核心类型定义](#2.1 核心类型定义)
- [2.2 组合管理器实现](#2.2 组合管理器实现)
- [三、React 组件实现](#三、React 组件实现)
-
- [3.1 手势组合 Hook](#3.1 手势组合 Hook)
- [3.2 组合演示组件](#3.2 组合演示组件)
- [四、HarmonyOS 平台适配](#四、HarmonyOS 平台适配)
-
- [4.1 多点触控配置](#4.1 多点触控配置)
- [4.2 手势组合最佳实践](#4.2 手势组合最佳实践)
- 项目源码
摘要
本文系统阐述 React Native 在 HarmonyOS 平台上实现复杂手势组合的技术方案。通过分析手势协同的四种模式------并行、互斥、竞速、序列组合,提供一套可复用的手势组合框架。重点解决多手势场景下的状态同步、优先级管理和性能优化问题。
一、手势组合模式体系
1.1 组合模式分类
手势组合模式
│
┌───────────────┼───────────────┐
│ │ │
同时触发 优先触发 顺序执行
Simultaneous Exclusive Sequenced
│ │ │
┌───┴───┐ ┌───┴───┐ ┌───┴───┐
缩放 旋转 滑动 点击 长按 拖拽
双指操作 单指优先 时序依赖
1.2 模式特性对比
| 模式 | 特性 | 典型场景 | HarmonyOS 适配 |
|---|---|---|---|
| Simultaneous | 多手势同时激活 | 图片缩放+旋转 | 需启用多点触控 |
| Exclusive | 按优先级单一激活 | 列表滑动vs返回 | 配置系统手势避让 |
| Race | 先识别先执行 | 双击vs长按 | 调整时间阈值 |
| Sequenced | 按顺序组合触发 | 长按后拖拽 | 状态机管理 |
二、手势组合框架
2.1 核心类型定义
typescript
/**
* 手势基类
*/
abstract class BaseGesture {
abstract type: string;
abstract recognize(event: TouchEvent): GestureResult;
abstract reset(): void;
}
/**
* 手势识别结果
*/
interface GestureResult {
recognized: boolean;
confidence: number; // 识别置信度 0-1
data?: any;
}
/**
* 组合模式枚举
*/
enum CompositionMode {
SIMULTANEOUS = 'simultaneous', // 同时
EXCLUSIVE = 'exclusive', // 互斥
RACE = 'race', // 竞速
SEQUENCED = 'sequenced', // 序列
}
/**
* 手势组合配置
*/
interface GestureComposition {
mode: CompositionMode;
gestures: BaseGesture[];
priority?: number[]; // 各手势优先级
timeout?: number; // 竞速模式超时
}
2.2 组合管理器实现
typescript
/**
* 手势组合管理器
* 负责协调多个手势的识别与执行
*/
class GestureCompositionManager {
private compositions: Map<string, GestureComposition> = new Map();
private activeGestures: Set<string> = new Set();
/**
* 注册手势组合
*/
register(id: string, composition: GestureComposition): void {
this.compositions.set(id, composition);
}
/**
* 处理触摸事件
*/
handleTouchEvent(event: TouchEvent): Map<string, GestureResult> {
const results = new Map<string, GestureResult>();
for (const [id, composition] of this.compositions) {
const result = this.processComposition(id, composition, event);
if (result) {
results.set(id, result);
}
}
return results;
}
/**
* 处理单个手势组合
*/
private processComposition(
id: string,
composition: GestureComposition,
event: TouchEvent
): GestureResult | null {
switch (composition.mode) {
case CompositionMode.SIMULTANEOUS:
return this.processSimultaneous(composition, event);
case CompositionMode.EXCLUSIVE:
return this.processExclusive(id, composition, event);
case CompositionMode.RACE:
return this.processRace(composition, event);
case CompositionMode.SEQUENCED:
return this.processSequenced(composition, event);
default:
return null;
}
}
/**
* 同时模式:所有手势并行识别
*/
private processSimultaneous(
composition: GestureComposition,
event: TouchEvent
): GestureResult {
const results = composition.gestures.map(g => g.recognize(event));
const allRecognized = results.every(r => r.recognized);
return {
recognized: allRecognized,
confidence: results.reduce((sum, r) => sum + r.confidence, 0) / results.length,
data: results.map(r => r.data),
};
}
/**
* 互斥模式:按优先级选择
*/
private processExclusive(
id: string,
composition: GestureComposition,
event: TouchEvent
): GestureResult {
// 如果已有活跃手势,继续处理
if (this.activeGestures.has(id)) {
const activeIdx = this.getActiveGestureIndex(id);
if (activeIdx !== -1) {
return composition.gestures[activeIdx].recognize(event);
}
}
// 按优先级尝试识别
const priorities = composition.priority ?? composition.gestures.map((_, i) => i);
const sorted = composition.gestures
.map((g, i) => ({ gesture: g, priority: priorities[i] }))
.sort((a, b) => b.priority - a.priority);
for (const { gesture } of sorted) {
const result = gesture.recognize(event);
if (result.recognized) {
const idx = composition.gestures.indexOf(gesture);
this.activeGestures.add(`${id}:${idx}`);
return result;
}
}
return { recognized: false, confidence: 0 };
}
/**
* 竞速模式:先识别先得
*/
private processRace(
composition: GestureComposition,
event: TouchEvent
): GestureResult {
const startTime = Date.now();
for (const gesture of composition.gestures) {
const result = gesture.recognize(event);
if (result.recognized) {
// 检查超时
const elapsed = Date.now() - startTime;
if (composition.timeout && elapsed > composition.timeout) {
continue;
}
return result;
}
}
return { recognized: false, confidence: 0 };
}
/**
* 序列模式:按顺序依次识别
*/
private processSequenced(
composition: GestureComposition,
event: TouchEvent
): GestureResult {
const sequenceState = this.getSequenceState(composition);
const currentIndex = sequenceState.currentIndex ?? 0;
if (currentIndex >= composition.gestures.length) {
// 序列完成
return { recognized: true, confidence: 1, data: sequenceState.data };
}
const currentGesture = composition.gestures[currentIndex];
const result = currentGesture.recognize(event);
if (result.recognized) {
// 当前手势识别成功,进入下一个
sequenceState.currentIndex = currentIndex + 1;
sequenceState.data = sequenceState.data ?? [];
sequenceState.data.push(result.data);
// 检查是否完成整个序列
if (sequenceState.currentIndex >= composition.gestures.length) {
return { recognized: true, confidence: 1, data: sequenceState.data };
}
}
return { recognized: false, confidence: 0 };
}
private getActiveGestureIndex(id: string): number {
for (const active of this.activeGestures) {
if (active.startsWith(id)) {
return parseInt(active.split(':')[1]);
}
}
return -1;
}
private sequenceStates: Map<GestureComposition, any> = new Map();
private getSequenceState(composition: GestureComposition): any {
if (!this.sequenceStates.has(composition)) {
this.sequenceStates.set(composition, { currentIndex: 0, data: null });
}
return this.sequenceStates.get(composition)!;
}
/**
* 重置所有组合状态
*/
reset(): void {
this.activeGestures.clear();
this.sequenceStates.clear();
for (const composition of this.compositions.values()) {
for (const gesture of composition.gestures) {
gesture.reset();
}
}
}
}
三、React 组件实现
3.1 手势组合 Hook
typescript
import React, { useRef, useCallback, useMemo } from 'react';
import {
View,
StyleSheet,
Animated,
PanResponder,
Dimensions,
GestureResponderEvent,
PanResponderGestureState,
} from 'react-native';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
/**
* 手势组合 Hook
*/
function useGestureComposition() {
// 动画值
const translateX = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(0)).current;
const scale = useRef(new Animated.Value(1)).current;
const rotation = useRef(new Animated.Value(0)).current;
// 手势状态
const [activeMode, setActiveMode] = React.useState<CompositionMode | null>(null);
const [gestureLog, setGestureLog] = React.useState<string[]>([]);
const addLog = useCallback((message: string) => {
const timestamp = new Date().toLocaleTimeString();
setGestureLog(prev => [`[${timestamp}] ${message}`, ...prev.slice(0, 5)]);
}, []);
// Simultaneous 模式:缩放 + 旋转
const simultaneousGesture = useMemo(
() =>
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
setActiveMode(CompositionMode.SIMULTANEOUS);
addLog('Simultaneous: 开始双指手势');
scale.setOffset(scale._value || 1);
rotation.setOffset(rotation._value || 0);
scale.setValue(1);
rotation.setValue(0);
},
onPanResponderMove: (evt: GestureResponderEvent) => {
const touches = evt.nativeEvent.touches;
if (touches && touches.length === 2) {
const t1 = touches[0];
const t2 = touches[1];
// 计算距离(缩放)
const distance = Math.hypot(
t2.locationX - t1.locationX,
t2.locationY - t1.locationY
);
const initialDistance = 100; // 假设初始距离
scale.setValue(distance / initialDistance);
// 计算角度(旋转)
const angle = Math.atan2(
t2.locationY - t1.locationY,
t2.locationX - t1.locationX
) * (180 / Math.PI);
rotation.setValue(angle);
}
},
onPanResponderRelease: () => {
addLog('Simultaneous: 手势结束');
Animated.parallel([
Animated.spring(scale, { toValue: 1, useNativeDriver: true }),
Animated.spring(rotation, { toValue: 0, useNativeDriver: true }),
]).start(() => setActiveMode(null));
},
}),
[scale, rotation, addLog]
);
// Exclusive 模式:拖拽 优先于 点击
const exclusiveGesture = useMemo(
() =>
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: (_, gestureState) => {
// 移动超过阈值才视为拖拽
const moved = Math.abs(gestureState.dx) > 10 || Math.abs(gestureState.dy) > 10;
if (moved) {
setActiveMode(CompositionMode.EXCLUSIVE);
addLog('Exclusive: 拖拽优先');
}
return moved;
},
onPanResponderGrant: () => {
translateX.setOffset(translateX._value || 0);
translateY.setOffset(translateY._value || 0);
translateX.setValue(0);
translateY.setValue(0);
},
onPanResponderMove: Animated.event(
[null, { dx: translateX, dy: translateY }],
{ useNativeDriver: true }
),
onPanResponderRelease: () => {
addLog('Exclusive: 手势结束');
Animated.spring(translateX, { toValue: 0, useNativeDriver: true }).start();
Animated.spring(translateY, { toValue: 0, useNativeDriver: true }).start();
setActiveMode(null);
},
}),
[translateX, translateY, addLog]
);
// Race 模式:双击 vs 长按
const raceGesture = useMemo(
() => {
let tapCount = 0;
let longPressTimer: NodeJS.Timeout | null = null;
let lastTapTime = 0;
return PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
const now = Date.now();
// 检查双击
if (now - lastTapTime < 300) {
tapCount++;
if (tapCount === 2) {
setActiveMode(CompositionMode.RACE);
addLog('Race: 双击获胜!');
if (longPressTimer) clearTimeout(longPressTimer);
tapCount = 0;
return;
}
} else {
tapCount = 1;
}
lastTapTime = now;
// 设置长按定时器
longPressTimer = setTimeout(() => {
setActiveMode(CompositionMode.RACE);
addLog('Race: 长按获胜!');
tapCount = 0;
}, 500);
},
onPanResponderRelease: () => {
if (longPressTimer) clearTimeout(longPressTimer);
},
onPanResponderTerminate: () => {
if (longPressTimer) clearTimeout(longPressTimer);
tapCount = 0;
},
});
},
[addLog]
);
// Sequenced 模式:长按 → 拖拽
const sequencedGesture = useMemo(
() => {
let sequenceState: 'idle' | 'longPress' | 'dragging' = 'idle';
let longPressTimer: NodeJS.Timeout | null = null;
return PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
sequenceState = 'idle';
addLog('Sequenced: 等待长按...');
longPressTimer = setTimeout(() => {
sequenceState = 'longPress';
addLog('Sequenced: 长按确认,可以拖拽');
}, 500);
translateX.setOffset(translateX._value || 0);
translateY.setOffset(translateY._value || 0);
translateX.setValue(0);
translateY.setValue(0);
},
onMoveShouldSetPanResponder: () => {
const canDrag = sequenceState === 'longPress';
if (canDrag) {
sequenceState = 'dragging';
setActiveMode(CompositionMode.SEQUENCED);
addLog('Sequenced: 拖拽开始');
}
return canDrag;
},
onPanResponderMove: Animated.event(
[null, { dx: translateX, dy: translateY }],
{ useNativeDriver: true }
),
onPanResponderRelease: () => {
if (longPressTimer) clearTimeout(longPressTimer);
addLog(`Sequenced: 序列结束 (${sequenceState})`);
Animated.spring(translateX, { toValue: 0, useNativeDriver: true }).start();
Animated.spring(translateY, { toValue: 0, useNativeDriver: true }).start();
sequenceState = 'idle';
setActiveMode(null);
},
});
},
[translateX, translateY, addLog]
);
return {
translateX,
translateY,
scale,
rotation,
simultaneousGesture,
exclusiveGesture,
raceGesture,
sequencedGesture,
activeMode,
gestureLog,
addLog,
};
}
3.2 组合演示组件
typescript
/**
* 手势组合演示主组件
*/
const GestureCompositionDemo: React.FC = () => {
const {
translateX,
translateY,
scale,
rotation,
simultaneousGesture,
exclusiveGesture,
raceGesture,
sequencedGesture,
activeMode,
gestureLog,
} = useGestureComposition();
const modeConfigs = [
{
mode: CompositionMode.SIMULTANEOUS,
name: 'Simultaneous',
desc: '缩放 + 旋转同时进行',
icon: '🤏',
color: '#4CAF50',
gesture: simultaneousGesture,
},
{
mode: CompositionMode.EXCLUSIVE,
name: 'Exclusive',
desc: '拖拽优先于点击',
icon: '🎯',
color: '#2196F3',
gesture: exclusiveGesture,
},
{
mode: CompositionMode.RACE,
name: 'Race',
desc: '双击 vs 长按竞速',
icon: '🏁',
color: '#FF9800',
gesture: raceGesture,
},
{
mode: CompositionMode.SEQUENCED,
name: 'Sequenced',
desc: '长按 → 拖拽序列',
icon: '🔗',
color: '#9C27B0',
gesture: sequencedGesture,
},
];
return (
<View style={styles.container}>
{/* 演示区域 */}
{modeConfigs.map((config) => (
<View key={config.mode} style={styles.demoSection}>
<View style={styles.modeHeader}>
<Text style={styles.modeIcon}>{config.icon}</Text>
<View style={styles.modeInfo}>
<Text style={styles.modeName}>{config.name}</Text>
<Text style={styles.modeDesc}>{config.desc}</Text>
</View>
<View
style={[
styles.modeIndicator,
{
backgroundColor:
activeMode === config.mode ? config.color : '#e0e0e0',
},
]}
/>
</View>
<Animated.View
{...config.gesture.panHandlers}
style={[
styles.demoBox,
{
transform:
config.mode === CompositionMode.SIMULTANEOUS
? [
{ scale: scale },
{
rotate: rotation.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
}),
},
]
: [{ translateX: translateX }, { translateY: translateY }],
},
]}
>
<Text style={styles.demoText}>{config.icon}</Text>
</Animated.View>
</View>
))}
{/* 日志区域 */}
{gestureLog.length > 0 && (
<View style={styles.logContainer}>
<Text style={styles.logTitle}>事件日志</Text>
{gestureLog.map((log, i) => (
<Text key={i} style={styles.logText}>{log}</Text>
))}
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 16,
},
demoSection: {
marginBottom: 20,
},
modeHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
modeIcon: {
fontSize: 28,
marginRight: 12,
},
modeInfo: {
flex: 1,
},
modeName: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
},
modeDesc: {
fontSize: 12,
color: '#666',
},
modeIndicator: {
width: 12,
height: 12,
borderRadius: 6,
},
demoBox: {
width: '100%',
height: 100,
backgroundColor: '#fff',
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
},
demoText: {
fontSize: 40,
},
logContainer: {
backgroundColor: '#263238',
borderRadius: 12,
padding: 12,
marginTop: 8,
},
logTitle: {
color: '#80CBC4',
fontSize: 12,
fontWeight: 'bold',
marginBottom: 8,
},
logText: {
color: '#80CBC4',
fontSize: 11,
fontFamily: 'monospace',
marginBottom: 4,
},
});
export default GestureCompositionDemo;
四、HarmonyOS 平台适配
4.1 多点触控配置
json5
// module.json5
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"multimodalInput": {
"maxPointers": 5,
"enableMultiFinger": true
}
}
]
}
}
4.2 手势组合最佳实践
| 场景 | 推荐模式 | 配置要点 |
|---|---|---|
| 图片编辑 | Simultaneous | 启用双指触控 |
| 列表操作 | Exclusive | 设置 minDist 阈值 |
| 快捷操作 | Race | 调整 timeout 值 |
| 精确控制 | Sequenced | 状态机管理 |
项目源码
完整项目:https://atomgit.com/lbbxmx111/AtomGitNewsDemo
鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
📕个人领域 :Linux/C++/java/AI
🚀 个人主页 :有点流鼻涕 · CSDN
💬 座右铭 : "向光而行,沐光而生。"
