前言
最近项目中有柱状图的功能,看了下JS中的组件chart,发现并不适用要求,研究之后决定用canvas动手画一个。
项目说明
本项目基于ArkUI中JS扩展的类Web开发范式,关于语法和概念直接看官网官方文档地址:基于JS扩展的类Web开发范式1 基于JS扩展的类Web开发范式2。
- 工具版本:DevEco Studio 3.0 Beta2。
- SDK版本:3.0.0.1(API Version 7 Beta2)。
提供画布组件,用于自定义绘制图形:画布组件canvas。
效果演示
默认选中第一条,点击柱状图,切换选中效果。
但是发现绘制复杂一点的内容,调用清屏接口clearRect()后,重新绘制内容会闪烁,在官网论坛上面问了其他人,也都有遇到此问题,希望官方早点修复这个bug。
使用到的API
实现步骤
组件设置了上下左右间距,所以内容区域宽度 = 组件宽度 - 左间距 - 右间距,内容区域的高度 = 组件高度 - 上间距 - 下间距。
1、画横线(x轴)、右边的文字
// 获取画布组件
const element = this.$refs.canvas;
// 获取绘图上下文
const ctx = element.getContext('2d', {antialias: true});
// 获取组件大小和位置信息
const rect = element.getBoundingClientRect()
// 测量y轴最大数的文本长度
const yTextMaxWidth = ctx.measureText('' + this.yAxisMaxValue).width
// x轴起始坐标
const xAxisStart = this.paddingLeft
// x轴结束坐标 = 组件宽度 - 右间距 - y轴文字最大宽度
const xAxisEnd = rect.width - this.paddingRight - yTextMaxWidth
// y轴初始坐标
const yAxisStart = this.paddingTop
// y轴结束坐标 = 组件高度 - 底间距 - 底部文字高度 - 文字顶间距
const yAxisEnd = rect.height - this.paddingBottom - 10 - this.xAxisTextTopPadding
// y轴初始值-画文字
let yValue = this.yAxisMaxValue
// y轴平均值-画文字
let yAverageValue = (this.yAxisMaxValue - this.yAxisMinValue) / this.yAxisDivide
// y轴初始坐标
let yAxis = yAxisStart
// 画横线、画右边文字
while (yAxis <= yAxisEnd) {
// 画线
// 对当前的绘图上下文进行保存
ctx.save()
// 线颜色
ctx.strokeStyle = this.xAxisColor
// 创建一个新的绘制路径
ctx.beginPath()
// 线条的宽度
ctx.lineWidth = this.xAxisWidth
// 路径从当前点移动到指定点
ctx.moveTo(xAxisStart, yAxis)
// 从当前点到指定点进行路径连接
ctx.lineTo(xAxisEnd, yAxis)
// 进行边框绘制操作
ctx.stroke();
// 对保存的绘图上下文进行恢复
ctx.restore()
// 画文本
ctx.save()
// 指定绘制的填充色
ctx.fillStyle = this.yAxisTextColor
// 设置文本绘制中的字体样式
ctx.font = this.yAxisTextFont
// 设置文本绘制中的文本对齐方式:文本右对齐
ctx.textAlign = 'right'
// 绘制填充类文本
ctx.fillText('' + yValue, xAxisEnd + yTextMaxWidth, yAxis + 2.5)
ctx.restore()
// 右边文本
yValue -= yAverageValue
// 更新y轴坐标:每次加上y轴等分
yAxis += (yAxisEnd - yAxisStart) / this.yAxisDivide
}
2、画圆柱图、和底部文字
// 画x轴的实际宽度,两边柱状图不靠边
const xDrawWidth = xAxisEnd - xAxisStart - this.xAxisPadding * 2
// 起始点
let xAxis = this.paddingLeft + this.xAxisPadding
// x轴平均值
let xAverageWidth = (xDrawWidth) / (this.chartData.length - 1)
// 画x轴上的文字和数据
for (let i = 0; i < this.chartData.length; i++) {
const item = this.chartData[i]
// 画文本
ctx.save()
ctx.fillStyle = this.xAxisTextColor
ctx.font = this.xAxisTextFont
ctx.textAlign = 'center'
ctx.fillText(item.xData, xAxis, rect.height - this.paddingBottom)
ctx.restore()
// 画柱图
ctx.save()
// 创建一个新的绘制路径
ctx.beginPath()
// 线条的宽度,设置到最小,因为我们需要填充圆柱
ctx.lineWidth = 0.1
// 路径从当前点移动到指定点
ctx.moveTo(xAxis - this.columnarWidth / 2, yAxisEnd - this.xAxisWidth)
// 根据数据比例得出 画的高度
const yDrawHeight = (yAxisEnd - yAxisStart) * item.yData / this.yAxisMaxValue
// 从y轴结束开始画,值结束的坐标:yAxisValueEnd 为了显示圆角:+ 柱图宽度/2(圆的半径)
const yAxisValueEnd = yAxisEnd - yDrawHeight + this.xAxisWidth + this.columnarWidth / 2
// 从当前点到指定点进行路径连接
ctx.lineTo(xAxis - this.columnarWidth / 2, yAxisValueEnd)
// 画圆角
ctx.arc(xAxis, yAxisValueEnd, this.columnarWidth / 2, 3.14, 0)
// 从当前点到指定点进行路径连接
ctx.lineTo(xAxis + this.columnarWidth / 2, yAxisEnd - this.xAxisWidth)
// 进行边框绘制操作
ctx.stroke()
// 填充颜色
ctx.fillStyle = this.columnarColor
// 填充
ctx.fill()
ctx.restore()
// 更新x轴坐标:每次加上x轴等分
xAxis += xAverageWidth
}
3、点击选中效果:画柱图时,记录x轴的位置
// 存入索引和对应的坐标
this.columnarXArray = []
// 画x轴上的文字和数据
for (let i = 0; i < this.chartData.length; i++) {
const item = this.chartData[i]
const columnarX = {
index: i, columnarX: xAxis
}
// 记录x轴,索引和对应的坐标
this.columnarXArray.push(columnarX)
// 画文本
// 文本颜色
ctx.fillStyle = this.selectIndex === i ? this.selectXAxisTextColor : this.xAxisTextColor
// 画选中的线
if(this.selectIndex === i){
// 画线
ctx.save()
ctx.strokeStyle = this.yAxisColor
ctx.lineCap = 'butt'
ctx.lineWidth = this.yAxisWidth
ctx.beginPath()
ctx.moveTo(xAxis, yAxisEnd - this.xAxisWidth)
ctx.lineTo(xAxis, this.paddingTop)
ctx.stroke();
ctx.restore()
}
// 画柱图
// 填充颜色
ctx.fillStyle = this.selectIndex === i ? this.selectColumnarColor : this.columnarColor
}
根据触摸事件的坐标,判断是否在范围内,更新选中的索引。
// 触摸按下
onTouchStart(event) {
console.log(event.touches[0].localX)
// 点击x坐标,相对于组件
const clickX = event.touches[0].localX
let lastSelectIndex = this.selectIndex
// 筛选出点击的柱图索引
for (let i = 0; i < this.columnarXArray.length; i++) {
let item = this.columnarXArray[i]
if (Math.abs(item.columnarX - clickX) <= this.columnarWidth + 5) {
this.selectIndex = item.index
break
}
}
// 重新绘制
if (this.selectIndex !== lastSelectIndex) {
this.draw()
}
}
项目地址:
完整代码:https://gitee.com/liangdidi/HistogramDemo。
总结
此项目并没有特别复杂的地方,注释也很详细,主要是xy轴的起始、结束位置的计算,屏幕的原点坐标(0,0)是在左上角,最后根据系统提供的api画出想要的效果。有些效果看起来很复杂,但是一步一步的拆解,懂得其原理之后,多练多用,也能做出炫酷的效果。