没有现成 API?教你在 ArkUI 里手写一个“施放”交互效果
在 HarmonyOS 的 ArkUI 开发中,经常会遇到这样一种交互需求: 很多同学在一开始都会问一个问题: 答案是:没有。 这篇文章就从一个最基础的拖拽开始,一步一步讲清楚: 随着 HarmonyOS 应用交互越来越偏向“自然操作”,像拖拽、投放、抛出这类交互,在实际项目中出现得非常多,比如: 在 ArkUI 里,这些效果并不是某一个组件单独完成的,而是多种能力的组合。 从技术角度来看,所谓“施放”,本质就是三步: 换句话说就是: 这个 Demo 不考虑目标区域,只关注三件事: 这里其实就三行是核心: 组件的位置完全由 而“施放”的感觉来自这里: 只要状态变化发生在动画作用域内,就会自动过渡。 在真实项目中,施放通常不是随便松手就算成功,而是: 这本质上是一个区域命中判断。 典型应用: 逻辑上非常清晰: 这种交互非常常见,关键点是: 你也可以配合透明度一起做: 结合你后续可能做的鸿蒙设备管理场景: 这时就可以升级到 Drag & Drop,实现跨组件投放。 目标区域: 这种方式更适合复杂业务。 绝对定位是死的,而 在 ArkUI 中,“施放功能”并不是某一个 API,而是一种交互设计模式: 只要你理解了这个组合思路,就可以根据项目需求,灵活实现各种拖拽、投放、释放效果,而且代码非常干净、可维护性也很好。
摘要
用户按下某个组件,拖动它,然后在松手的一瞬间触发一个“释放”动作,比如飞出去、回弹、投放到某个区域,或者触发业务逻辑。
ArkUI 里有没有现成的“施放 API”?
但 ArkUI 提供的 手势系统、状态管理和动画能力,已经足够我们组合出各种“施放效果”。
ArkUI 中的“施放功能”到底是怎么实现的,以及在真实项目中该怎么用。引言
理解这一点之后,你会发现实现起来并不复杂,而且扩展性非常强。ArkUI 中“施放”的本质是什么
手势负责输入,状态负责位置,动画负责感觉。最基础的施放实现:拖拽 + 松手回弹
实现思路
可运行 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)场景三:设备管理中的“拖拽分组”
Box()
.draggable(true)
.onDragStart(() => {
return { data: 'device-id-001' }
})Column()
.onDrop((event) => {
console.log('接收到设备:', event.data)
})QA 常见问题
Q1:为什么不用绝对定位?
translate 是基于状态的,动画过渡更自然,也更安全。Q2:施放动画卡顿怎么办?
onUpdate 里写复杂逻辑Q3:PanGesture 和 Drag 怎么选?
总结