当手势遇见鸿蒙,交互体验竟能如此惊艳(3)——虚拟摇杆
在鸿蒙 UI 开发中,开发者云杰分享了一个通过运动盘组件实现人物移动控制的交互方案。由于现成代码无法满足流畅度需求,他决定从零开始梳理交互逻辑,并最终将包含手势库、运动盘组件和完整交互逻辑的项目代码开源,供其他开发者直接使用。开发过程中,他通过大轮盘和小轮盘的组合,结合滑动手势和边界限制,实现了流畅的移动控制。此外,他还提供了高亮效果的实现思路,帮助开发者根据具体项目需求进行优化。最终,该方案可以
各位开发者小伙伴们好呀!我是青蓝逐码的云杰~还记得之前咱们一起拆解过各种手势交互的实现逻辑,也聊过我在项目里踩过的那些手势适配坑吗?最近我又在鸿蒙 UI 开发中碰到了一个有意思的需求 —— 通过运动盘组件实现人物移动控制。本想着去各大技术论坛薅点现成代码 “借鉴” 一下,结果翻遍了,来回折腾了好几版都卡在交互流畅度上,简直让人头秃(扶额)。
痛定思痛之后,我决定从零开始梳理这个运动盘交互方案,考虑到很多小伙伴在开发时可能会像我一样优先搜索现成方案,文末老规矩,会把包含手势库、运动盘组件和完整交互逻辑的项目代码打包开源,大家可以直接在鸿蒙开发环境中导入使用,把更多精力留给项目的核心业务开发~
开发过程

脑子不够用啦,在开发过程中避不开要用到🖊,事实证明挺好用的哈哈哈
成果展示

再结合一下卡牌人,来控制他的位置,我们开发一个低配版王者哈哈(开玩笑)
让我们进入正题吧!!
开发
- 大轮盘和小轮盘
Column() {
Stack() {
//外层大轮盘
Image($r('app.media.turntableBackGround'))
.width(120)
.height(120)
.zIndex(1)
//小运动摇杆
Image($r('app.media.smallTurntable'))
.width(30)
.height(30)
}
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#848484')

-
小轮盘加滑动手势
//小运动摇杆 Image($r('app.media.smallTurntable')) .width(30) .height(30) .translate({ x: this.offsetX, y: this.offsetY, z: 0 }) .zIndex(3) .gesture( PanGesture({ fingers: 1 }) .onActionUpdate((event: GestureEvent) => { if (event) { this.offsetX = this.positionX + event.offsetX this.offsetY = this.positionY + event.offsetY } }) .onActionEnd((event) => { this.offsetX = 0 this.offsetY = 0 }) )

-
给移动的位置加限制
checkBoundary = () => { //限制范围 let distanceZ = parseInt(Math.sqrt(Math.pow(this.offsetX, 2) + Math.pow(this.offsetY, 2)).toString()) if (distanceZ > 45) { this.offsetX = (this.offsetX / distanceZ * 45); this.offsetY = (this.offsetY / distanceZ * 45); } }

这段代码的功能是把offsetX和offsetY这两个坐标所确定的点,限制在以原点(0,0)为中心、半径为45的圆形区域之内。下面来详细解释:
代码分步解析
- 计算距离:
let distanceZ = parseInt(Math.sqrt(Math.pow(this.offsetX, 2) + Math.pow(this.offsetY, 2)).toString())
这里运用了勾股定理,计算从原点(0,0)到点(this.offsetX, this.offsetY)的直线距离。
- 距离判断与坐标调整:
if (distanceZ > 45) {
this.offsetX = (this.offsetX / distanceZ * 45);
this.offsetY = (this.offsetY / distanceZ * 45);
}
当计算出的距离大于45时,就会按比例对offsetX和offsetY进行缩放,使新的坐标点刚好落在半径为45的圆上。
这样的坐标限制常用于游戏或交互界面中,目的是保证操作的范围不会超出合理区间。例如:
- 在虚拟摇杆的设计中,防止用户滑动范围过大。
- 在拖拽操作里,避免元素被拖出指定区域。
举个例子:
假设初始坐标是(offsetX, offsetY) = (60, 30),我们来看看具体的处理过程:
- 计算距离:
distanceZ = √(60² + 30²) = √(3600 + 900) = √4500 ≈ 67.08 - 进行缩放:
- 新的
offsetX = 60 / 67.08 * 45 ≈ 40.25 - 新的
offsetY = 30 / 67.08 * 45 ≈ 20.12
- 新的
- 验证结果:
√(40.25² + 20.12²) ≈ 45,坐标被成功限制在了圆的边界上。

-
加高亮效果
这一部分要根据具体项目具体加效果,我只给小伙伴提供一下思路哦
checkBoundary = () => { //1.限制范围 let distanceZ = parseInt(Math.sqrt(Math.pow(this.offsetX, 2) + Math.pow(this.offsetY, 2)).toString()) if (distanceZ > 45) { this.offsetX = (this.offsetX / distanceZ * 45); this.offsetY = (this.offsetY / distanceZ * 45); } //2.计算角度 设置高亮 let angle = Math.atan2(this.offsetY, this.offsetX); //将弧度转换为角度 this.angleDegrees = Number((angle * (180 / Math.PI)).toFixed(2)) if (this.angleDegrees < 0) { this.angleDegrees += 360; } if (distanceZ > 40) { this.HighlightingAngleShow = true } else { this.HighlightingAngleShow = false } /** * 给小伙伴自己加高亮效果哦,在大轮盘和小轮盘之间加就行了,记得加照片组件加一下zindex(2) */ // if (angleDegrees >= 22.5 && angleDegrees < 67.5) { // console.log('右下') // this.angleResourse = $r('app.media.rightDown') // } else if (angleDegrees >= 67.5 && angleDegrees < 112.5) { // console.log('正下') // this.angleResourse = $r('app.media.down') // } else if (angleDegrees >= 112.5 && angleDegrees < 157.5) { // console.log('左下') // this.angleResourse = $r('app.media.leftDown') // } else if (angleDegrees >= 157.5 && angleDegrees < 202.5) { // console.log('左') // this.angleResourse = $r('app.media.left') // } else if (angleDegrees >= 202.5 && angleDegrees < 247.5) { // console.log('左上') // this.angleResourse = $r('app.media.leftTop') // } else if (angleDegrees >= 247.5 && angleDegrees < 292.5) { // console.log('正上') // this.angleResourse = $r('app.media.top') // } else if (angleDegrees >= 292.5 && angleDegrees < 337.5) { // console.log('右上') // this.angleResourse = $r('app.media.rightTop') // } else { // console.log('右') // this.angleResourse = $r('app.media.right') // } }
这段代码主要完成两个任务:一是计算坐标点相对于原点的角度,二是依据距离来控制高亮显示。下面为你详细解释:
角度计算原理
-
使用
atan2计算弧度:let angle = Math.atan2(this.offsetY, this.offsetX);atan2(y, x)函数会返回从X轴正方向逆时针旋转到点(x, y)所形成的角度,单位是弧度。- 该角度的取值范围是
-π到π(也就是-180°到180°)。 - 例如,点
(1, 1)的角度是π/4(即45°),点(-1, 1)的角度是3π/4(即135°)。
-
弧度转换为角度并取整:
this.angleDegrees = Number((angle * (180 / Math.PI)).toFixed(2))- 通过
angle * (180 / Math.PI)把弧度转换为角度。 toFixed(2)将结果保留两位小数,然后用Number()把字符串转换回数字。
- 通过
-
将负角度转换为0-360度范围:
if (this.angleDegrees < 0) { this.angleDegrees += 360; }-
把负角度(像
-45°)转换为对应的正角度(即315°)。
-
高亮显示控制逻辑
if (distanceZ > 40) { this.HighlightingAngleShow = true } else { this.HighlightingAngleShow = false }- 当坐标点到原点的距离大于40时,就会显示角度高亮效果。
- 结合上一段代码的限制范围(半径45),这个高亮效果会在距离处于40-45之间时显示。
-

开源地址
结语
``
- atan2(y, x)函数会返回从X轴正方向逆时针旋转到点(x, y)所形成的角度,单位是弧度。
- 该角度的取值范围是-π到π(也就是-180°到180°)。
- 例如,点(1, 1)的角度是π/4(即45°),点(-1, 1)的角度是3π/4(即135°)。
-
弧度转换为角度并取整:
this.angleDegrees = Number((angle * (180 / Math.PI)).toFixed(2))- 通过
angle * (180 / Math.PI)把弧度转换为角度。 toFixed(2)将结果保留两位小数,然后用Number()把字符串转换回数字。
- 通过
-
将负角度转换为0-360度范围:
if (this.angleDegrees < 0) { this.angleDegrees += 360; }-
把负角度(像
-45°)转换为对应的正角度(即315°)。
-
高亮显示控制逻辑
if (distanceZ > 40) {
this.HighlightingAngleShow = true
} else {
this.HighlightingAngleShow = false
}
- 当坐标点到原点的距离大于40时,就会显示角度高亮效果。
- 结合上一段代码的限制范围(半径45),这个高亮效果会在距离处于40-45之间时显示。
[外链图片转存中…(img-ALEe1mJq-1747301743629)]
开源地址
结语
技术探索的乐趣就在于不断拆解问题、重构方案,这次从「CV 失败」到「自主实现」的过程,让我更深切体会到:现成代码能节省时间,但理解背后的交互逻辑才能真正解决复杂场景的问题。如果你在开发中遇到类似的手势交互难题,或者想了解鸿蒙系统特有的触摸事件优化技巧,欢迎在评论区留言讨论,我会第一时间和大家讨论,或者也可以关注一下我们的组织——青蓝逐码,我们的官网是https://www.qinglanzhuma.cn/
更多推荐



所有评论(0)