【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开发案例:转盘的更多相关文章

  1. 最全华为鸿蒙 HarmonyOS 开发资料汇总

    开发 本示例基于 OpenHarmony 下的 JavaScript UI 框架,进行项目目录解读,JS FA.常用和自定义组件.用户交互.JS 动画的实现,通过本示例可以基本了解和学习到 JavaS ...

  2. 使用Jquery+EasyUI 进行框架项目开发案例讲解之五 模块(菜单)管理源码分享

    http://www.cnblogs.com/huyong/p/3454012.html 使用Jquery+EasyUI 进行框架项目开发案例讲解之五  模块(菜单)管理源码分享    在上四篇文章 ...

  3. 使用Jquery+EasyUI 进行框架项目开发案例讲解之四 组织机构管理源码分享

    http://www.cnblogs.com/huyong/p/3404647.html 在上三篇文章  <使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享> ...

  4. 使用Jquery+EasyUI 进行框架项目开发案例讲解之三---角色管理源码分享

    使用Jquery+EasyUI 进行框架项目开发案例讲解之三 角色管理源码分享    在上两篇文章  <使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享> ...

  5. 使用Jquery+EasyUI 进行框架项目开发案例讲解之二---用户管理源码分享

    使用Jquery+EasyUI 进行框架项目开发案例讲解之二 用户管理源码分享   在上一篇文章<使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享>我们分享 ...

  6. 【推荐】使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享

    使用Jquery+EasyUI 进行框架项目开发案例讲解之一 员工管理源码分享   在开始讲解之前,我们先来看一下什么是Jquery EasyUI?jQuery EasyUI是一组基于jQuery的U ...

  7. 使用Jquery+EasyUI进行框架项目开发案例解说之中的一个---员工管理源代码分享

    使用Jquery+EasyUI 进行框架项目开发案例解说之中的一个 员工管理源代码分享 在開始解说之前,我们先来看一下什么是Jquery EasyUI?jQuery EasyUI是一组基于jQuery ...

  8. 百度UEditor开发案例(JSP)

    本案例的开发环境:MyEclipse+tomcat+jdk     本案例的开发内容: 用百度编辑器发布新闻(UEditor的初始化开发部署) 编辑已发过的新闻(UEditor的应用——编辑旧文章) ...

  9. 使用Jquery+EasyUI 进行框架项目开发案例解说之二---用户管理源代码分享

    使用Jquery+EasyUI 进行框架项目开发案例解说之二 用户管理源代码分享  在上一篇文章<使用Jquery+EasyUI进行框架项目开发案例解说之中的一个---员工管理源代码分享> ...

  10. 使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享

    使用Jquery+EasyUI进行框架项目开发案例讲解之一---员工管理源码分享 使用Jquery+EasyUI 进行框架项目开发案例讲解之一 员工管理源码分享    在开始讲解之前,我们先来看一下什 ...

随机推荐

  1. 如何让你的C语言程序打印的log多一点色彩?(超级实用)

    接着上一篇文章<由字节对齐引发的一场"血案" > 在平常的调试中,printf字体格式与颜色都是默认一致的. 如果可以根据log信息的重要程度,配以不同的颜色与格式,可 ...

  2. Redis分布式锁防止缓存击穿

    一.Nuget引入 StackExchange.Redis.DistributedLock.Redis依赖 二.使用 StackExchange.Redis 对redis操作做简单封装 public ...

  3. JAVA——instanceof运算符(问题待处理)

    2024/07/12 1.问题 2.问题拆解 3.解决 1.问题 今天学了一个运算符--instanceof,概念很抽象,感觉暂时理解不了,什么实例对象.类.子类,看得迷迷糊糊的,先记录下来,有空做一 ...

  4. [学习笔记]在不同项目中切换Node.js版本

    @ 目录 使用 Node Version Manager (NVM) 安装 NVM 使用 NVM 安装和切换 Node.js 版本 为项目指定 Node.js 版本 使用环境变量指定 Node.js ...

  5. MSI Afterburner 使用

    MSI Afterburner 是一款显卡超频软件,同时可以监测硬件运行数据(CPU 温度.GPU 温度.帧率.帧生成时间等).与其捆绑安装的 RivaTuner Statistics Server ...

  6. 【Python自动化】之运用Git+jenkins集成来运行展示pytest+allure测试报告

    目录: 一.安装allure 二.生成allure报告 三.结合jenkins来集成pytest+allure 四.结合Git集成Jenkins+Pytest+Allure测试报告 五.附录 一.安装 ...

  7. OpenTelemetry 实战:gRPC 监控的实现原理

    前言 最近在给 opentelemetry-java-instrumentation 提交了一个 PR,是关于给 gRPC 新增四个 metrics: rpc.client.request.size: ...

  8. CSS & JS Effect – 脉冲 Pulse Play Button

    效果 参考 Youtube – Create a pulsing animation with CSS 重点 在背后做一个一样大的 div border 然后 animation scale up. ...

  9. 聊聊 iframe, CSP, 安全, 跨域

    refer : https://www.cnblogs.com/kunmomo/p/12131818.html (跨域) https://segmentfault.com/a/119000000450 ...

  10. 全面掌握 Jest:从零开始的测试指南(下篇)

    在上一篇测试指南中,我们介绍了Jest 的背景.如何初始化项目.常用的匹配器语法以及钩子函数的使用.这一篇篇将继续深入探讨 Jest 的高级特性,包括 Mock 函数.异步请求的处理.Mock 请求的 ...