本节笔者带领读者实现一个饼状图 PieChart
组件,该组件是根据笔者之前封装的 MiniCanvas 实现的, PieChart
的最终演示效果如下图所示:
饼状图实现的拆分
根据上图的样式效果,实现一个饼状图,实质就是绘制一个个的实心圆弧加上圆弧对应颜色就搞定了,圆弧的大小是根据饼状的数据分布计算出来的,对应的颜色自己指定就可以了,其次手指点击到饼状图,需要找到对应的饼状块并突出显示,找到饼状块先计算手指点击坐标和圆弧中心的夹角,根据夹角和每个圆弧的大小找到对应的圆弧,找到圆弧后计算圆弧的突出偏移量并重置所有饼状块的圆弧起始值就可以了。
- 计算夹角
计算夹角就是计算手指点击饼状图上的坐标 (x, y) 和饼状图的圆心坐标 (centerX, centerY) 之间的顺时针角度,计算方法如下所示:
private getTouchedAngle(centerX: number, centerY, x: number, y: number) {
var deltaX = x - centerX;
var deltaY = centerY - y;
var t = deltaY / Math.sqrt(deltaX * deltaX + deltaY * deltaY);
var angle = 0;
if (deltaX > 0) {
if (deltaY > 0) {
angle = Math.asin(t);
} else {
angle = Math.PI * 2 + Math.asin(t);
}
} else if (deltaY > 0) {
angle = Math.PI - Math.asin(t);
} else {
angle = Math.PI - Math.asin(t);
}
return 360 - (angle * 180 / Math.PI) % 360;
}
- 找圆弧块
计算出手指点击位置和圆心的夹角后,遍历每一个饼状块做比较就可以了,代码如下所示:
private getTouchedPieItem(angle: number): PieItem {
for(var i = 0; i < this.pieItems.length; i++) {
var item = this.pieItems[i];
if(item.getStopAngle() < 360) {
if(angle >= item.getStartAngle() && angle < item.getStopAngle()) {
return item;
}
} else {
if(angle >= item.getStartAngle() && angle < 360 || (angle >= 0 && angle < item.getStopAngle() - 360)) {
return item;
}
}
}
return null;
}
- 计算偏移量
找到圆弧块后,根据圆弧块的圆弧大小,计算出该圆弧突出后的偏移量,代码如下所示:
private calculateRoteAngle(item: PieItem): number {
var result = item.getStartAngle() + item.getAngle() / 2 + this.getDirectionAngle();
if (result >= 360) {
result -= 360;
}
if (result <= 180) {
result = -result;
} else {
result = 360 - result;
}
return result;
}
- 重置偏移量
有了目标圆弧块的偏移角度后,重置每一个圆弧块的起始偏移量就可以了,代码如下所示:
private resetStartAngle(angle: number) {
this.pieItems.forEach((item) => {
item.setSelected(false);
item.setStartAngle(item.getStartAngle() + angle);
});
}
- 重新绘制圆弧
绘制圆弧使用 MiniCanvas 提供的drawArc()
方法即可,代码如下所示:
drawPieItem() {
this.pieItems.forEach((item) => {
this.paint.setColor(item.color);
var x = this.calculateCenterX(item.isSelected());
var y = this.calculateCenterY(item.isSelected());
this.canvas.drawArc(x, y, this.radius, item.getStartAngle(), item.getStopAngle(), this.paint);
})
}
饼状图的实现
拆分完饼状图的步骤后,实现起来就方便多了, PieChart
的完整代码如下所示:
import { MiniCanvas, Paint, ICanvas } from './icanvas'
@Entry @Component struct PieChart {
private delegate: PieChartDelegate;
build() {
Column() {
MiniCanvas({
attribute: {
width: this.delegate.calculateWidth(),
height: this.delegate.calculateHeight(),
clickListener: (event) => {
// 根据点击绘制突出的饼状块
this.delegate.onClicked(event.x, event.y);
}
},
onDraw: (canvas) => {
// 开始绘制
this.delegate.setCanvas(canvas);
this.delegate.drawPieItem();
}
})
}
.padding(10)
.size({width: "100%", height: "100%"})
}
aboutToAppear() {
// mock测试数据
var pieItems = PieItem.mock();
// 初始化delegate
this.delegate = new PieChartDelegate(pieItems, RotateDirection.BOTTOM);
}
}
// 定义饼状块的属性,包括角度,起始角度,占比,颜色,是否选中突出
export class PieItem {
private startAngle: number = 0;
private rate: number = 0;
private angle: number = 0;
private selected: boolean = false;
constructor(public count: number, public color: string) {
}
setSelected(selected: boolean) {
this.selected = selected;
return this;
}
isSelected() {
return this.selected;
}
setStartAngle(startAngle: number) {
this.startAngle = startAngle > 360 ? startAngle - 360 : startAngle < 0 ? 360 + startAngle : startAngle;
return this;
}
getStartAngle() {
return this.startAngle;
}
getStopAngle() {
return this.startAngle + this.angle;
}
setRate(rate: number) {
this.rate = rate;
return this;
}
getRate() {
return this.rate;
}
setAngle(angle: number) {
this.angle = angle;
return this;
}
getAngle() {
return this.angle;
}
// mock一份测试数据
static mock(): Array<PieItem> {
var pieItems = new Array<PieItem>();
pieItems.push(new PieItem(21, "#6A5ACD"))
pieItems.push(new PieItem(18, "#20B2AA"))
pieItems.push(new PieItem(29, "#FFFF00"))
pieItems.push(new PieItem(12, "#00BBFF"))
pieItems.push(new PieItem(20, "#DD5C5C"))
pieItems.push(new PieItem(13, "#8B668B"))
return pieItems;
}
}
// 饼状块的突出方向
export enum RotateDirection {
LEFT,
TOP,
RIGHT,
BOTTOM
}
// 饼状图绘制的具体实现类
class PieChartDelegate {
private paint: Paint;
private canvas: ICanvas;
constructor(private pieItems: Array<PieItem>, private direction: RotateDirection = RotateDirection.BOTTOM, private offset: number = 10, private radius: number = 80) {
this.calculateItemAngle();
}
setPitItems(pieItems: Array<PieItem>) {
this.pieItems = pieItems;
}
setCanvas(canvas: ICanvas) {
this.canvas = canvas;
this.paint = new Paint();
}
onClicked(x: number, y: number) {
if(this.canvas) {
var touchedAngle = this.getTouchedAngle(this.radius, this.radius, x, y);
var touchedItem = this.getTouchedPieItem(touchedAngle);
if(touchedItem) {
var rotateAngle = this.calculateRoteAngle(touchedItem);
this.resetStartAngle(rotateAngle);
touchedItem.setSelected(true)
this.clearCanvas();
this.drawPieItem();
}
} else {
console.warn("canvas invalid!!!")
}
}
clearCanvas() {
this.canvas.clear();
}
drawPieItem() {
this.pieItems.forEach((item) => {
this.paint.setColor(item.color);
var x = this.calculateCenterX(item.isSelected());
var y = this.calculateCenterY(item.isSelected());
this.canvas.drawArc(x, y, this.radius, item.getStartAngle(), item.getStopAngle(), this.paint);
})
}
calculateWidth(): number {
if (this.direction == RotateDirection.LEFT || this.direction == RotateDirection.RIGHT) {
return this.radius * 2 + this.offset;
} else {
return this.radius * 2;
}
}
calculateHeight(): number {
if (this.direction == RotateDirection.TOP || this.direction == RotateDirection.BOTTOM) {
return this.radius * 2 + this.offset;
} else {
return this.radius * 2;
}
}
private calculateCenterX(hint: boolean): number {
if(this.direction == RotateDirection.LEFT) {
return hint ? this.radius : this.radius + this.offset;
} else if(this.direction == RotateDirection.TOP) {
return this.radius;
} else if(this.direction == RotateDirection.RIGHT) {
return hint ? this.radius + this.offset : this.radius;
} else {
return this.radius;
}
}
private calculateCenterY(hint: boolean): number {
if(this.direction == RotateDirection.LEFT) {
return this.radius;
} else if(this.direction == RotateDirection.TOP) {
return hint ? this.radius : this.radius + this.offset;
} else if(this.direction == RotateDirection.RIGHT) {
return this.radius;
} else {
return hint ? this.radius + this.offset : this.radius;
}
}
private resetStartAngle(angle: number) {
this.pieItems.forEach((item) => {
item.setSelected(false);
item.setStartAngle(item.getStartAngle() + angle);
});
}
private calculateRoteAngle(item: PieItem): number {
var result = item.getStartAngle() + item.getAngle() / 2 + this.getDirectionAngle();
if (result >= 360) {
result -= 360;
}
if (result <= 180) {
result = -result;
} else {
result = 360 - result;
}
return result;
}
private calculateItemAngle() {
var total = 0;
this.pieItems.forEach((item) => {
total += item.count;
})
for(var i = 0; i < this.pieItems.length; i++) {
var data = this.pieItems[i];
data.setRate(data.count / total);
data.setAngle(data.getRate() * 360);
if (i == 0) {
data.setStartAngle(0);
} else {
var preData = this.pieItems[i - 1];
data.setStartAngle(preData.getStopAngle());
}
}
}
private getDirectionAngle(): number {
var result = 270;
if (this.direction == RotateDirection.RIGHT) {
result = 0;
}
if (this.direction == RotateDirection.BOTTOM) {
result = 270;
}
if (this.direction == RotateDirection.LEFT) {
result = 180;
}
if (this.direction == RotateDirection.TOP) {
result = 90;
}
return result;
}
private getTouchedAngle(centerX: number, centerY, x: number, y: number) {
var deltaX = x - centerX;
var deltaY = centerY - y;
var t = deltaY / Math.sqrt(deltaX * deltaX + deltaY * deltaY);
var angle = 0;
if (deltaX > 0) {
if (deltaY > 0) {
angle = Math.asin(t);
} else {
angle = Math.PI * 2 + Math.asin(t);
}
} else if (deltaY > 0) {
angle = Math.PI - Math.asin(t);
} else {
angle = Math.PI - Math.asin(t);
}
return 360 - (angle * 180 / Math.PI) % 360;
}
private getTouchedPieItem(angle: number): PieItem {
for(var i = 0; i < this.pieItems.length; i++) {
var item = this.pieItems[i];
if(item.getStopAngle() < 360) {
if(angle >= item.getStartAngle() && angle < item.getStopAngle()) {
return item;
}
} else {
if(angle >= item.getStartAngle() && angle < 360 || (angle >= 0 && angle < item.getStopAngle() - 360)) {
return item;
}
}
}
return null;
}
}
以上就是笔者介绍的实现一个饼状图的思路和实现,读者可以阅读源码,目前 PieChart
在选中饼状块并突出时没有动画特效而是直接旋转过来了,后续笔者会把旋转的动效加上。