鸿蒙NEXT开发案例:转盘

【1】引言(完整代码在最后面)
在鸿蒙NEXT系统中,开发一个有趣且实用的转盘应用不仅可以提升用户体验,还能展示鸿蒙系统的强大功能。本文将详细介绍如何使用鸿蒙NEXT系统开发一个转盘应用,涵盖从组件定义到用户交互的完整过程。
【2】环境准备
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:mate60 pro
语言:ArkTS、ArkUI
【3】难点分析
1. 扇形路径的计算
难点:创建扇形的路径需要精确计算起始点、结束点和弧线参数。尤其是涉及到三角函数的使用,初学者可能会对如何将角度转换为坐标感到困惑。
解决方案:可以通过绘制简单的示意图来帮助理解扇形的构造,并在代码中添加详细注释,解释每一步的计算过程。
2. 动态角度计算
难点:在转盘旋转时,需要根据单元格的比例动态计算每个单元格的角度和旋转角度。这涉及到累加和比例计算,可能会导致逻辑错误。
解决方案:使用数组的 reduce 方法来计算总比例,并在计算每个单元格的角度时,确保逻辑清晰。可以通过单元测试来验证每个单元格的角度是否正确。
3. 动画效果的实现
难点:实现转盘的旋转动画需要对动画的持续时间、曲线和结束后的状态进行管理。初学者可能会对如何控制动画的流畅性和效果感到困惑。
解决方案:可以参考鸿蒙NEXT的动画文档,了解不同的动画效果和参数设置。通过逐步调试,观察动画效果并进行调整。
4. 用户交互的处理
难点:处理用户点击事件,尤其是在动画进行时,如何禁用按钮以防止重复点击,可能会导致状态管理的复杂性。
解决方案:在按钮的点击事件中,使用状态变量(如 isAnimating)来控制按钮的可用性,并在动画结束后恢复按钮的状态。
5. 组件的状态管理
难点:在多个组件之间传递状态(如当前选中的单元格、转盘的角度等)可能会导致状态管理混乱。
解决方案:使用状态管理工具(如 @State 和 @Trace)来确保状态的统一管理,并在需要的地方进行状态更新,保持组件之间的解耦。
【完整代码】
import { CounterComponent, CounterType } from '@kit.ArkUI'; // 导入计数器组件和计数器类型
// 定义扇形组件
@Component
struct Sector {
@Prop radius: number; // 扇形的半径
@Prop angle: number; // 扇形的角度
@Prop color: string; // 扇形的颜色
// 创建扇形路径的函数
createSectorPath(radius: number, angle: number): string {
const centerX = radius / 2; // 计算扇形中心的X坐标
const centerY = radius / 2; // 计算扇形中心的Y坐标
const startX = centerX; // 扇形起始点的X坐标
const startY = centerY - radius; // 扇形起始点的Y坐标
const halfAngle = angle / 4; // 计算半个角度
// 计算扇形结束点1的坐标
const endX1 = centerX + radius * Math.cos((halfAngle * Math.PI) / 180);
const endY1 = centerY - radius * Math.sin((halfAngle * Math.PI) / 180);
// 计算扇形结束点2的坐标
const endX2 = centerX + radius * Math.cos((-halfAngle * Math.PI) / 180);
const endY2 = centerY - radius * Math.sin((-halfAngle * Math.PI) / 180);
// 判断是否为大弧
const largeArcFlag = angle / 2 > 180 ? 1 : 0;
const sweepFlag = 1; // 设置弧线方向为顺时针
// 生成SVG路径命令
const pathCommands =
`M${startX} ${startY} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${endX1} ${endY1} L${centerX} ${centerY} L${endX2} ${endY2} A${radius} ${radius} 0 ${largeArcFlag} ${1 -
sweepFlag} ${startX} ${startY} Z`;
return pathCommands; // 返回路径命令
}
// 构建扇形组件
build() {
Stack() {
// 创建第一个扇形路径
Path()
.width(`${this.radius}px`) // 设置宽度为半径
.height(`${this.radius}px`) // 设置高度为半径
.commands(this.createSectorPath(this.radius, this.angle)) // 设置路径命令
.fillOpacity(1) // 设置填充透明度
.fill(this.color) // 设置填充颜色
.strokeWidth(0) // 设置边框宽度为0
.rotate({ angle: this.angle / 4 - 90 }); // 旋转扇形
// 创建第二个扇形路径
Path()
.width(`${this.radius}px`) // 设置宽度为半径
.height(`${this.radius}px`) // 设置高度为半径
.commands(this.createSectorPath(this.radius, this.angle)) // 设置路径命令
.fillOpacity(1) // 设置填充透明度
.fill(this.color) // 设置填充颜色
.strokeWidth(0) // 设置边框宽度为0
.rotate({ angle: 180 - (this.angle / 4 - 90) }); // 旋转扇形
}
}
}
// 定义单元格类
@ObservedV2
class Cell {
@Trace angle: number = 0; // 扇形的角度
@Trace title: string; // 当前格子的标题
@Trace color: string; // 背景颜色
@Trace rotate: number = 0; // 在转盘要旋转的角度
angleStart: number = 0; // 轮盘所在区间的起始
angleEnd: number = 0; // 轮盘所在区间的结束
proportion: number = 0; // 所占比例
// 构造函数
constructor(proportion: number, title: string, color: string) {
this.proportion = proportion; // 设置比例
this.title = title; // 设置标题
this.color = color; // 设置颜色
}
}
// 定义转盘组件
@Entry
@Component
struct Wheel {
@State cells: Cell[] = []; // 存储单元格的数组
@State wheelWidth: number = 600; // 转盘的宽度
@State currentAngle: number = 0; // 当前转盘的角度
@State selectedName: string = ""; // 选中的名称
isAnimating: boolean = false; // 动画状态
colorIndex: number = 0; // 颜色索引
colorPalette: string[] = [ // 颜色调色板
"#26c2ff",
"#978efe",
"#c389fe",
"#ff85bd",
"#ff7051",
"#fea800",
"#ffcf18",
"#a9c92a"
];
// 组件即将出现时调用
aboutToAppear(): void {
// 初始化单元格
this.cells.push(new Cell(1, "跑步", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
this.cells.push(new Cell(2, "跳绳", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
this.cells.push(new Cell(1, "唱歌", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
this.cells.push(new Cell(4, "跳舞", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
this.calculateAngles(); // 计算角度
}
// 计算每个单元格的角度
private calculateAngles() {
// 根据比例计算总比例
const totalProportion = this.cells.reduce((sum, cell) => sum + cell.proportion, 0);
this.cells.forEach(cell => {
cell.angle = (cell.proportion * 360) / totalProportion; // 计算每个单元格的角度
});
let cumulativeAngle = 0; // 累计角度
this.cells.forEach(cell => {
cell.angleStart = cumulativeAngle; // 设置起始角度
cumulativeAngle += cell.angle; // 更新累计角度
cell.angleEnd = cumulativeAngle; // 设置结束角度
cell.rotate = cumulativeAngle - (cell.angle / 2); // 计算旋转角度
});
}
// 构建转盘组件
build() {
Column() {
Row() {
Text('转盘').fontSize(20).fontColor("#0b0e15"); // 显示转盘标题
}.width('100%').height(44).justifyContent(FlexAlign.Center); // 设置行的宽度和高度
// 显示当前状态
Text(this.isAnimating ? '旋转中' : `${this.selectedName}`).fontSize(20).fontColor("#0b0e15").height(40);
Stack() {
Stack() {
// 遍历每个单元格并绘制扇形
ForEach(this.cells, (cell: Cell) => {
Stack() {
Sector({ radius: lpx2px(this.wheelWidth) / 2, angle: cell.angle, color: cell.color }); // 创建扇形
Text(cell.title).fontColor(Color.White).margin({ bottom: `${this.wheelWidth / 1.4}lpx` }); // 显示单元格标题
}.width('100%').height('100%').rotate({ angle: cell.rotate }); // 设置宽度和高度,并旋转
});
}
.borderRadius('50%') // 设置圆角
.backgroundColor(Color.Gray) // 设置背景颜色
.width(`${this.wheelWidth}lpx`) // 设置转盘宽度
.height(`${this.wheelWidth}lpx`) // 设置转盘高度
.rotate({ angle: this.currentAngle }); // 旋转转盘
// 创建指针
Polygon({ width: 20, height: 10 })
.points([[0, 0], [10, -20], [20, 0]]) // 设置指针的点
.fill("#d72b0b") // 设置指针颜色
.height(20) // 设置指针高度
.margin({ bottom: '140lpx' }); // 设置指针底部边距
// 创建开始按钮
Button('开始')
.fontColor("#c53a2c") // 设置按钮字体颜色
.borderWidth(10) // 设置按钮边框宽度
.borderColor("#dd2218") // 设置按钮边框颜色
.backgroundColor("#fde427") // 设置按钮背景颜色
.width('200lpx') // 设置按钮宽度
.height('200lpx') // 设置按钮高度
.borderRadius('50%') // 设置按钮为圆形
.clickEffect({ level: ClickEffectLevel.LIGHT }) // 设置点击效果
.onClick(() => { // 点击按钮时的回调函数
if (this.isAnimating) { // 如果正在动画中,返回
return;
}
this.selectedName = ""; // 清空选中的名称
this.isAnimating = true; // 设置动画状态为正在动画
animateTo({ // 开始动画
duration: 5000, // 动画持续时间为5000毫秒
curve: Curve.EaseInOut, // 动画曲线为缓入缓出
onFinish: () => { // 动画完成后的回调
this.currentAngle %= 360; // 保持当前角度在0到360之间
for (const cell of this.cells) { // 遍历每个单元格
// 检查当前角度是否在单元格的角度范围内
if (360 - this.currentAngle >= cell.angleStart && 360 - this.currentAngle <= cell.angleEnd) {
this.selectedName = cell.title; // 设置选中的名称为当前单元格的标题
break; // 找到后退出循环
}
}
this.isAnimating = false; // 设置动画状态为未动画
},
}, () => { // 动画进行中的回调
this.currentAngle += (360 * 5 + Math.floor(Math.random() * 360)); // 更新当前角度,增加随机旋转
});
});
}
// 创建滚动区域
Scroll() {
Column() {
// 遍历每个单元格,创建输入框和计数器
ForEach(this.cells, (item: Cell, index: number) => {
Row() {
// 创建文本输入框,显示单元格标题
TextInput({ text: item.title })
.layoutWeight(1) // 设置输入框占据剩余空间
.onChange((value) => { // 输入框内容变化时的回调
item.title = value; // 更新单元格标题
});
// 创建计数器组件
CounterComponent({
options: {
type: CounterType.COMPACT, // 设置计数器类型为紧凑型
numberOptions: {
label: `当前占比`, // 设置计数器标签
value: item.proportion, // 设置计数器初始值
min: 1, // 设置最小值
max: 100, // 设置最大值
step: 1, // 设置步长
onChange: (value: number) => { // 计数器值变化时的回调
item.proportion = value; // 更新单元格的比例
this.calculateAngles(); // 重新计算角度
}
}
}
});
// 创建删除按钮
Button('删除').onClick(() => {
this.cells.splice(index, 1); // 从单元格数组中删除当前单元格
this.calculateAngles(); // 重新计算角度
});
}.width('100%').justifyContent(FlexAlign.SpaceBetween) // 设置行的宽度和内容对齐方式
.padding({ left: 40, right: 40 }); // 设置左右内边距
});
}.layoutWeight(1); // 设置滚动区域占据剩余空间
}.layoutWeight(1) // 设置滚动区域占据剩余空间
.margin({ top: 20, bottom: 20 }) // 设置上下外边距
.align(Alignment.Top); // 设置对齐方式为顶部对齐
// 创建添加新内容按钮
Button('添加新内容').onClick(() => {
// 向单元格数组中添加新单元格
this.cells.push(new Cell(1, "新内容", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
this.calculateAngles(); // 重新计算角度
}).margin({ top: 20, bottom: 20 }); // 设置按钮的上下外边距
}
.height('100%') // 设置组件高度为100%
.width('100%') // 设置组件宽度为100%
.backgroundColor("#f5f8ff"); // 设置组件背景颜色
}
}
鸿蒙NEXT开发案例:转盘的更多相关文章
- 最全华为鸿蒙 HarmonyOS 开发资料汇总
开发 本示例基于 OpenHarmony 下的 JavaScript UI 框架,进行项目目录解读,JS FA.常用和自定义组件.用户交互.JS 动画的实现,通过本示例可以基本了解和学习到 JavaS ...
- 使用Jquery+EasyUI 进行框架项目开发案例讲解之五 模块(菜单)管理源码分享
http://www.cnblogs.com/huyong/p/3454012.html 使用Jquery+EasyUI 进行框架项目开发案例讲解之五 模块(菜单)管理源码分享 在上四篇文章 ...
- 使用Jquery+EasyUI 进行框架项目开发案例讲解之四 组织机构管理源码分享
http://www.cnblogs.com/huyong/p/3404647.html 在上三篇文章 <使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享> ...
- 使用Jquery+EasyUI 进行框架项目开发案例讲解之三---角色管理源码分享
使用Jquery+EasyUI 进行框架项目开发案例讲解之三 角色管理源码分享 在上两篇文章 <使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享> ...
- 使用Jquery+EasyUI 进行框架项目开发案例讲解之二---用户管理源码分享
使用Jquery+EasyUI 进行框架项目开发案例讲解之二 用户管理源码分享 在上一篇文章<使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享>我们分享 ...
- 【推荐】使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享
使用Jquery+EasyUI 进行框架项目开发案例讲解之一 员工管理源码分享 在开始讲解之前,我们先来看一下什么是Jquery EasyUI?jQuery EasyUI是一组基于jQuery的U ...
- 使用Jquery+EasyUI进行框架项目开发案例解说之中的一个---员工管理源代码分享
使用Jquery+EasyUI 进行框架项目开发案例解说之中的一个 员工管理源代码分享 在開始解说之前,我们先来看一下什么是Jquery EasyUI?jQuery EasyUI是一组基于jQuery ...
- 百度UEditor开发案例(JSP)
本案例的开发环境:MyEclipse+tomcat+jdk 本案例的开发内容: 用百度编辑器发布新闻(UEditor的初始化开发部署) 编辑已发过的新闻(UEditor的应用——编辑旧文章) ...
- 使用Jquery+EasyUI 进行框架项目开发案例解说之二---用户管理源代码分享
使用Jquery+EasyUI 进行框架项目开发案例解说之二 用户管理源代码分享 在上一篇文章<使用Jquery+EasyUI进行框架项目开发案例解说之中的一个---员工管理源代码分享> ...
- 使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享
使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享 使用Jquery+EasyUI 进行框架项目开发案例讲解之一 员工管理源码分享 在开始讲解之前,我们先来看一下什 ...
随机推荐
- MFC连接Access2007数据库
// TODO: 在此添加额外的初始化代码 //初始化ADO环境 if (!AfxOleInit()) { AfxMessageBox(L"OLE初始化失败"); return F ...
- layui的各模块的基本用途是什么?
Layui 是一个流行的前端UI框架,提供了多个模块,每个模块有不同的功能和用途.以下是 Layui 的一些主要模块以及它们的基本用途: 核心模块(layui): Layui 的核心模块包含了一些核心 ...
- 怎样在局域网中给网站作ssl认证,使其能以https协议访问(转)
如果要在局域网达到效果需要满足以下几点要求: 1. 证书由可信任的CA机构颁发 2. 证书在有效期 3. 访问地址和证书的认证地址一致 说明: 1. 需要在局域网内构建CA机构 2. 证书的有效期建议 ...
- android 访问域名接口报错
1. 移动端访问https域名及接口,显示 java.net.UnknownHostException: Unable to resolve host "xxx" : No add ...
- 【YashanDB知识库】如何使用yasldr导入lob类型?
问题现象 在各个项目实施中,有时候会使用到yasldr工具进行csv数据的导入.关于yasldr一般的使用方法,官方文档已经有详细的介绍,具体可见:yasldr使用指导.但在涉及LOB类型的导入时,会 ...
- Linux firewalld 的用法
大乌龙事件之防火墙作祟: 在我的初始化环境中 selinux.iptables.firewalld 是关闭的, 莫名的firewalld 是没有关闭的这是我在安装完docker后一段时间后部署jenk ...
- Qml 实现瀑布流布局
[写在前面] 最近在刷掘金的时候看到一篇关于瀑布流布局的文章,然鹅他们的实现都是前端的那套,就想着 Qml 有没有类似实现. 结果百度了一圈也没有( T_T Qml 凉了凉了 ),于是,我按照自己理解 ...
- Figma 学习笔记 – 黑科技
Figma 其实有蛮多黑科技的, 因为需求真的太多了, 在还没有实现的期间就诞生了很多 hacking 的 way. Feature Issue 实现 min-height 通过 0px 黑科技 其实 ...
- AJAX——简介
AJAX 同步与异步 AJAX 快速入门
- 【赵渝强老师】什么是Redis Cluster
(一)什么是Redis Cluster? Redis Cluster是Redis的分布式解决方案,在Redis 3.0版本正式推出的,有效解决了Redis分布式方面的需求.当遇到单机内存.并发.流量等 ...