
【1】引言
在鸿蒙NEXT系统中,开发一个有趣且实用的转盘应用不仅可以提升用户体验,还能展示鸿蒙系统的强大功能。本文将介绍如何使用鸿蒙NEXT系统开发一个转盘应用,涵盖从组件定义到用户交互的过程。
【2】环境准备
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:mate60 pro
语言:ArkTS、ArkUI
【3】难点分析
- 扇形路径的计算
难点:创建扇形的路径需要精确计算起始点、结束点和弧线参数。尤其是涉及到三角函数的使用,初学者可能会对如何将角度转换为坐标感到困惑。
解决方案:可以通过绘制简单的示意图来帮助理解扇形的构造,并在代码中添加详细注释,解释每一步的计算过程。
- 动态角度计算
难点:在转盘旋转时,需要根据单元格的比例动态计算每个单元格的角度和旋转角度。这涉及到累加和比例计算,可能会导致逻辑错误。
解决方案:使用数组的 reduce 方法来计算总比例,并在计算每个单元格的角度时,确保逻辑清晰。可以通过单元测试来验证每个单元格的角度是否正确。
- 动画效果的实现
难点:实现转盘的旋转动画需要对动画的持续时间、曲线和结束后的状态进行管理。初学者可能会对如何控制动画的流畅性和效果感到困惑。
解决方案:可以参考鸿蒙NEXT的动画文档,了解不同的动画效果和参数设置。通过逐步调试,观察动画效果并进行调整。
- 用户交互的处理
难点:处理用户点击事件,尤其是在动画进行时,如何禁用按钮以防止重复点击,可能会导致状态管理的复杂性。
解决方案:在按钮的点击事件中,使用状态变量(如 isAnimating)来控制按钮的可用性,并在动画结束后恢复按钮的状态。
- 组件的状态管理
难点:在多个组件之间传递状态(如当前选中的单元格、转盘的角度等)可能会导致状态管理混乱。
解决方案:使用状态管理工具(如 @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;
const centerY = radius / 2;
const startX = centerX;
const startY = centerY - radius;
const halfAngle = angle / 4;
const endX1 = centerX + radius * Math.cos((halfAngle * Math.PI) / 180);
const endY1 = centerY - radius * Math.sin((halfAngle * Math.PI) / 180);
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;
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)
.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)
.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,
curve: Curve.EaseInOut,
onFinish: () => {
this.currentAngle %= 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%')
.width('100%')
.backgroundColor("#f5f8ff");
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.