自定义组件之<六>自定义饼状图(PieChart)
9.6:自定义饼状图(PieChart)
本节笔者带领读者实现一个饼状图 PieChart 组件,该组件是依托笔者之前封装的 MiniCanvas 实现的, PieChart 的最终演示效果如下图所示:

9.6.1:饼状图实现的拆分
根据上图的样式效果,实现一个饼状图,实质就是绘制一个个的实心圆弧加上圆弧对应颜色就搞定了,圆弧的大小是根据饼状的数据分布计算出来的,对应的颜色自己指定就可以了,其次手指点击到饼状图,需要找到对应的饼状块并突出显示,找到饼状块先计算手指点击坐标和圆弧中心的夹角,根据夹角和每个圆弧的大小找到对应的圆弧,找到圆弧后计算圆弧的突出偏移量并重置所有饼状块的圆弧起始值就可以了。
- 
  计算夹角 计算夹角就是计算手指点击饼状图上的坐标 (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); }) }
9.6.2:饼状图的实现
拆分完饼状图的步骤后,实现起来就方便多了, PieChart 的完整代码如下所示:
import { MiniCanvas, Paint, ICanvas } from './icanvas'
@Entry @ComponentV2 struct PieChart {
  private delegate?: PieChartDelegate = undefined
  build() {
    Column() {
      MiniCanvas({
        attribute: {
          width: this.delegate?.calculateWidth(),
          height: this.delegate?.calculateHeight(),
          clickListener: (event) => {
            // 根据点击绘制突出的饼状块
            if (event && this.delegate) {
              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测试数据
    let 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;
  public count: number;
  public color: string;
  constructor(count: number, color: string) {
    this.count = count;
    this.color = color;
  }
  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> {
    let 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;
  private pieItems: Array<PieItem>;
  private direction: RotateDirection = RotateDirection.BOTTOM;
  private offset: number = 10;
  private radius: number = 80;
  constructor(pieItems: Array<PieItem>, direction: RotateDirection = RotateDirection.BOTTOM, offset: number = 10, radius: number = 80) {
    this.pieItems = pieItems;
    this.direction = direction;
    this.offset = offset;
    this.radius = radius;
    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) {
      let touchedAngle = this.getTouchedAngle(this.radius, this.radius, x, y);
      let touchedItem = this.getTouchedPieItem(touchedAngle);
      if(touchedItem) {
        let 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) => {
      if (this.paint) {
        this.paint?.setColor(item.color);
        let x = this.calculateCenterX(item.isSelected());
        let 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 {
    let 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() {
    let total = 0;
    this.pieItems.forEach((item) => {
      total += item.count;
    })
    for(let i = 0; i < this.pieItems.length; i++) {
      let data = this.pieItems[i];
      data.setRate(data.count / total);
      data.setAngle(data.getRate() * 360);
      if (i == 0) {
        data.setStartAngle(0);
      } else {
        let preData = this.pieItems[i - 1];
        data.setStartAngle(preData.getStopAngle());
      }
    }
  }
  private getDirectionAngle(): number {
    let 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: number, x: number, y: number) {
    let deltaX = x - centerX;
    let deltaY = centerY - y;
    let t = deltaY / Math.sqrt(deltaX * deltaX + deltaY * deltaY);
    let 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 | null {
    for(let i = 0; i < this.pieItems.length; i++) {
      let 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;
  }
}
9.6.3:小结
本节简单介绍了实现一个饼状图的思路和实现,具体实现读者可以阅读源码,目前 PieChart 在选中饼状块并突出时没有动画特效而是直接旋转过来了,笔者尝试使用 setInterval() 实现,目前没有效果,已经在 OpenHarmony 仓库反馈该问题了,有了结果后笔者会把旋转的动效加上。
备注
作者:灰太狼
來源:坚果派
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。否则追究相关责任。
- 0回答
- 1粉丝
- 0关注
- 自定义组件之<七>自定义组件之插槽(slot)
- 自定义组件之<二>自定义圆环(Ring)
- 自定义组件之<八>自定义下拉刷新(RefreshList)
- 自定义组件之<三>自定义标题栏(TitleBar)
- 自定义组件之<四>自定义对话框(Dialog)
- 自定义组件之<五>自定义对话框(PromptAction)
- 自定义组件之<九>自定义下拉刷新上拉加载(RefreshLayout)
- HarmonyOS Next 之组件之自定义弹窗(CustomDialog)
- 自定义组件之<十>发布开源库
- 自定义组件之<一>组件语法和生命周期
- 自定义Tabbar和我的位置Map和骨架图
- HarmonyOS实战:3秒实现一个自定义轮播图
- HarmonyOS NEXT 鸿蒙实现自定义组件插槽
- @ComponentV2装饰器:自定义组件
- 页面和自定义组件生命周期

