鸿蒙 ArkUI 交互响应学习笔记

实验三・核心代码分析与问题记录(2026 年 10 月 12 日)


一、核心代码分析

按代码的四个功能模块分段展示:每段代码带完整注释,代码下方为详细讲解。

(一)① 状态变量:按钮位置与菜单开关

// 按钮 X 坐标(@State 修饰:变量一变,界面自动刷新)
@State x: number = 100
// 按钮 Y 坐标
@State y: number = 100

// 拖拽基准点:按下瞬间记住按钮位置,防止拖动时按钮“跳”到手指处
@State startX: number = 0
@State startY: number = 0

// 菜单开关:true=显示蓝色“菜单”按钮,false=隐藏
@State menuBtnVisible: boolean = false

讲解:这是整个交互的核心 ——状态驱动 UI。@State 装饰器让变量成为响应式状态:变量一变,界面上所有引用它的组件自动重绘。拖拽、键盘、长按本质上都是 “改坐标” 这一个动作,界面自动跟随,无需手动操作界面。

(二)② 蓝色 “菜单” 按钮:条件渲染 + 跟随定位

// 条件渲染:只有 menuBtnVisible 为 true 时,蓝色按钮才“存在”
if (this.menuBtnVisible) {
  Column() {
    // 按钮文字:菜单
    Text('菜单').fontSize(12).fontColor('#FFFFFF')
  }
  .width(45).height(30)              // 尺寸 45×30
  .backgroundColor('#3B82F6')        // 蓝色背景
  .borderRadius(8)                   // 圆角
  .justifyContent(FlexAlign.Center)  // 内容水平居中
  // 位置:粉色按钮右上角外侧(粉色按钮宽 100,所以 x+95;高 50,所以 y-15 略上浮)
  .position({ x: this.x + 95, y: this.y - 15 })
  .onClick(() => {
    this.menuBtnVisible = false      // 点击后隐藏自己
    // 弹出提示“你打开了菜单”
    this.getUIContext().getPromptAction().showToast({ message: '你打开了菜单' })
  })
}

讲解:这段演示两个要点。条件渲染:用 “数据开关”(menuBtnVisible)控制界面元素显隐,为 true 才存在、为 false 自动移除。跟随定位:位置用 this.x + 95、this.y - 15 计算,拖拽粉色按钮时蓝色按钮会一直贴在其右上角。

(三)③ 粉色按钮:三种手势(拖拽 / 长按 / 双击)

// 粉色“交互”按钮本体
Column(){
  Text("交互").fontSize(20)
}
.width(100).height(50)                  // 尺寸 100×50
.backgroundColor(Color.Pink)            // 粉色背景
.borderRadius(16)                       // 圆角
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.position({ x: this.x, y: this.y })    // 位置绑定状态变量

// 手势1:拖拽(PanGesture)
.gesture(
  PanGesture()
    .onActionStart((event: GestureEvent) => {
      // 按下瞬间:记录基准点(当前位置)
      this.startX = this.x
      this.startY = this.y
    })
    .onActionUpdate((event: GestureEvent) => {
      // 拖动中:新位置 = 基准点 + 手指位移
      this.x = this.startX + event.offsetX
      this.y = this.startY + event.offsetY
    })
)

// 手势2:长按 500ms → 显示蓝色菜单按钮
.gesture(
  LongPressGesture({ repeat: false, duration: 500 })
    .onAction(() => {
      this.menuBtnVisible = true
    })
)

// 手势3:双击 → 撤回蓝色菜单按钮
.gesture(
  TapGesture({ count: 2 })
    .onAction(() => {
      this.menuBtnVisible = false
    })
)

讲解:

  • 拖拽基准点技巧:event.offsetX 是手指相对按下点的位移。如果不记基准点直接写 this.x = event.offsetX,按钮会 “跳” 到手指位置;记基准点(startX/startY)实现的是 “相对拖动”,手感最自然。

  • 多手势共存:TapGesture、LongPressGesture、PanGesture 同时绑在一个组件上,系统自动识别冲突:快速两击 = 双击、按住不动 = 长按、移动 = 拖拽,互不干扰。

(四)④ 键盘控制:焦点 + 键码映射

// 容器允许获得焦点(键盘事件的前提)
.focusable(true)
// 页面启动自动获焦:不用先点击按钮就能收到按键
.defaultFocus(true)
// 监听键盘事件
.onKeyEvent((event: KeyEvent) => {
  // 只在“按下”时响应(松开时 KeyType.Up 不处理)
  if (event.type === KeyType.Down) {
    // 调试打印:查看实际键码
    console.info('KEYCODE=' + event.keyCode)
    switch (event.keyCode) {
      // 上:Y 坐标减小 20
      case 2012: this.y -= 20; break
      // 下:Y 坐标增大 20
      case 2013: this.y += 20; break
      // 左:X 坐标减小 20
      case 2014: this.x -= 20; break
      // 右:X 坐标增大 20
      case 2015: this.x += 20; break
      default: break
    }
  }
})

讲解:键盘事件三要素 ——focusable(true) 允许获焦、defaultFocus(true) 自动获焦、onKeyEvent 接收按键,缺一不可。按键通过 keyCode 区分方向,每次按下把坐标增减 20(vp 单位),结合 @State 自动刷新实现移动。

(五)核心机制速览

机制一句话
状态驱动 UI@State 变量一变,界面自动重绘
拖拽基准点新位置 = 按下时位置 + 手指位移,防跳动
条件渲染if 数据开关控制元素显隐
多手势共存系统自动区分双击 / 长按 / 拖动
键盘三要素允许获焦 + 自动获焦 + 监听事件

二、实践问题(方向键映射错乱)

(一)问题现象

在测试键盘控制功能时,出现按键方向与预期不一致:按 “右方向键” 按钮却向左移动,按键盘 “a” 键按钮向上移动、按 “b” 键按钮向下移动,且找不到能触发 “向右” 的按键。

(二)原因分析

onKeyEvent 通过 keyCode(键码数字)区分按键。代码中按标准键码匹配方向(如 2015 = 左、2016 = 右),但实际运行环境(预览器)的键盘映射与标准不一致:物理按键被映射成了其他键码,导致 “按右方向键” 上报的却是 “左” 的键码,按键错位。

(三)解决过程

在 onKeyEvent 中加入 console.info(‘KEYCODE=’ + event.keyCode) 打印实际键码,逐个按键查看日志,拿到本机环境的真实键码后,按实测值修改 switch 分支:2012 = 上、2013 = 下、2014 = 左、2015 = 右,问题解决。

同时将测试环境从预览器切换到模拟器 —— 模拟器的键盘映射与真机一致,预览器对键盘事件支持不完整,仅适合查看界面布局。

(四)经验总结

遇到 “按键 / 手势没反应” 类问题,先判断是事件没到达还是参数不匹配:打印日志(KEYCODE)是最快的定位手段;同时注意不同测试环境的键盘映射可能不同,最终应以模拟器或真机为准。


三、源码

完整源码展示(无注释版本,可直接对照运行)。

@Entry
@Component
struct Index {
  @State x: number = 100
  @State y: number = 100
  @State startX: number = 0
  @State startY: number = 0
  @State menuBtnVisible: boolean = false

  build() {
    Stack() {
      if (this.menuBtnVisible) {
        Column() {
          Text('菜单').fontSize(12).fontColor('#FFFFFF')
        }
        .width(45).height(30)
        .backgroundColor('#3B82F6')
        .borderRadius(8)
        .justifyContent(FlexAlign.Center)
        .position({ x: this.x + 95, y: this.y - 15 })
        .onClick(() => {
          this.menuBtnVisible = false
          this.getUIContext().getPromptAction().showToast({ message: '你打开了菜单' })
        })
      }

      Column(){
        Text("交互").fontSize(20)
      }.width(100).height(50).backgroundColor(Color.Pink).borderRadius(16)
       .justifyContent(FlexAlign.Center)
       .alignItems(HorizontalAlign.Center)
       .position({ x: this.x, y: this.y })
       .gesture(
         PanGesture()
           .onActionStart((event: GestureEvent) => {
             this.startX = this.x
             this.startY = this.y
           })
           .onActionUpdate((event: GestureEvent) => {
             this.x = this.startX + event.offsetX
             this.y = this.startY + event.offsetY
           })
       )
       .gesture(
         LongPressGesture({ repeat: false, duration: 500 })
           .onAction(() => {
             this.menuBtnVisible = true
           })
       )
       .gesture(
         TapGesture({ count: 2 })
           .onAction(() => {
             this.menuBtnVisible = false
           })
       )
    }
    .width('100%')
    .height('100%')
    .focusable(true)
    .defaultFocus(true)
    .onKeyEvent((event: KeyEvent) => {
      if (event.type === KeyType.Down) {
        console.info('KEYCODE=' + event.keyCode)
        switch (event.keyCode) {
          case 2012: this.y -= 20; break
          case 2013: this.y += 20; break
          case 2014: this.x -= 20; break
          case 2015: this.x += 20; break
          default: break
        }
      }
    })
  }
}

Logo

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

更多推荐