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

发布于 1周前 作者 yibo5220 来自 鸿蒙OS

《ArkUI实战》之HarmonyOS 鸿蒙Next自定义饼状图组件PieChart
<markdown _ngcontent-evb-c237="" class="markdownPreContainer">

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

www.arkui.club

饼状图实现的拆分

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

  • 计算夹角

计算夹角就是计算手指点击饼状图上的坐标 (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;
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>
  • 找圆弧块

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

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;
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>
  • 计算偏移量

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

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;
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>
  • 重置偏移量

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

private resetStartAngle(angle: number) {
  this.pieItems.forEach((item) => {
    item.setSelected(false);
    item.setStartAngle(item.getStartAngle() + angle);
  });
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>
  • 重新绘制圆弧

绘制圆弧使用 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);
  })
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

饼状图的实现

拆分完饼状图的步骤后,实现起来就方便多了, 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; })

<span class="hljs-keyword"><span class="hljs-keyword">for</span></span>(<span class="hljs-keyword"><span class="hljs-keyword">var</span></span> i = <span class="hljs-number"><span class="hljs-number">0</span></span>; i &lt; <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.pieItems.length; i++) {
  <span class="hljs-keyword"><span class="hljs-keyword">var</span></span> data = <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.pieItems[i];
  data.setRate(data.count / total);
  data.setAngle(data.getRate() * <span class="hljs-number"><span class="hljs-number">360</span></span>);
  <span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (i == <span class="hljs-number"><span class="hljs-number">0</span></span>) {
    data.setStartAngle(<span class="hljs-number"><span class="hljs-number">0</span></span>);
  } <span class="hljs-keyword"><span class="hljs-keyword">else</span></span> {
    <span class="hljs-keyword"><span class="hljs-keyword">var</span></span> preData = <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.pieItems[i - <span class="hljs-number"><span class="hljs-number">1</span></span>];
    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; } } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

以上就是笔者介绍的实现一个饼状图的思路和实现,具体实现读者可以阅读源码,目前 PieChart 在选中饼状块并突出时没有动画特效而是直接旋转过来了,后续笔者会把旋转的动效加上。

</markdown>

关于《ArkUI实战》之HarmonyOS 鸿蒙Next自定义饼状图组件PieChart的问题,您也可以访问:https://www.itying.com/category-93-b0.html 联系官网客服。
9 回复

找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17

找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17

HarmonyOS的开发者模式提供了很多实用的工具,方便我们进行调试和优化。

向大神学习
回到顶部