《HarmonyOS-ArkUI实战》之自定义饼状图组件PieChart

30 篇文章 0 订阅
30 篇文章 0 订阅

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

0890086000102259286.20230223005249.29803389542507851928705587469454.gif

饼状图实现的拆分

根据上图的样式效果,实现一个饼状图,实质就是绘制一个个的实心圆弧加上圆弧对应颜色就搞定了,圆弧的大小是根据饼状的数据分布计算出来的,对应的颜色自己指定就可以了,其次手指点击到饼状图,需要找到对应的饼状块并突出显示,找到饼状块先计算手指点击坐标和圆弧中心的夹角,根据夹角和每个圆弧的大小找到对应的圆弧,找到圆弧后计算圆弧的突出偏移量并重置所有饼状块的圆弧起始值就可以了。

  • 计算夹角

计算夹角就是计算手指点击饼状图上的坐标 (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 在选中饼状块并突出时没有动画特效而是直接旋转过来了,后续笔者会把旋转的动效加上。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值