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

本节笔者带领读者实现一个饼状图 PieChart 组件, 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 在选中饼状块并突出时没有动画特效而是直接旋转过来了。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值