一、适用版本

本文所涉及的功能适用于 Harmony OS NEXT / 5.0 / API 12 + 版本。在这些版本的鸿蒙系统上,开发者能够借助相关 API 实现丰富且流畅的手势交互功能,为应用增添独特魅力。

二、效果展示

三、实现逻辑

(一)组件状态管理

通过 @State 装饰器定义了三个关键状态变量,它们如同组件的 “指挥棒”,掌控着整个交互过程的状态变化。

  • showDialog 用于控制弹出对话框的显示与隐藏,就像舞台的幕布开关,决定着特定交互界面是否呈现给用户。初始值为 false,意味着默认情况下对话框是隐藏的。
  • currentMode 表示当前的操作模式,其类型为自定义的 SelectType 枚举。枚举包含 DELETETEXTNONE 三种模式,分别对应不同的操作含义。currentMode 的初始值为 SelectType.NONE,表示初始状态下没有特定的操作模式。
  • winWidth 用于存储设备窗口的宽度,这对于判断滑动操作的位置至关重要。初始值为 0,随后会在组件即将显示时获取实际的窗口宽度。

(二)对话框内容构建

利用 @Builder 装饰器创建 getCountUI 方法,这个方法就像是一个精巧的设计师,负责构建弹出对话框的具体内容。

  • 首先构建一个垂直方向的 Column 布局,作为对话框的整体框架。
  • Column 布局内放置一个水平方向的 Row 布局,用于排列操作选项。Row 布局中的两个 Text 组件分别显示 “删除” 和 “文字”,它们通过调用扩展方法 newExtendText 来设置统一的样式,包括固定的宽高、圆角、文本居中对齐、白色字体以及旋转角度等。并且,这两个 Text 组件的背景颜色会根据 currentMode 的值动态变化,直观地向用户展示当前所处的操作模式。
  • Row 布局设置了 heightwidthjustifyContent 属性,使其内部元素均匀分布在水平方向上,且占据整个父容器的空间。
  • 最后,Column 布局设置了整体的 widthheightbackgroundColor,为对话框营造出一个半透明的遮罩效果,突出对话框内的操作选项。

(三)手势交互逻辑

  1. 长按手势:在 Button 组件上绑定了 GestureGroup,其中包含长按手势 LongPressGesture。当用户长按按钮时,onAction 事件触发,将 showDialog 设置为 true,如同拉开舞台幕布,显示出对话框。当长按结束时,onActionEnd 事件触发,将 showDialog 设置为 false,关闭对话框。
  2. 滑动手势GestureGroup 中还包含滑动手势 PanGesture。在滑动过程中,onActionUpdate 事件获取手指在屏幕上的全局 X 坐标,并与窗口宽度的一半进行比较。如果手指的 X 坐标小于窗口宽度的一半,将 currentMode 设置为 SelectType.DELETE;否则,设置为 SelectType.TEXT。当滑动结束时,onActionEnd 事件将 currentMode 重置为 SelectType.NONE,为下一次操作做好准备。

(四)组件生命周期处理

  1. 获取窗口宽度:在组件即将显示时,aboutToAppear 生命周期方法被调用。通过 display.getAllDisplays() 获取设备的所有显示信息,并从返回结果中提取第一个显示设备的宽度,赋值给 winWidth。这一步为后续判断滑动操作的位置提供了关键依据。
  2. 动态更新窗口宽度:当页面区域发生变化时,onAreaChange 事件被触发,更新 winWidth 的值,确保在窗口大小改变的情况下,手势交互逻辑依然能够准确运行。

(五)页面布局与交互整合

build 方法中,通过 Column 布局构建页面结构。页面由一个包含 ButtonRow 组成,Button 设置了宽度、内边距和按钮类型。通过 gesture 方法绑定 GestureGroup,将长按和滑动手势整合到按钮的交互中。最后,使用 bindContentCover 方法将对话框与页面进行绑定,并设置 modalTransitionModalTransition.NONE,实现对话框以无过渡效果的方式显示在页面上。

四、源码解析

import { display, promptAction } from '@kit.ArkUI'

@Entry
@Component
struct LongPressGesturePage {
    @State showDialog: boolean = false
    @State currentMode: SelectType = SelectType.NONE
    @State winWidth: number = 0

    // 拖地获取尺寸
    aboutToAppear(): void {
        display.getAllDisplays()
          .then(res => {
                this.winWidth = res[0].width as number
            })
    }

    @Builder
    getCountUI() {
        Column() {
            Row() {
                Text("删除")
                  .newExtendText()
                  .backgroundColor(this.currentMode === SelectType.DELETE? Color.Red : Color.Gray)
                Text("文字")
                  .newExtendText()
                  .backgroundColor(this.currentMode === SelectType.TEXT? Color.Green : Color.Gray)
                  .rotate({
                        angle: 20
                    })
            }
           .height('100%')
           .width('100%')
           .justifyContent(FlexAlign.SpaceEvenly)

        }
       .width('100%')
       .height('100%')
       .backgroundColor("rgba(0,0,0, 0.25)")
    }

    build() {
        Column() {
            Row() {
                Button('语    音')
                  .width('80%')
                  .padding(10)
                  .type(ButtonType.Normal)
                  .gesture(
                        GestureGroup(GestureMode.Parallel,
                            LongPressGesture()
                              .onAction(() => {
                                    this.showDialog = true
                                })
                              .onActionEnd(() => {
                                    this.showDialog = false
                                }),
                            PanGesture()
                              .onActionUpdate((e) => {
                                    let figerX = e.fingerList[0].globalX.toString()
                                    if (this.winWidth / 2 > Number(figerX)) {
                                        this.currentMode = SelectType.DELETE

                                    } else {
                                        this.currentMode = SelectType.TEXT

                                    }
                                })
                              .onActionEnd(() => {
                                    this.currentMode = SelectType.NONE
                                })
                        )

                    )
            }
           .padding(20)
           .height('100%')
           .alignItems(VerticalAlign.Bottom)
        }
       .width('100%')
       .bindContentCover(this.showDialog, this.getCountUI(), { modalTransition: ModalTransition.NONE })
       .onAreaChange((oldW, newW) => {
            this.winWidth = newW.width as number
        })
    }
}

enum SelectType {
    DELETE,
    TEXT,
    NONE
}

@Extend(Text)
function newExtendText() {
   .width(50)
   .height(50)
   .borderRadius(25)
   .textAlign(TextAlign.Center)
   .fontColor(Color.White)
   .rotate({
        angle: -20
    })
}
  1. 导入模块:从 @kit.ArkUI 导入 displaypromptAction 模块。display 用于获取设备显示相关信息,在代码中用于获取窗口宽度;promptAction 虽然在当前代码中未实际使用,但它通常用于显示弹窗等提示信息,为后续功能扩展提供了可能性。
  2. 组件定义:使用 @Entry@Component 装饰器定义 LongPressGesturePage 组件,这是整个交互功能的核心组件。
  3. 状态变量:如前文所述,showDialogcurrentModewinWidth 三个状态变量分别控制对话框显示、操作模式和窗口宽度,它们在组件的交互过程中起着关键作用。
  4. aboutToAppear 方法:在组件即将显示时,通过 display.getAllDisplays() 获取设备的显示信息,并从中提取窗口宽度赋值给 winWidth,确保在组件显示时能够准确获取窗口尺寸,为手势交互提供准确的数据支持。
  5. getCountUI 方法:通过 @Builder 装饰器构建对话框的 UI 结构。内部使用 ColumnRow 布局,以及 Text 组件来展示操作选项,并根据 currentMode 的值动态改变 Text 组件的背景颜色,同时为 Text 组件设置了统一的样式。
  6. build 方法:构建页面的整体布局。Button 组件绑定了包含长按和滑动手势的 GestureGroup,实现了按钮的长按和滑动交互功能。通过 bindContentCover 方法将对话框与页面进行绑定,实现对话框的弹出显示。onAreaChange 方法用于在页面区域变化时更新 winWidth 的值,保证手势交互逻辑在窗口大小改变时依然准确。
  7. 枚举定义SelectType 枚举定义了三种操作模式,为 currentMode 提供了取值范围,使代码的可读性和可维护性更强。
  8. 扩展方法:通过 @Extend(Text)Text 组件扩展 newExtendText 方法,为 Text 组件设置了固定的宽高、圆角、文本对齐方式、字体颜色和旋转角度等样式,简化了 Text 组件的样式设置过程。

五、可能的优化方向

  1. 增加操作反馈:在用户进行长按和滑动操作时,可以添加一些视觉或触觉反馈,如按钮的轻微缩放、震动反馈等,让用户更清晰地感知到操作的响应,提升交互的趣味性和直观性。
  2. 优化对话框样式:当前对话框的样式较为简单,可以进一步优化,如添加阴影效果、调整透明度渐变等,使其在视觉上更加突出和美观,与应用的整体风格更加契合。
  3. 代码结构优化:可以将手势相关的逻辑提取到单独的方法中,使 build 方法更加简洁明了,提高代码的可读性和可维护性。例如:
private setupGestures() {
    return GestureGroup(GestureMode.Parallel,
        LongPressGesture()
          .onAction(() => {
                this.showDialog = true
            })
          .onActionEnd(() => {
                this.showDialog = false
            }),
        PanGesture()
          .onActionUpdate((e) => {
                let figerX = e.fingerList[0].globalX.toString()
                if (this.winWidth / 2 > Number(figerX)) {
                    this.currentMode = SelectType.DELETE
                } else {
                    this.currentMode = SelectType.TEXT
                }
            })
          .onActionEnd(() => {
                this.currentMode = SelectType.NONE
            })
    )
}

build() {
    Column() {
        Row() {
            Button('语    音')
              .width('80%')
              .padding(10)
              .type(ButtonType.Normal)
              .gesture(this.setupGestures())
        }
       .padding(20)
       .height('100%')
       .alignItems(VerticalAlign.Bottom)
    }
   .width('100%')
   .bindContentCover(this.showDialog, this.getCountUI(), { modalTransition: ModalTransition.NONE })
   .onAreaChange((oldW, newW) => {
        this.winWidth = newW.width as number
    })
}

4.错误处理优化:在获取窗口宽度的过程中,display.getAllDisplays() 方法返回的 Promise 可能会 reject,当前代码没有处理这种情况。可以添加错误处理逻辑,例如:

aboutToAppear(): void {
    display.getAllDisplays()
      .then(res => {
            this.winWidth = res[0].width as number
        })
      .catch(error => {
            console.error('获取屏幕宽度失败:', error)
            // 可以在这里添加提示用户的逻辑,比如显示一个错误弹窗
        })
}

关于鸿蒙应用开发中手势交互的更多技巧和优化方法,我在博客中还有更详细的分享,感兴趣的话不妨前往查看,相信能为你的开发工作带来更多启发。你在开发过程中有没有遇到过类似的手势交互问题呢?欢迎在评论区留言交流。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐