在这里插入图片描述

摘要

在 HarmonyOS 的 ArkUI 开发中,经常会遇到这样一种交互需求:
用户按下某个组件,拖动它,然后在松手的一瞬间触发一个“释放”动作,比如飞出去、回弹、投放到某个区域,或者触发业务逻辑。

很多同学在一开始都会问一个问题:
ArkUI 里有没有现成的“施放 API”?

答案是:没有。
但 ArkUI 提供的 手势系统、状态管理和动画能力,已经足够我们组合出各种“施放效果”。

这篇文章就从一个最基础的拖拽开始,一步一步讲清楚:
ArkUI 中的“施放功能”到底是怎么实现的,以及在真实项目中该怎么用。

引言

随着 HarmonyOS 应用交互越来越偏向“自然操作”,像拖拽、投放、抛出这类交互,在实际项目中出现得非常多,比如:

  • 卡片拖到指定区域触发操作
  • 图标长按后丢进回收区
  • 功能模块拖拽排序
  • 智能设备管理中,把设备“丢”进分组

在 ArkUI 里,这些效果并不是某一个组件单独完成的,而是多种能力的组合
理解这一点之后,你会发现实现起来并不复杂,而且扩展性非常强。

ArkUI 中“施放”的本质是什么

从技术角度来看,所谓“施放”,本质就是三步:

  1. 用手势感知用户操作
  2. 用状态驱动组件位置变化
  3. 在松手时,通过动画完成“释放效果”

换句话说就是:
手势负责输入,状态负责位置,动画负责感觉。

最基础的施放实现:拖拽 + 松手回弹

实现思路

这个 Demo 不考虑目标区域,只关注三件事:

  • 手指拖动时,组件跟着动
  • 松手后触发动画
  • 动画结束后回到原位

可运行 Demo 示例

@Entry
@Component
struct CastBasicDemo {
  @State offsetX: number = 0
  @State offsetY: number = 0

  build() {
    Column() {
      Text('拖拽组件,松手后施放')
        .fontSize(18)
        .margin(20)

      Box()
        .width(80)
        .height(80)
        .backgroundColor(Color.Blue)
        .translate({ x: this.offsetX, y: this.offsetY })
        .gesture(
          PanGesture()
            .onUpdate((event) => {
              // 拖动过程中,组件位置实时更新
              this.offsetX = event.offsetX
              this.offsetY = event.offsetY
            })
            .onEnd(() => {
              // 松手瞬间,触发“施放”动画
              animateTo({
                duration: 300,
                curve: Curve.EaseOut
              }, () => {
                this.offsetX = 0
                this.offsetY = 0
              })
            })
        )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

代码讲解(重点)

这里其实就三行是核心:

this.offsetX = event.offsetX
this.offsetY = event.offsetY

组件的位置完全由 @State 控制,手势只是不断修改状态。

而“施放”的感觉来自这里:

animateTo({}, () => {
  this.offsetX = 0
  this.offsetY = 0
})

只要状态变化发生在动画作用域内,就会自动过渡。

带目标区域的“施放”:成功 or 回弹

在真实项目中,施放通常不是随便松手就算成功,而是:

  • 拖到某个区域才成功
  • 没拖到就回弹

思路拆解

  • 拖拽过程中,持续记录位移
  • 松手时判断最终位置
  • 根据结果执行不同动画

示例代码

@Entry
@Component
struct CastTargetDemo {
  @State offsetX: number = 0
  @State offsetY: number = 0

  build() {
    Stack() {
      // 目标区域
      Box()
        .width(120)
        .height(120)
        .backgroundColor(Color.Grey)
        .position({ x: 200, y: 300 })

      // 可施放组件
      Box()
        .width(80)
        .height(80)
        .backgroundColor(Color.Green)
        .translate({ x: this.offsetX, y: this.offsetY })
        .gesture(
          PanGesture()
            .onUpdate((event) => {
              this.offsetX = event.offsetX
              this.offsetY = event.offsetY
            })
            .onEnd(() => {
              if (this.offsetX > 150 && this.offsetY > 250) {
                // 施放成功,吸附到目标
                animateTo({ duration: 200 }, () => {
                  this.offsetX = 200
                  this.offsetY = 300
                })
              } else {
                // 失败,回弹
                animateTo({ duration: 300 }, () => {
                  this.offsetX = 0
                  this.offsetY = 0
                })
              }
            })
        )
    }
    .width('100%')
    .height('100%')
  }
}

这里在做什么判断

if (this.offsetX > 150 && this.offsetY > 250)

这本质上是一个区域命中判断
在正式项目中,你可以:

  • 根据组件尺寸动态计算
  • 封装成工具函数
  • 甚至引入碰撞检测逻辑

真实应用场景示例

场景一:卡片拖拽投放到功能区

典型应用
首页卡片管理、模块编辑模式。

示例核心代码
.onEnd(() => {
  if (this.offsetX > 180) {
    animateTo({ duration: 200 }, () => {
      this.offsetX = 220
      this.offsetY = 0
    })
    // 这里可以触发业务逻辑,比如加入列表
  } else {
    animateTo({ duration: 300 }, () => {
      this.offsetX = 0
      this.offsetY = 0
    })
  }
})

逻辑上非常清晰:
UI 动画和业务逻辑是分开的,不会互相影响。

场景二:图标拖进回收站

这种交互非常常见,关键点是:

  • 松手瞬间让组件消失
  • 而不是回弹
.onEnd(() => {
  if (this.offsetY > 400) {
    animateTo({ duration: 200 }, () => {
      this.offsetY = 600
    })
  } else {
    animateTo({ duration: 300 }, () => {
      this.offsetX = 0
      this.offsetY = 0
    })
  }
})

你也可以配合透明度一起做:

.opacity(this.isRemoved ? 0 : 1)

场景三:设备管理中的“拖拽分组”

结合你后续可能做的鸿蒙设备管理场景:

  • 左侧设备列表
  • 右侧分组区域
  • 拖拽设备到分组完成绑定

这时就可以升级到 Drag & Drop,实现跨组件投放。

Box()
  .draggable(true)
  .onDragStart(() => {
    return { data: 'device-id-001' }
  })

目标区域:

Column()
  .onDrop((event) => {
    console.log('接收到设备:', event.data)
  })

这种方式更适合复杂业务。

QA 常见问题

Q1:为什么不用绝对定位?

绝对定位是死的,而 translate 是基于状态的,动画过渡更自然,也更安全。

Q2:施放动画卡顿怎么办?

  • 确保只操作必要的状态
  • 避免在 onUpdate 里写复杂逻辑
  • 动画时间不要太长

Q3:PanGesture 和 Drag 怎么选?

  • 单组件内部效果:PanGesture
  • 跨组件、跨区域:Drag & Drop

总结

在 ArkUI 中,“施放功能”并不是某一个 API,而是一种交互设计模式

  • 手势负责感知用户行为
  • 状态决定组件位置
  • 动画塑造最终体验

只要你理解了这个组合思路,就可以根据项目需求,灵活实现各种拖拽、投放、释放效果,而且代码非常干净、可维护性也很好。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐