【鸿蒙实战开发】ArkUI实战之自定义饼状图组件PieChart

71 篇文章 0 订阅
65 篇文章 0 订阅

本节笔者带领读者实现一个饼状图 PieChart 组件,该组件是依托笔者之前封装的 MiniCanvas 实现的, 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)文档用来跟着学习是非常有必要的。

这份鸿蒙(HarmonyOS NEXT)文档包含了鸿蒙开发必掌握的核心知识要点,内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony南向开发、鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。

希望这一份鸿蒙学习文档能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!

获取这份完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习文档

鸿蒙(HarmonyOS NEXT)5.0最新学习路线

在这里插入图片描述

有了路线图,怎么能没有学习文档呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。

获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习文档

《鸿蒙 (OpenHarmony)开发入门教学视频》

在这里插入图片描述

《鸿蒙生态应用开发V3.0白皮书》

在这里插入图片描述

《鸿蒙 (OpenHarmony)开发基础到实战手册》

OpenHarmony北向、南向开发环境搭建

在这里插入图片描述

《鸿蒙开发基础》

●ArkTS语言
●安装DevEco Studio
●运用你的第一个ArkTS应用
●ArkUI声明式UI开发
.……
在这里插入图片描述

《鸿蒙开发进阶》

●Stage模型入门
●网络管理
●数据管理
●电话服务
●分布式应用开发
●通知与窗口管理
●多媒体技术
●安全技能
●任务管理
●WebGL
●国际化开发
●应用测试
●DFX面向未来设计
●鸿蒙系统移植和裁剪定制
……
在这里插入图片描述

《鸿蒙进阶实战》

●ArkTS实践
●UIAbility应用
●网络案例
……
在这里插入图片描述

获取以上完整鸿蒙HarmonyOS学习文档,请点击→纯血版全套鸿蒙HarmonyOS学习文档

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在layui中导出饼状图需要使用到layui的扩展模块lay.extend()和echarts的组件库。下面是实现步骤: 1. 引入echarts的组件库和相关的CSS文件。 ```html <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/echarts/5.1.2/echarts.min.css"> <script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.1.2/echarts.min.js"></script> ``` 2. 创建一个具有指定id的元素,作为饼状图的容器。 ```html <div id="pieChart" style="width: 100%; height: 400px;"></div> ``` 3. 使用layui的扩展模块lay.extend()定义一个自定义模块。 ```javascript layui.extend({ echarts: '{/}path/to/echarts/echarts' }); ``` 4. 在layui.use()方法中引入自定义模块,以及使用echarts的组件库进行饼状图的初始化和绘制。 ```javascript layui.use(['echarts'], function() { var echarts = layui.echarts; // 绘制饼状图 var pieChart = echarts.init(document.getElementById('pieChart')); var option = { // 饼状图的配置项和数据 series: [ { name: '访问来源', type: 'pie', radius: '55%', data: [ {value: 335, name: '直接访问'}, {value: 310, name: '邮件营销'}, {value: 234, name: '联盟广告'}, {value: 135, name: '视频广告'}, {value: 1548, name: '搜索引擎'} ] } ] }; pieChart.setOption(option); // 导出图片 $('#exportBtn').click(function(){ pieChart.export('png', function(dataUrl) { var link = document.createElement('a'); link.href = dataUrl; link.download = '饼状图.png'; link.click(); }); }); }); ``` 以上代码实现了一个基本的导出饼状图功能。在页面中,饼状图的容器使用一个具有指定id的div元素,然后使用layui的lay.extend()方法引入了echarts的组件库,并在layui.use()方法中使用echarts.init()初始化了饼状图的实例,并使用setOption()方法设置了饼状图的配置项和数据。最后,通过点击一个按钮,调用export()方法将饼状图导出为PNG格式的图片并保存到本地。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值