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

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

📋 前言

在移动应用交互设计中,单一手势往往无法满足复杂的用户需求 。想象一下图片编辑应用:用户需要同时支持拖拽移动、双指缩放、双指旋转,甚至长按弹出菜单------这些手势需要协同工作而非相互冲突。

随着HarmonyOS 6.0的发布(2026年1月),手势组合与协同能力得到了显著增强。本文将深入探讨React Native在HarmonyOS平台上的手势组合与协同实战方案,帮助你构建流畅、自然的多手势交互体验。


一、技术背景与核心概念

1.1 为什么需要手势组合?

场景 单一手势局限 组合手势优势
图片编辑器 只能缩放或只能移动 同时支持拖拽+缩放+旋转
地图应用 滑动与缩放冲突 智能识别用户意图
游戏控制 多指操作无法区分 多指协同精确控制
文档阅读 翻页与标注冲突 长按标注+滑动翻页

1.2 HarmonyOS手势组合三种模式

HarmonyOS通过GestureGroup支持三种手势组合模式:

typescript 复制代码
// HarmonyOS原生手势组合模式
GestureGroup(
  mode: GestureMode,  // 组合模式
  gesture: GestureType[]  // 手势数组
)
模式 枚举值 行为特征 适用场景
顺序识别 Sequence 手势按注册顺序依次识别 长按后拖拽、双击后缩放
并行识别 Race 多个手势同时识别,优先级高的优先响应 拖拽与缩放共存
互斥识别 Exclusive 同一时间只允许一个手势生效 滑动删除vs点击进入

1.3 React Native手势协同架构

复制代码
┌─────────────────────────────────────────────────────────┐
│                    React Native JS Layer                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐  │
│  │ PanResponder│  │GestureHandler│  │  Custom Hook   │  │
│  └─────────────┘  └─────────────┘  └─────────────────┘  │
├─────────────────────────────────────────────────────────┤
│              Native Bridge Layer (ArkTS)                 │
│  ┌─────────────────────────────────────────────────────┐│
│  │           HarmonyOS Gesture Coordination            ││
│  │  ┌──────────┐  ┌──────────┐  ┌──────────────────┐  ││
│  │  │GestureGroup│ │PriorityMgr│ │ ConflictResolver │  ││
│  │  └──────────┘  └──────────┘  └──────────────────┘  ││
│  └─────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────┤
│              HarmonyOS Gesture Engine                    │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐│
│  │  Tap     │  │  Pan     │  │  Pinch   │  │ Rotate   ││
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘│
└─────────────────────────────────────────────────────────┘

二、环境搭建与依赖配置

2.1 开发环境要求

bash 复制代码
# Node.js版本
node >= 18.0.0

# 鸿蒙开发工具
DevEco Studio >= 6.0.0

# React Native for OpenHarmony
@ohos/react-native-arkui >= 0.15.0
@ohos/harmony-gesture-system >= 1.0.0

2.2 项目初始化

bash 复制代码
# 创建React Native项目
npx react-native init HarmonyGestureCombo --template @ohos/react-native-template

# 安装手势处理核心依赖
npm install react-native-gesture-handler
npm install @react-native-oh/react-native-harmony

# 安装手势冲突解决库
npm install @ohos/harmony-gesture-system

# 安装动画库(用于手势反馈)
npm install react-native-reanimated

2.3 项目结构建议

复制代码
HarmonyGestureCombo/
├── src/
│   ├── components/           # 可复用组件
│   │   ├── GestureCanvas/    # 手势画布组件
│   │   ├── MultiTouchView/   # 多触控视图
│   │   └── GestureFeedback/  # 手势反馈组件
│   ├── gestures/             # 手势处理模块
│   │   ├── GestureCombiner/  # 手势组合器
│   │   ├── ConflictResolver/ # 冲突解决器
│   │   └── PriorityManager/  # 优先级管理器
│   ├── hooks/                # 自定义Hooks
│   │   ├── useGestureCombo/  # 组合手势Hook
│   │   └── useGestureSync/   # 手势同步Hook
│   ├── screens/              # 页面组件
│   └── utils/                # 工具函数
├── native/                   # 鸿蒙原生模块
│   └── GestureBridge.ets     # 手势桥接层
├── App.tsx
└── index.js

三、基础手势组合实现

3.1 使用GestureCombiner实现双模式组合

typescript 复制代码
// src/gestures/GestureCombiner/index.ts
import { PanResponder, GestureResponderEvent } from 'react-native';
import { Platform } from 'react-native';

export enum GestureComboMode {
  SEQUENCE = 'sequence',    // 顺序识别
  RACE = 'race',           // 并行识别
  EXCLUSIVE = 'exclusive',  // 互斥识别
}

export interface GestureConfig {
  id: string;
  type: 'tap' | 'pan' | 'pinch' | 'rotate' | 'longpress';
  priority: number;
  enabled: boolean;
  threshold?: number;
}

export interface GestureComboConfig {
  mode: GestureComboMode;
  gestures: GestureConfig[];
  onGestureStart?: (gestureId: string) => void;
  onGestureUpdate?: (gestureId: string, data: any) => void;
  onGestureEnd?: (gestureId: string) => void;
  onConflict?: (winner: string, losers: string[]) => void;
}

export class GestureCombiner {
  private config: GestureComboConfig;
  private activeGestures: Map<string, boolean> = new Map();
  private gestureHistory: Map<string, Array<{ timestamp: number; data: any }>> = new Map();
  private isHarmonyOS: boolean;

  constructor(config: GestureComboConfig) {
    this.config = config;
    this.isHarmonyOS = Platform.OS === 'harmony';
    this.initializeGestureHistory();
  }

  private initializeGestureHistory() {
    this.config.gestures.forEach((gesture) => {
      this.gestureHistory.set(gesture.id, []);
      this.activeGestures.set(gesture.id, false);
    });
  }

  // 创建组合手势响应器
  createPanResponder(): PanResponder {
    return PanResponder.create({
      onStartShouldSetPanResponder: (_, gestureState) => {
        return this.shouldStartGesture(gestureState);
      },

      onMoveShouldSetPanResponder: (_, gestureState) => {
        return this.shouldMoveGesture(gestureState);
      },

      onPanResponderGrant: (_, gestureState) => {
        this.handleGestureStart(gestureState);
      },

      onPanResponderMove: (_, gestureState) => {
        this.handleGestureMove(gestureState);
      },

      onPanResponderRelease: (_, gestureState) => {
        this.handleGestureEnd(gestureState);
      },

      onPanResponderTerminate: () => {
        this.handleGestureTerminate();
      },

      onPanResponderReject: () => {
        this.handleGestureReject();
      },
    });
  }

  private shouldStartGesture(gestureState: GestureResponderEvent): boolean {
    const enabledGestures = this.config.gestures.filter((g) => g.enabled);
    
    switch (this.config.mode) {
      case GestureComboMode.EXCLUSIVE:
        // 互斥模式:只允许一个手势开始
        return !Array.from(this.activeGestures.values()).some((active) => active);
      
      case GestureComboMode.RACE:
        // 并行模式:优先级高的优先
        return enabledGestures.length > 0;
      
      case GestureComboMode.SEQUENCE:
      default:
        // 顺序模式:按注册顺序
        return enabledGestures[0]?.enabled ?? false;
    }
  }

  private shouldMoveGesture(gestureState: GestureResponderEvent): boolean {
    const threshold = this.config.gestures[0]?.threshold ?? 5;
    return (
      Math.abs(gestureState.dx) > threshold ||
      Math.abs(gestureState.dy) > threshold
    );
  }

  private handleGestureStart(gestureState: GestureResponderEvent) {
    const winner = this.resolveConflict('start', gestureState);
    
    if (winner) {
      this.activeGestures.set(winner, true);
      this.config.onGestureStart?.(winner);
      
      // 记录手势历史
      this.recordGestureHistory(winner, {
        type: 'start',
        x: gestureState.x0,
        y: gestureState.y0,
        timestamp: Date.now(),
      });
    }
  }

  private handleGestureMove(gestureState: GestureResponderEvent) {
    const activeGesture = Array.from(this.activeGestures.entries()).find(
      ([, active]) => active
    )?.[0];

    if (activeGesture) {
      this.config.onGestureUpdate?.(activeGesture, {
        dx: gestureState.dx,
        dy: gestureState.dy,
        vx: gestureState.vx,
        vy: gestureState.vy,
        x: gestureState.moveX,
        y: gestureState.moveY,
      });

      this.recordGestureHistory(activeGesture, {
        type: 'move',
        dx: gestureState.dx,
        dy: gestureState.dy,
        timestamp: Date.now(),
      });
    }
  }

  private handleGestureEnd(gestureState: GestureResponderEvent) {
    const activeGesture = Array.from(this.activeGestures.entries()).find(
      ([, active]) => active
    )?.[0];

    if (activeGesture) {
      this.config.onGestureEnd?.(activeGesture);
      this.activeGestures.set(activeGesture, false);

      this.recordGestureHistory(activeGesture, {
        type: 'end',
        dx: gestureState.dx,
        dy: gestureState.dy,
        timestamp: Date.now(),
      });
    }
  }

  private handleGestureTerminate() {
    this.activeGestures.forEach((_, key) => {
      this.activeGestures.set(key, false);
    });
  }

  private handleGestureReject() {
    this.handleGestureTerminate();
  }

  // 冲突解决核心逻辑
  private resolveConflict(
    phase: 'start' | 'move' | 'end',
    gestureState: GestureResponderEvent
  ): string | null {
    const enabledGestures = this.config.gestures
      .filter((g) => g.enabled)
      .sort((a, b) => b.priority - a.priority);

    if (enabledGestures.length === 0) return null;

    switch (this.config.mode) {
      case GestureComboMode.EXCLUSIVE:
        // 互斥模式:返回优先级最高的
        return enabledGestures[0].id;

      case GestureComboMode.RACE:
        // 并行模式:根据手势特征判断
        return this.raceResolution(enabledGestures, gestureState);

      case GestureComboMode.SEQUENCE:
      default:
        // 顺序模式:按注册顺序
        return enabledGestures[0].id;
    }
  }

  private raceResolution(
    gestures: GestureConfig[],
    gestureState: GestureResponderEvent
  ): string {
    // 根据移动距离和速度判断手势类型
    const distance = Math.sqrt(
      gestureState.dx * gestureState.dx + gestureState.dy * gestureState.dy
    );
    const velocity = Math.sqrt(
      gestureState.vx * gestureState.vx + gestureState.vy * gestureState.vy
    );

    // 快速移动优先识别为拖拽
    if (velocity > 0.5) {
      const panGesture = gestures.find((g) => g.type === 'pan');
      if (panGesture) return panGesture.id;
    }

    // 小范围移动优先识别为点击
    if (distance < 10) {
      const tapGesture = gestures.find((g) => g.type === 'tap');
      if (tapGesture) return tapGesture.id;
    }

    // 默认返回优先级最高的
    return gestures[0].id;
  }

  private recordGestureHistory(gestureId: string, data: any) {
    const history = this.gestureHistory.get(gestureId) || [];
    history.push(data);
    
    // 保留最近50条记录
    if (history.length > 50) {
      history.shift();
    }
    this.gestureHistory.set(gestureId, history);
  }

  // 获取手势历史
  getGestureHistory(gestureId: string) {
    return this.gestureHistory.get(gestureId) || [];
  }

  // 启用/禁用特定手势
  setGestureEnabled(gestureId: string, enabled: boolean) {
    const gesture = this.config.gestures.find((g) => g.id === gestureId);
    if (gesture) {
      gesture.enabled = enabled;
    }
  }

  // 更新手势优先级
  setGesturePriority(gestureId: string, priority: number) {
    const gesture = this.config.gestures.find((g) => g.id === gestureId);
    if (gesture) {
      gesture.priority = priority;
    }
  }
}

3.2 使用Hook封装组合手势

typescript 复制代码
// src/hooks/useGestureCombo.ts
import { useState, useCallback, useEffect, useRef } from 'react';
import { PanResponder } from 'react-native';
import {
  GestureCombiner,
  GestureComboMode,
  GestureConfig,
  GestureComboConfig,
} from '../gestures/GestureCombiner';

export interface UseGestureComboOptions {
  mode: GestureComboMode;
  initialGestures: GestureConfig[];
  onGestureStart?: (gestureId: string) => void;
  onGestureUpdate?: (gestureId: string, data: any) => void;
  onGestureEnd?: (gestureId: string) => void;
  onConflict?: (winner: string, losers: string[]) => void;
}

export interface UseGestureComboReturn {
  panResponder: PanResponder;
  activeGesture: string | null;
  gestureStates: Record<string, any>;
  enableGesture: (gestureId: string) => void;
  disableGesture: (gestureId: string) => void;
  setGesturePriority: (gestureId: string, priority: number) => void;
  resetGestures: () => void;
}

export const useGestureCombo = (
  options: UseGestureComboOptions
): UseGestureComboReturn => {
  const [activeGesture, setActiveGesture] = useState<string | null>(null);
  const [gestureStates, setGestureStates] = useState<Record<string, any>>({});
  const combinerRef = useRef<GestureCombiner | null>(null);

  // 初始化组合器
  useEffect(() => {
    const config: GestureComboConfig = {
      mode: options.mode,
      gestures: options.initialGestures,
      onGestureStart: (gestureId) => {
        setActiveGesture(gestureId);
        options.onGestureStart?.(gestureId);
      },
      onGestureUpdate: (gestureId, data) => {
        setGestureStates((prev) => ({
          ...prev,
          [gestureId]: { ...prev[gestureId], ...data },
        }));
        options.onGestureUpdate?.(gestureId, data);
      },
      onGestureEnd: (gestureId) => {
        if (activeGesture === gestureId) {
          setActiveGesture(null);
        }
        options.onGestureEnd?.(gestureId);
      },
      onConflict: options.onConflict,
    };

    combinerRef.current = new GestureCombiner(config);

    return () => {
      combinerRef.current = null;
    };
  }, [options]);

  const panResponder = useCallback(() => {
    if (combinerRef.current) {
      return combinerRef.current.createPanResponder();
    }
    return PanResponder.create({});
  }, []);

  const enableGesture = useCallback((gestureId: string) => {
    combinerRef.current?.setGestureEnabled(gestureId, true);
  }, []);

  const disableGesture = useCallback((gestureId: string) => {
    combinerRef.current?.setGestureEnabled(gestureId, false);
  }, []);

  const setGesturePriority = useCallback(
    (gestureId: string, priority: number) => {
      combinerRef.current?.setGesturePriority(gestureId, priority);
    },
    []
  );

  const resetGestures = useCallback(() => {
    setActiveGesture(null);
    setGestureStates({});
    options.initialGestures.forEach((g) => {
      combinerRef.current?.setGestureEnabled(g.id, g.enabled);
    });
  }, [options.initialGestures]);

  return {
    panResponder: panResponder(),
    activeGesture,
    gestureStates,
    enableGesture,
    disableGesture,
    setGesturePriority,
    resetGestures,
  };
};

四、实战案例:图片编辑器多手势协同

4.1 完整组件实现

typescript 复制代码
// src/components/ImageEditor/GestureImageEditor.tsx
import React, { useRef, useState, useCallback } from 'react';
import {
  View,
  StyleSheet,
  Image,
  Animated,
  Dimensions,
  Text,
} from 'react-native';
import { useGestureCombo, GestureComboMode } from '../../hooks/useGestureCombo';
import { GestureFeedback } from '../GestureFeedback';

const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');

interface GestureImageEditorProps {
  imageUrl: string;
  onTransformChange?: (transform: TransformState) => void;
}

interface TransformState {
  x: number;
  y: number;
  scale: number;
  rotation: number;
}

export const GestureImageEditor: React.FC<GestureImageEditorProps> = ({
  imageUrl,
  onTransformChange,
}) => {
  // 变换状态
  const [transform, setTransform] = useState<TransformState>({
    x: 0,
    y: 0,
    scale: 1,
    rotation: 0,
  });

  // Animated值
  const panX = useRef(new Animated.Value(0)).current;
  const panY = useRef(new Animated.Value(0)).current;
  const scale = useRef(new Animated.Value(1)).current;
  const rotation = useRef(new Animated.Value(0)).current;

  // 手势状态显示
  const [gestureInfo, setGestureInfo] = useState('');

  // 配置组合手势
  const { panResponder, activeGesture, gestureStates, resetGestures } =
    useGestureCombo({
      mode: GestureComboMode.RACE, // 并行模式,支持多手势同时识别
      initialGestures: [
        {
          id: 'pan',
          type: 'pan',
          priority: 3,
          enabled: true,
          threshold: 5,
        },
        {
          id: 'pinch',
          type: 'pinch',
          priority: 2,
          enabled: true,
          threshold: 10,
        },
        {
          id: 'rotate',
          type: 'rotate',
          priority: 2,
          enabled: true,
          threshold: 5,
        },
        {
          id: 'longpress',
          type: 'longpress',
          priority: 1,
          enabled: true,
          threshold: 500, // 毫秒
        },
      ],
      onGestureStart: (gestureId) => {
        setGestureInfo(`手势开始: ${gestureId}`);
      },
      onGestureUpdate: (gestureId, data) => {
        setGestureInfo(
          `手势更新: ${gestureId} | dx: ${data.dx?.toFixed(2)} | dy: ${data.dy?.toFixed(2)}`
        );

        // 根据手势类型更新变换
        if (gestureId === 'pan') {
          panX.setValue(data.dx || 0);
          panY.setValue(data.dy || 0);
        } else if (gestureId === 'pinch') {
          // 模拟捏合缩放(实际项目中需要多指支持)
          const newScale = Math.max(0.5, Math.min(3, 1 + (data.dx || 0) * 0.01));
          scale.setValue(newScale);
        } else if (gestureId === 'rotate') {
          // 模拟旋转(实际项目中需要角度计算)
          const newRotation = (data.dx || 0) * 0.5;
          rotation.setValue(newRotation);
        }

        // 通知父组件
        onTransformChange?.({
          x: data.dx || 0,
          y: data.dy || 0,
          scale: scale._value,
          rotation: rotation._value,
        });
      },
      onGestureEnd: (gestureId) => {
        setGestureInfo(`手势结束: ${gestureId}`);
        
        // 手势结束后添加弹性动画
        Animated.spring(panX, {
          toValue: 0,
          useNativeDriver: true,
        }).start();
        Animated.spring(panY, {
          toValue: 0,
          useNativeDriver: true,
        }).start();
      },
      onConflict: (winner, losers) => {
        console.log(`手势冲突解决: 获胜者=${winner}, 失败者=${losers.join(', ')}`);
      },
    });

  // 双击重置
  const handleDoubleTap = useCallback(() => {
    resetGestures();
    Animated.parallel([
      Animated.spring(panX, { toValue: 0, useNativeDriver: true }),
      Animated.spring(panY, { toValue: 0, useNativeDriver: true }),
      Animated.spring(scale, { toValue: 1, useNativeDriver: true }),
      Animated.spring(rotation, { toValue: 0, useNativeDriver: true }),
    ]).start();
    setTransform({ x: 0, y: 0, scale: 1, rotation: 0 });
  }, [panX, panY, scale, rotation, resetGestures]);

  return (
    <View style={styles.container}>
      {/* 手势画布 */}
      <Animated.View
        {...panResponder.panHandlers}
        style={styles.gestureCanvas}
      >
        {/* 图片容器 */}
        <Animated.View
          style={[
            styles.imageContainer,
            {
              transform: [
                { translateX: panX },
                { translateY: panY },
                { scale },
                { rotate: rotation.interpolate({
                  inputRange: [-180, 180],
                  outputRange: ['-180deg', '180deg'],
                }) },
              ],
            },
          ]}
        >
          <Image source={{ uri: imageUrl }} style={styles.image} />
        </Animated.View>

        {/* 手势反馈层 */}
        <GestureFeedback
          activeGesture={activeGesture}
          gestureStates={gestureStates}
        />
      </Animated.View>

      {/* 信息面板 */}
      <View style={styles.infoPanel}>
        <Text style={styles.infoText}>当前手势: {activeGesture || '无'}</Text>
        <Text style={styles.infoText}>{gestureInfo}</Text>
        <Text style={styles.infoText}>
          变换: x={transform.x.toFixed(1)}, y={transform.y.toFixed(1)}, 
          scale={transform.scale.toFixed(2)}, rotation={transform.rotation.toFixed(1)}°
        </Text>
      </View>

      {/* 控制按钮 */}
      <View style={styles.controlPanel}>
        <Text style={styles.controlText}>双击重置 | 拖拽移动 | 捏合缩放</Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#1a1a2e',
  },
  gestureCanvas: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  imageContainer: {
    width: 300,
    height: 300,
    justifyContent: 'center',
    alignItems: 'center',
  },
  image: {
    width: '100%',
    height: '100%',
    resizeMode: 'contain',
  },
  infoPanel: {
    position: 'absolute',
    top: 50,
    left: 20,
    right: 20,
    backgroundColor: 'rgba(0, 0, 0, 0.7)',
    padding: 15,
    borderRadius: 10,
  },
  infoText: {
    color: '#fff',
    fontSize: 14,
    marginBottom: 5,
  },
  controlPanel: {
    position: 'absolute',
    bottom: 30,
    left: 20,
    right: 20,
    backgroundColor: 'rgba(255, 255, 255, 0.9)',
    padding: 15,
    borderRadius: 10,
    alignItems: 'center',
  },
  controlText: {
    color: '#333',
    fontSize: 14,
    fontWeight: '500',
  },
});

4.2 手势反馈组件

typescript 复制代码
// src/components/GestureFeedback/index.tsx
import React from 'react';
import { View, Text, StyleSheet, Animated } from 'react-native';

interface GestureFeedbackProps {
  activeGesture: string | null;
  gestureStates: Record<string, any>;
}

export const GestureFeedback: React.FC<GestureFeedbackProps> = ({
  activeGesture,
  gestureStates,
}) => {
  const opacity = React.useRef(new Animated.Value(0)).current;

  React.useEffect(() => {
    if (activeGesture) {
      Animated.sequence([
        Animated.timing(opacity, {
          toValue: 1,
          duration: 150,
          useNativeDriver: true,
        }),
        Animated.delay(2000),
        Animated.timing(opacity, {
          toValue: 0,
          duration: 300,
          useNativeDriver: true,
        }),
      ]).start();
    }
  }, [activeGesture, opacity]);

  if (!activeGesture) return null;

  return (
    <Animated.View style={[styles.feedbackContainer, { opacity }]}>
      <View style={styles.feedbackBubble}>
        <Text style={styles.feedbackIcon}>
          {getGestureIcon(activeGesture)}
        </Text>
        <Text style={styles.feedbackText}>{getGestureName(activeGesture)}</Text>
      </View>
    </Animated.View>
  );
};

const getGestureIcon = (gesture: string): string => {
  const icons: Record<string, string> = {
    pan: '✋',
    pinch: '🤏',
    rotate: '🔄',
    longpress: '⏱️',
    tap: '👆',
  };
  return icons[gesture] || '❓';
};

const getGestureName = (gesture: string): string => {
  const names: Record<string, string> = {
    pan: '拖拽',
    pinch: '缩放',
    rotate: '旋转',
    longpress: '长按',
    tap: '点击',
  };
  return names[gesture] || gesture;
};

const styles = StyleSheet.create({
  feedbackContainer: {
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: [{ translateX: -75 }, { translateY: -75 }],
    zIndex: 100,
  },
  feedbackBubble: {
    backgroundColor: 'rgba(0, 0, 0, 0.8)',
    paddingHorizontal: 20,
    paddingVertical: 12,
    borderRadius: 25,
    flexDirection: 'row',
    alignItems: 'center',
  },
  feedbackIcon: {
    fontSize: 20,
    marginRight: 8,
  },
  feedbackText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

五、高级手势协同场景

5.1 多指协同手势

typescript 复制代码
// src/gestures/MultiTouchGesture/index.ts
import { useState, useCallback, useRef } from 'react';
import { PanResponder, GestureResponderEvent } from 'react-native';

interface TouchPoint {
  identifier: number;
  x: number;
  y: number;
  startTime: number;
}

interface MultiTouchState {
  activeTouches: Map<number, TouchPoint>;
  centerPoint: { x: number; y: number };
  distance: number;
  angle: number;
}

export const useMultiTouchGesture = () => {
  const [multiTouchState, setMultiTouchState] = useState<MultiTouchState>({
    activeTouches: new Map(),
    centerPoint: { x: 0, y: 0 },
    distance: 0,
    angle: 0,
  });

  const touchHistory = useRef<Map<number, TouchPoint[]>>(new Map());

  // 计算两点中心
  const calculateCenter = (
    touch1: TouchPoint,
    touch2: TouchPoint
  ): { x: number; y: number } => ({
    x: (touch1.x + touch2.x) / 2,
    y: (touch1.y + touch2.y) / 2,
  });

  // 计算两点距离
  const calculateDistance = (
    touch1: TouchPoint,
    touch2: TouchPoint
  ): number => {
    const dx = touch2.x - touch1.x;
    const dy = touch2.y - touch1.y;
    return Math.sqrt(dx * dx + dy * dy);
  };

  // 计算两点角度
  const calculateAngle = (
    touch1: TouchPoint,
    touch2: TouchPoint
  ): number => {
    const dx = touch2.x - touch1.x;
    const dy = touch2.y - touch1.y;
    return (Math.atan2(dy, dx) * 180) / Math.PI;
  };

  const panResponder = useCallback(
    PanResponder.create({
      onStartShouldSetPanResponder: (_, gestureState) => {
        // 支持多指开始
        return gestureState.numberActiveTouches >= 1;
      },

      onMoveShouldSetPanResponder: (_, gestureState) => {
        return gestureState.numberActiveTouches >= 1;
      },

      onPanResponderGrant: (_, gestureState) => {
        const newTouches = new Map<number, TouchPoint>();
        
        gestureState.touches.forEach((touch) => {
          const touchPoint: TouchPoint = {
            identifier: touch.identifier,
            x: touch.locationX,
            y: touch.locationY,
            startTime: Date.now(),
          };
          newTouches.set(touch.identifier, touchPoint);
          
          // 记录历史
          const history = touchHistory.current.get(touch.identifier) || [];
          history.push(touchPoint);
          touchHistory.current.set(touch.identifier, history);
        });

        // 计算初始状态
        const touchesArray = Array.from(newTouches.values());
        let centerPoint = { x: 0, y: 0 };
        let distance = 0;
        let angle = 0;

        if (touchesArray.length >= 2) {
          centerPoint = calculateCenter(touchesArray[0], touchesArray[1]);
          distance = calculateDistance(touchesArray[0], touchesArray[1]);
          angle = calculateAngle(touchesArray[0], touchesArray[1]);
        } else if (touchesArray.length === 1) {
          centerPoint = {
            x: touchesArray[0].x,
            y: touchesArray[0].y,
          };
        }

        setMultiTouchState({
          activeTouches: newTouches,
          centerPoint,
          distance,
          angle,
        });
      },

      onPanResponderMove: (_, gestureState) => {
        const updatedTouches = new Map<number, TouchPoint>();
        
        gestureState.touches.forEach((touch) => {
          const existingTouch = multiTouchState.activeTouches.get(
            touch.identifier
          );
          const touchPoint: TouchPoint = existingTouch
            ? { ...existingTouch, x: touch.locationX, y: touch.locationY }
            : {
                identifier: touch.identifier,
                x: touch.locationX,
                y: touch.locationY,
                startTime: Date.now(),
              };
          updatedTouches.set(touch.identifier, touchPoint);

          // 更新历史
          const history = touchHistory.current.get(touch.identifier) || [];
          history.push(touchPoint);
          if (history.length > 20) history.shift();
          touchHistory.current.set(touch.identifier, history);
        });

        // 计算新状态
        const touchesArray = Array.from(updatedTouches.values());
        let centerPoint = multiTouchState.centerPoint;
        let distance = multiTouchState.distance;
        let angle = multiTouchState.angle;

        if (touchesArray.length >= 2) {
          centerPoint = calculateCenter(touchesArray[0], touchesArray[1]);
          distance = calculateDistance(touchesArray[0], touchesArray[1]);
          angle = calculateAngle(touchesArray[0], touchesArray[1]);
        }

        setMultiTouchState({
          activeTouches: updatedTouches,
          centerPoint,
          distance,
          angle,
        });
      },

      onPanResponderRelease: (_, gestureState) => {
        const remainingTouches = new Map<number, TouchPoint>();
        
        gestureState.touches.forEach((touch) => {
          const existingTouch = multiTouchState.activeTouches.get(
            touch.identifier
          );
          if (existingTouch) {
            remainingTouches.set(touch.identifier, existingTouch);
          }
        });

        setMultiTouchState({
          activeTouches: remainingTouches,
          centerPoint: multiTouchState.centerPoint,
          distance: multiTouchState.distance,
          angle: multiTouchState.angle,
        });
      },

      onPanResponderTerminate: () => {
        setMultiTouchState({
          activeTouches: new Map(),
          centerPoint: { x: 0, y: 0 },
          distance: 0,
          angle: 0,
        });
        touchHistory.current.clear();
      },
    }),
    [multiTouchState]
  );

  return {
    multiTouchState,
    panResponder,
    touchCount: multiTouchState.activeTouches.size,
    isPinch: multiTouchState.activeTouches.size === 2,
  };
};

5.2 手势序列组合(长按后拖拽)

typescript 复制代码
// src/gestures/GestureSequence/index.ts
import { useState, useCallback, useRef } from 'react';
import { PanResponder } from 'react-native';

export interface GestureSequenceConfig {
  sequence: Array<{
    id: string;
    type: 'tap' | 'longpress' | 'pan' | 'pinch';
    duration?: number; // 毫秒,用于longpress
    threshold?: number; // 像素阈值
  }>;
  onSequenceComplete?: (sequence: string[]) => void;
  onSequenceFail?: (completedSteps: string[], failedStep: string) => void;
}

export const useGestureSequence = (
  config: GestureSequenceConfig
) => {
  const [completedSteps, setCompletedSteps] = useState<string[]>([]);
  const [currentStep, setCurrentStep] = useState<number>(0);
  const longPressTimer = useRef<NodeJS.Timeout | null>(null);
  const startTime = useRef<number>(0);

  const panResponder = useCallback(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,

      onPanResponderGrant: (_, gestureState) => {
        startTime.current = Date.now();
        const currentGestureConfig = config.sequence[currentStep];

        if (currentGestureConfig?.type === 'longpress') {
          const duration = currentGestureConfig.duration || 500;
          longPressTimer.current = setTimeout(() => {
            // 长按完成
            setCompletedSteps((prev) => [...prev, currentGestureConfig.id]);
            setCurrentStep((prev) => prev + 1);
            
            if (currentStep + 1 >= config.sequence.length) {
              config.onSequenceComplete?.([
                ...completedSteps,
                currentGestureConfig.id,
              ]);
            }
          }, duration);
        }
      },

      onPanResponderMove: (_, gestureState) => {
        const currentGestureConfig = config.sequence[currentStep];
        const elapsed = Date.now() - startTime.current;

        // 如果移动超过阈值,取消长按
        if (
          currentGestureConfig?.type === 'longpress' &&
          (Math.abs(gestureState.dx) > 10 || Math.abs(gestureState.dy) > 10)
        ) {
          if (longPressTimer.current) {
            clearTimeout(longPressTimer.current);
            longPressTimer.current = null;
          }
          
          config.onSequenceFail?.(completedSteps, currentGestureConfig.id);
          resetSequence();
        }

        // 处理拖拽步骤
        if (currentGestureConfig?.type === 'pan') {
          const threshold = currentGestureConfig.threshold || 5;
          if (
            Math.abs(gestureState.dx) > threshold ||
            Math.abs(gestureState.dy) > threshold
          ) {
            setCompletedSteps((prev) => [...prev, currentGestureConfig.id]);
            setCurrentStep((prev) => prev + 1);
          }
        }
      },

      onPanResponderRelease: () => {
        if (longPressTimer.current) {
          clearTimeout(longPressTimer.current);
          longPressTimer.current = null;
        }

        // 检查是否完成所有步骤
        if (currentStep >= config.sequence.length) {
          config.onSequenceComplete?.(completedSteps);
        }
      },

      onPanResponderTerminate: () => {
        resetSequence();
      },
    }),
    [currentStep, completedSteps, config]
  );

  const resetSequence = useCallback(() => {
    if (longPressTimer.current) {
      clearTimeout(longPressTimer.current);
      longPressTimer.current = null;
    }
    setCompletedSteps([]);
    setCurrentStep(0);
  }, []);

  const getProgress = useCallback(() => {
    return currentStep / config.sequence.length;
  }, [currentStep, config.sequence.length]);

  return {
    panResponder,
    completedSteps,
    currentStep,
    totalSteps: config.sequence.length,
    progress: getProgress(),
    resetSequence,
  };
};

六、手势冲突解决策略

6.1 智能冲突解析器

typescript 复制代码
// src/gestures/ConflictResolver/index.ts
export interface ConflictResolutionResult {
  winner: string;
  losers: string[];
  reason: string;
}

export class SmartConflictResolver {
  private gestureWeights: Map<string, number> = new Map();
  private recentGestures: Array<{ gestureId: string; timestamp: number }> = [];

  // 注册手势权重
  registerGesture(gestureId: string, weight: number) {
    this.gestureWeights.set(gestureId, weight);
  }

  // 解析冲突
  resolve(
    competingGestures: string[],
    context: {
      touchCount: number;
      velocity: number;
      distance: number;
      duration: number;
    }
  ): ConflictResolutionResult {
    // 1. 基于权重排序
    const sortedGestures = competingGestures.sort(
      (a, b) => (this.gestureWeights.get(b) || 0) - (this.gestureWeights.get(a) || 0)
    );

    // 2. 根据上下文调整
    const adjustedScores = sortedGestures.map((gestureId) => {
      let score = this.gestureWeights.get(gestureId) || 0;

      // 多指场景提升捏合/旋转权重
      if (context.touchCount >= 2) {
        if (gestureId === 'pinch' || gestureId === 'rotate') {
          score *= 1.5;
        }
      }

      // 高速移动提升拖拽权重
      if (context.velocity > 0.5) {
        if (gestureId === 'pan') {
          score *= 1.3;
        }
      }

      // 长时间按压提升长按权重
      if (context.duration > 400) {
        if (gestureId === 'longpress') {
          score *= 1.4;
        }
      }

      return { gestureId, score };
    });

    // 3. 选择获胜者
    const winner = adjustedScores.reduce(
      (max, current) => (current.score > max.score ? current : max),
      adjustedScores[0]
    );

    const losers = adjustedScores
      .filter((g) => g.gestureId !== winner.gestureId)
      .map((g) => g.gestureId);

    // 4. 记录历史
    this.recentGestures.push({
      gestureId: winner.gestureId,
      timestamp: Date.now(),
    });
    if (this.recentGestures.length > 100) {
      this.recentGestures.shift();
    }

    return {
      winner: winner.gestureId,
      losers,
      reason: this.getResolutionReason(winner.gestureId, context),
    };
  }

  private getResolutionReason(
    gestureId: string,
    context: {
      touchCount: number;
      velocity: number;
      distance: number;
      duration: number;
    }
  ): string {
    const reasons: Record<string, string> = {
      pan: `高速移动 (velocity: ${context.velocity.toFixed(2)})`,
      pinch: `双指操作 (touches: ${context.touchCount})`,
      rotate: `双指旋转 (touches: ${context.touchCount})`,
      longpress: `长时间按压 (duration: ${context.duration}ms)`,
      tap: `快速点击 (duration: ${context.duration}ms)`,
    };
    return reasons[gestureId] || '默认优先级';
  }

  // 获取学习到的模式
  getLearnedPatterns() {
    const patternCount: Record<string, number> = {};
    this.recentGestures.forEach(({ gestureId }) => {
      patternCount[gestureId] = (patternCount[gestureId] || 0) + 1;
    });
    return patternCount;
  }
}

// 导出单例
export const conflictResolver = new SmartConflictResolver();

6.2 HarmonyOS平台特定优化

typescript 复制代码
// src/utils/harmonyGestureOptimizations.ts
import { Platform } from 'react-native';

export const isHarmonyOS = Platform.OS === 'harmony';

// 鸿蒙平台手势配置
export const getHarmonyGestureConfig = () => {
  if (isHarmonyOS) {
    return {
      // 鸿蒙触摸采样率更高
      touchSampleRate: 120, // Hz
      // 手势识别阈值
      gestureThresholds: {
        pan: 8, // 像素
        pinch: 15,
        rotate: 10,
        longpress: 450, // 毫秒
      },
      // 启用原生手势优化
      enableNativeOptimization: true,
      // 多指支持
      multiTouchSupport: true,
      // 最大支持手指数
      maxTouchPoints: 10,
    };
  }

  return {
    touchSampleRate: 60,
    gestureThresholds: {
      pan: 5,
      pinch: 10,
      rotate: 5,
      longpress: 500,
    },
    enableNativeOptimization: false,
    multiTouchSupport: false,
    maxTouchPoints: 5,
  };
};

// 鸿蒙手势性能监控
export const monitorHarmonyGesturePerformance = () => {
  if (!isHarmonyOS) return null;

  const startTime = performance.now();
  const markers: Array<{ name: string; time: number }> = [];

  return {
    mark: (name: string) => {
      markers.push({ name, time: performance.now() - startTime });
    },
    report: () => {
      const totalTime = performance.now() - startTime;
      console.log('[Harmony Gesture Performance]', {
        totalTime: `${totalTime.toFixed(2)}ms`,
        markers: markers.map((m) => `${m.name}: ${m.time.toFixed(2)}ms`),
      });
      return { totalTime, markers };
    },
  };
};

七、常见问题与解决方案

7.1 问题1:手势识别不准确

症状:捏合手势被误识别为拖拽

解决方案

typescript 复制代码
// 增加手势区分阈值
const config = {
  gestures: [
    {
      id: 'pan',
      type: 'pan',
      priority: 3,
      threshold: 10, // 增加阈值
    },
    {
      id: 'pinch',
      type: 'pinch',
      priority: 2,
      threshold: 20, // 捏合需要更大移动
    },
  ],
};

// 使用触摸点数量判断
if (gestureState.numberActiveTouches >= 2) {
  // 优先识别为捏合/旋转
  recognizePinchOrRotate();
} else {
  // 单指识别为拖拽
  recognizePan();
}

7.2 问题2:手势切换卡顿

症状:从拖拽切换到缩放时有明显延迟

解决方案

typescript 复制代码
// 1. 使用Animated直接驱动
const panX = useRef(new Animated.Value(0)).current;
const scale = useRef(new Animated.Value(1)).current;

// 2. 预加载手势状态
useEffect(() => {
  // 提前初始化所有手势状态
  initializeGestureStates();
}, []);

// 3. 使用原生驱动
Animated.event(
  [{ nativeEvent: { translationX: panX } }],
  { useNativeDriver: true }
);

7.3 问题3:内存泄漏

症状:长时间使用后应用变慢

解决方案

typescript 复制代码
// 组件卸载时清理
useEffect(() => {
  return () => {
    // 清理定时器
    if (longPressTimer.current) {
      clearTimeout(longPressTimer.current);
    }
    // 清理手势历史
    touchHistory.current.clear();
    // 重置状态
    resetGestures();
  };
}, []);

八、总结与最佳实践

8.1 技术选型建议

场景 推荐方案 理由
简单双手势 GestureCombiner + RACE模式 轻量、易实现
复杂多手势 自定义冲突解析器 灵活控制
序列手势 useGestureSequence 状态管理清晰
多指协同 useMultiTouchGesture 原生支持
高性能需求 Animated + 原生驱动 避免JS线程阻塞

8.2 最佳实践清单

  • ✅ 合理设置手势优先级和阈值
  • ✅ 使用冲突解析器处理手势竞争
  • ✅ 组件卸载时清理所有手势资源
  • ✅ 使用Animated驱动UI变化
  • ✅ 针对鸿蒙平台进行特定优化
  • ✅ 添加手势反馈提升用户体验
  • ✅ 记录手势历史用于调试和分析
  • ✅ 使用TypeScript确保类型安全

8.3 未来展望

随着HarmonyOS 6.0+的持续演进,手势组合与协同能力将进一步提升:

  • AI手势预测:利用鸿蒙AI能力预测用户手势意图
  • 跨设备手势同步:分布式能力支持多设备手势协同
  • 触觉反馈增强:结合鸿蒙线性马达提供丰富触感
  • 眼动+手势融合:多模态交互新体验

📚 参考资源

  1. HarmonyOS 6.0 官方文档
  2. React Native for OpenHarmony GitHub
  3. 手势冲突解决策略总结 - 稀土掘金
  4. HarmonyOS手势组合官方文档

💡 提示:本文代码示例基于2025-2026年最新技术栈,实际使用时请根据项目具体版本进行调整。欢迎在评论区交流讨论!

觉得有用?欢迎点赞、收藏、转发! 🚀

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

相关推荐
星空22232 小时前
【HarmonyOS】React Native of HarmonyOS实战:手势状态管理
react native·华为·harmonyos
平安的平安2 小时前
【OpenHarmony】React Native鸿蒙实战:WebSocket心跳保活
websocket·react native·harmonyos
键盘鼓手苏苏4 小时前
Flutter for OpenHarmony:markdown 纯 Dart 解析引擎(将文本转化为结构化 HTML/UI) 深度解析与鸿蒙适配指南
前端·网络·算法·flutter·ui·html·harmonyos
阿林来了10 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 原始插件源码分析
flutter·harmonyos·鸿蒙
不爱吃糖的程序媛10 小时前
Flutter 应用退出插件 HarmonyOS 适配技术详解
flutter·华为·harmonyos
lqj_本人15 小时前
Flutter三方库适配OpenHarmony【apple_product_name】华为Pura系列设备映射表
flutter·华为
木斯佳16 小时前
HarmonyOS实战(解决方案篇)—深色模式适配完全指南:从原理到实践
华为·harmonyos
阿林来了18 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 语音识别停止与取消
flutter·语音识别·harmonyos
松叶似针19 小时前
Flutter三方库适配OpenHarmony【secure_application】— 应用生命周期回调注册
flutter·harmonyos