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

【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

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
胖鱼罐头11 小时前
RNGH:指令式 vs JSX 形式深度对比
前端·react native
麟听科技12 小时前
HarmonyOS 6.0+ APP智能种植监测系统开发实战:农业传感器联动与AI种植指导落地
人工智能·分布式·学习·华为·harmonyos
前端不太难13 小时前
HarmonyOS PC 焦点系统重建
华为·状态模式·harmonyos
空白诗13 小时前
基础入门 Flutter for Harmony:Text 组件详解
javascript·flutter·harmonyos
lbb 小魔仙14 小时前
【HarmonyOS】React Native实战+Popover内容自适应
react native·华为·harmonyos
motosheep14 小时前
鸿蒙开发(四)播放 Lottie 动画实战(Canvas 渲染 + 资源加载踩坑总结)
华为·harmonyos
左手厨刀右手茼蒿15 小时前
Flutter for OpenHarmony 实战:Barcode — 纯 Dart 条形码与二维码生成全指南
android·flutter·ui·华为·harmonyos
果粒蹬i16 小时前
【HarmonyOS】React Native实战项目+NativeStack原生导航
react native·华为·harmonyos
waeng_luo16 小时前
HarmonyOS 应用开发 Skills
华为·harmonyos