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

饼状图实现的拆分

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

  • 计算夹角

计算夹角就是计算手指点击饼状图上的坐标 (x, y) 和饼状图的圆心坐标 (centerX, centerY) 之间的顺时针角度,计算方法如下所示:

ini 复制代码
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;
}
  • 找圆弧块

计算出手指点击位置和圆心的夹角后,遍历每一个饼状块做比较就可以了,代码如下所示:

kotlin 复制代码
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;
}
  • 计算偏移量

找到圆弧块后,根据圆弧块的圆弧大小,计算出该圆弧突出后的偏移量,代码如下所示:

ini 复制代码
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;
}
  • 重置偏移量

有了目标圆弧块的偏移角度后,重置每一个圆弧块的起始偏移量就可以了,代码如下所示:

typescript 复制代码
private resetStartAngle(angle: number) {
  this.pieItems.forEach((item) => {
    item.setSelected(false);
    item.setStartAngle(item.getStartAngle() + angle);
  });
}
  • 重新绘制圆弧

绘制圆弧使用 MiniCanvas 提供的 drawArc() 方法即可,代码如下所示:

kotlin 复制代码
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 的完整代码如下所示:

kotlin 复制代码
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 在选中饼状块并突出时没有动画特效而是直接旋转过来了。

相关推荐
sanzk2 小时前
华为鸿蒙应用开发
华为·harmonyos
SoraLuna6 小时前
「Mac畅玩鸿蒙与硬件28」UI互动应用篇5 - 滑动选择器实现
macos·ui·harmonyos
ClkLog-开源埋点用户分析7 小时前
ClkLog企业版(CDP)预售开启,更有鸿蒙SDK前来助力
华为·开源·开源软件·harmonyos
mg6687 小时前
鸿蒙系统的优势 开发 环境搭建 开发小示例
华为·harmonyos
lqj_本人8 小时前
鸿蒙next选择 Flutter 开发跨平台应用的原因
flutter·华为·harmonyos
lqj_本人8 小时前
使用 Flutter 绘制一个棋盘
harmonyos
lqj_本人11 小时前
Flutter&鸿蒙next 状态管理框架对比分析
flutter·华为·harmonyos
青瓷看世界12 小时前
华为HarmonyOS打造开放、合规的广告生态 - 插屏广告
华为·harmonyos·广告投放
青瓷看世界12 小时前
华为HarmonyOS借助AR引擎帮助应用实现虚拟与现实交互的能力2-管理AR会话
华为·ar·harmonyos·虚拟现实
2301_7955586413 小时前
鸿蒙的进化史
华为·harmonyos