HarmonyOS鸿蒙Next中如何实现应用内手势方向+速度组合识别?

HarmonyOS鸿蒙Next中如何实现应用内手势方向+速度组合识别? **问题描述:**需要根据用户的手势方向(如左右滑动)和滑动速度来区分不同的操作(如快速右滑代表删除,慢速右滑代表标记)。

3 回复

详细回答:

通过监听触摸事件并计算手指移动的方向和速度,可以实现手势方向与速度的组合识别。首先需要获取触摸点的位置变化以判断滑动方向,然后根据时间差计算出滑动速度。

支持手势识别的列表项组件

import { SwipeGestureRecognizer, SwipeInfo, SwipeDirection, SwipeSpeed } from './SwipeGestureRecognizer';

/**
 * @author J.query
 * @date 2025/12/26
 * @email j-query@foxmail.com
 * Description: 支持手势识别的列表项组件
 */

interface ListItemData {
  id: string;
  title: string;
  subtitle?: string;
  isMarked?: boolean;
}

@Component
export struct GestureListItem {
  @Prop itemData: ListItemData;
  onDelete?: (item: ListItemData) => void;
  onMark?: (item: ListItemData) => void;
  onAction?: (item: ListItemData, action: string) => void;
  
  @State private showDeleteConfirm: boolean = false;
  @State private showMarkFeedback: boolean = false;
  @State private isPressed: boolean = false;
  @State private swipeOffset: number = 0;

  build() {
    Column() {
      SwipeGestureRecognizer({
        config: {
          minDistance: 40,
          slowSpeedThreshold: 0.3,
          fastSpeedThreshold: 0.8,
          maxDuration: 800,
          diagonalTolerance: 0.4
        },
        callbacks: {
          onQuickSwipeRight: (info) => {
            this.handleQuickSwipeRight(info);
          },
          onSlowSwipeRight: (info) => {
            this.handleSlowSwipeRight(info);
          },
          onQuickSwipeLeft: (info) => {
            this.handleQuickSwipeLeft(info);
          },
          onSlowSwipeLeft: (info) => {
            this.handleSlowSwipeLeft(info);
          }
        }
      }) {
        Row() {
          // 左侧标记指示器
          if (this.itemData.isMarked) {
            Column() {
              Text('⭐')
                .fontSize(16)
            }
            .width(40)
            .justifyContent(FlexAlign.Center)
          }

          // 主要内容
          Column() {
            Text(this.itemData.title)
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
              .fontColor('#333333')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
              .width('100%')

            if (this.itemData.subtitle) {
              Text(this.itemData.subtitle)
                .fontSize(12)
                .fontColor('#666666')
                .maxLines(2)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
                .width('100%')
                .margin({ top: 4 })
            }
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          // 右侧箭头
          Column() {
            Text('>')
              .fontSize(16)
              .fontColor('#CCCCCC')
          }
          .width(30)
          .justifyContent(FlexAlign.Center)
        }
        .width('100%')
        .height(70)
        .padding({ left: 15, right: 15 })
        .backgroundColor(this.isPressed ? '#F0F0F0' : '#FFFFFF')
        .borderRadius(8)
        .shadow({
          radius: 2,
          color: '#00000010',
          offsetX: 0,
          offsetY: 1
        })
        .onClick(() => {
          this.onAction?.(this.itemData, 'tap');
        })
        .onTouch((event) => {
          if (event.type === TouchType.Down) {
            this.isPressed = true;
          } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
            this.isPressed = false;
          }
        })
      }
    }
    .width('100%')
    .margin({ bottom: 8 })
  }

  /**
   * 处理快速右滑 - 删除操作
   */
  private handleQuickSwipeRight(info: SwipeInfo) {
    console.log('快速右滑删除:', this.itemData.title);
    this.showDeleteConfirm = true;
  }

  /**
   * 处理慢速右滑 - 标记操作
   */
  private handleSlowSwipeRight(info: SwipeInfo) {
    console.log('慢速右滑标记:', this.itemData.title);
    this.showMarkFeedback = true;
    
    // 延迟后触发标记回调
    setTimeout(() => {
      this.showMarkFeedback = false;
      this.onMark?.(this.itemData);
    }, 500);
  }

  /**
   * 处理快速左滑
   */
  private handleQuickSwipeLeft(info: SwipeInfo) {
    console.log('快速左滑:', this.itemData.title);
    this.onAction?.(this.itemData, 'quick_swipe_left');
  }

  /**
   * 处理慢速左滑
   */
  private handleSlowSwipeLeft(info: SwipeInfo) {
    console.log('慢速左滑:', this.itemData.title);
    this.onAction?.(this.itemData, 'slow_swipe_left');
  }
}

手势列表项演示页面demo

import { StandardTitleBar } from '../components/StandardTitleBar';
import { GestureListItem } from '../components/GestureListItem';
import showToast from '../utils/ToastUtils';

/**
 * @author J.query
 * @date 2025/12/26
 * @email j-query@foxmail.com
 * Description: 手势列表项演示页面
 */

interface DemoItem {
  id: string;
  title: string;
  subtitle?: string;
  isMarked?: boolean;
}

@Entry
@Component
struct GestureListDemo {
  @State private items: DemoItem[] = [
    { id: '1', title: '视频文件.mp4', subtitle: '2025/12/26 10:30', isMarked: false },
    { id: '2', title: '会议录像.mp4', subtitle: '2025/12/25 14:20', isMarked: true },
    { id: '3', title: '教学视频.mp4', subtitle: '2025/12/24 09:15', isMarked: false },
    { id: '4', title: '产品演示.mp4', subtitle: '2025/12/23 16:45', isMarked: false },
    { id: '5', title: '用户指南.mp4', subtitle: '2025/12/22 11:30', isMarked: true },
    { id: '6', title: '培训课程.mp4', subtitle: '2025/12/21 13:20', isMarked: false },
  ];

  @State private actionHistory: string[] = [];

  aboutToAppear() {
    console.log('手势列表演示页面初始化');
  }

  /**
   * 处理删除操作
   */
  private handleDelete(item: DemoItem) {
    this.items = this.items.filter(i => i.id !== item.id);
    this.addToHistory(`删除: ${item.title}`);
    
    showToast(
       `已删除 "${item.title}"`);

  }

  /**
   * 处理标记操作
   */
  private handleMark(item: DemoItem) {
    const itemIndex = this.items.findIndex(i => i.id === item.id);
    if (itemIndex !== -1) {
      this.items[itemIndex].isMarked = !this.items[itemIndex].isMarked;
      const action = this.items[itemIndex].isMarked ? '标记' : '取消标记';
      this.addToHistory(`${action}: ${item.title}`);
    }
  }

  /**
   * 处理其他操作
   */
  private handleAction(item: DemoItem, action: string) {
    this.addToHistory(`${action}: ${item.title}`);
    
    if (action === 'tap') {
      console.log(`打开 "${item.title}"`);
    }
  }

  /**
   * 添加到操作历史
   */
  private addToHistory(action: string) {
    const timestamp = new Date().toLocaleTimeString();
    this.actionHistory.unshift(`${timestamp} - ${action}`);
    
    // 只保留最近10条记录
    if (this.actionHistory.length > 10) {
      this.actionHistory.pop();
    }
  }

  build() {
    Column() {
      // 标题栏
      StandardTitleBar({
        title: '手势列表演示',
        showBack: true
      })

      // 操作提示
      Column() {
        Text('🎯 手势操作说明')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 10 })

        Row() {
          Column() {
            Text('➡️ 快速右滑')
              .fontSize(12)
              .fontColor('#FF6B6B')
            Text('删除')
              .fontSize(10)
              .fontColor('#999999')
          }
          .width('30%')

          Column() {
            Text('➡️ 慢速右滑')
              .fontSize(12)
              .fontColor('#4ECDC4')
            Text('标记')
              .fontSize(10)
              .fontColor('#999999')
          }
          .width('30%')

          Column() {
            Text('👆 点击')
              .fontSize(12)
              .fontColor('#45B7D1')
            Text('打开')
              .fontSize(10)
              .fontColor('#999999')
          }
          .width('30%')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceAround)
      }
      .width('100%')
      .padding(15)
      .backgroundColor('#F0F8FF')
      .borderRadius(8)
      .margin({ left: 20, top: 10, bottom: 10 })

      // 列表内容
      Column() {
        if (this.items.length > 0) {
          List({ space: 0 }) {
            ForEach(this.items, (item: DemoItem) => {
              ListItem() {
                GestureListItem({
                  itemData: item,
                  onDelete: this.handleDelete.bind(this),
                  onMark: this.handleMark.bind(this),
                  onAction: this.handleAction.bind(this)
                })
              }
            })
          }
          .width('100%')
          .layoutWeight(1)
          .divider({
            strokeWidth: 0.5,
            color: '#F0F0F0',
            startMargin: 60,
            endMargin: 15
          })
        } else {
          Column() {
            Text('📭')
              .fontSize(48)
              .fontColor('#CCCCCC')
              .margin({ bottom: 10 })

            Text('暂无数据')
              .fontSize(16)
              .fontColor('#999999')

            Text('所有项目都已被删除')
              .fontSize(12)
              .fontColor('#CCCCCC')
              .margin({ top: 5 })
          }
          .width('100%')
          .height(200)
          .justifyContent(FlexAlign.Center)
        }
      }
      .width('100%')
      .padding({ bottom: 20 })
      .layoutWeight(1)

      // 操作历史
      Column() {
        Text('📋 操作历史')
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .textAlign(TextAlign.Start)
          .margin({ bottom: 8 })

        if (this.actionHistory.length > 0) {
          List({ space: 2 }) {
            ForEach(this.actionHistory.slice(0, 3), (history: string) => {
              ListItem() {
                Text(history)
                  .fontSize(10)
                  .fontColor('#666666')
                  .width('100%')
                  .padding({ left: 10, right: 10, top: 5, bottom: 5 })
                  .backgroundColor('#F8F8F8')
                  .borderRadius(4)
              }
            })
          }
          .width('100%')
        } else {
          Text('暂无操作记录')
            .fontSize(10)
            .fontColor('#CCCCCC')
            .textAlign(TextAlign.Center)
            .width('100%')
            .padding(10)
        }
      }
      .width('100%')
      .padding({ left: 20, bottom: 10 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

🎯 效果

cke_34212.png cke_44969.png

更多关于HarmonyOS鸿蒙Next中如何实现应用内手势方向+速度组合识别?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,可通过GestureGroup组合识别手势方向与速度。使用PanGesture监听滑动手势,其offset属性可判断方向(如x>0为右滑)。速度识别需结合PanGesture事件中的velocity属性(单位:像素/秒),该值反映实时速度。通过GestureGroupparallelsequential模式,将方向判断逻辑与速度阈值(如velocity > 1000)组合,实现特定方向与速度的复合手势识别。

在HarmonyOS Next中,可以通过GesturePanGesture组合监听器来实现应用内手势方向与速度的组合识别,核心在于分析PanGesture事件中的偏移量和速度向量。

以下是关键实现步骤和代码示例:

  1. 使用PanGesture监听器: 这是识别滑动(平移)手势的基础。它提供了丰富的触摸事件数据。

    // 在自定义组件或页面的build方法中为组件添加手势
    @Component
    struct GestureDemo {
      @State private offsetX: number = 0;
    
      build() {
        Column() {
          // 一个用于接收手势的容器
          Stack()
            .width('100%')
            .height(200)
            .backgroundColor(Color.Grey)
            .translate({ x: this.offsetX })
            // 绑定PanGesture手势识别
            .gesture(
              PanGesture({ distance: 5 }) // distance: 5 表示最小识别距离为5vp,可过滤微小误触
                .onActionStart((event: GestureEvent) => {
                  // 手势开始,可在此初始化状态
                  console.info(`Gesture start at (${event.offsetX}, ${event.offsetY})`);
                })
                .onActionUpdate((event: GestureEvent) => {
                  // 手势移动过程,实时更新UI(例如跟随手指移动)
                  this.offsetX = event.offsetX;
                })
                .onActionEnd((event: GestureEvent) => {
                  // 手势结束,这是进行方向与速度判断的关键时机
                  this.handleGestureEnd(event);
                })
                .onActionCancel(() => {
                  // 手势被取消(如来电中断),进行复位等操作
                  this.offsetX = 0;
                })
            )
        }
      }
    
  2. 在手势结束时(onActionEnd)进行判断GestureEvent对象(在onActionEnd回调中)包含了判断所需的核心信息:

    • 方向判断:通过event.offsetXevent.offsetY(从手势起点到终点的总偏移量)判断主要滑动方向。
    • 速度判断:通过event.velocity(一个Velocity对象,包含xy分量)获取松手瞬间的滑动速度,单位是vp/秒。
    private handleGestureEnd(event: GestureEvent) {
      // 1. 判断主要滑动方向(此处以水平滑动为例,可扩展为八方向)
      const isHorizontalSwipe = Math.abs(event.offsetX) > Math.abs(event.offsetY);
      if (isHorizontalSwipe) {
        if (event.offsetX > 0) {
          // 向右滑动
          this.evaluateRightSwipe(event.velocity.x);
        } else {
          // 向左滑动
          this.evaluateLeftSwipe(event.velocity.x);
        }
      } else {
        // 处理垂直滑动...
      }
    
      // 手势结束后,复位UI
      this.offsetX = 0;
    }
    
    private evaluateRightSwipe(velocityX: number) {
      // 2. 根据速度阈值区分操作
      const FAST_SPEED_THRESHOLD = 800; // 快速阈值,单位vp/s,需根据实际体验调整
      const SLOW_SPEED_THRESHOLD = 200; // 慢速阈值
    
      const absVelocity = Math.abs(velocityX);
      if (absVelocity >= FAST_SPEED_THRESHOLD) {
        console.info('快速右滑:执行删除操作');
        // 触发删除业务逻辑
      } else if (absVelocity <= SLOW_SPEED_THRESHOLD) {
        console.info('慢速右滑:执行标记操作');
        // 触发标记业务逻辑
      } else {
        console.info('中速右滑:可执行默认操作或忽略');
      }
    }
    

关键点与优化建议

  • 阈值调优:速度阈值(FAST_SPEED_THRESHOLD)和慢速阈值(SLOW_SPEED_THRESHOLD)需要根据具体交互场景和设备进行实测调整,以符合用户直觉。
  • 方向容差:在判断主方向时,可以加入一个容差范围,例如只有当水平位移与垂直位移的比值大于2:1时才判定为水平滑动,避免斜向滑动的误判。
  • 组合手势:如需更复杂识别(如长按后滑动),可将PanGestureLongPressGesture等通过GestureGroup进行组合(使用GestureMode.Exclusive等模式)。
  • 性能onActionUpdate中避免频繁进行复杂计算或状态更新,以免影响跟手性能。主要判断逻辑建议放在onActionEnd中。

此方案直接利用HarmonyOS Next手势系统提供的数据,无需手动计算时间差和位移,是实现方向+速度组合识别的标准且高效方式。

回到顶部