作者: 余香鑫
前言
最近项目有用到Canvas组件,想扩展熟悉下eTS Canvas组件,便有了这个项目。先看下实现效果,左边是参考样例, 右边是最终实现效果
(原生字体看起来不太协调, 但没有找到换字体的方法)
原型 |
实现效果 |
 |
 |
准备工作
开始之前, 我们需要对Canvas有一些基础概念
- Canvas是画布组件, 默认坐标原点在左上顶点. 构造函数接收一个
CanvasRenderingContext2D
对象, 可以理解为画笔Paint, 它提供了绘制矩形、文字、图片等API, 还支持对Canvas缩放、
旋转、平移等能力, 基于这些能力, 我们可以实现常规组件难以实现的效果

-
beginPath和closePath接口, 一个Canvas只能设置一个CanvasRenderingContext2D对象, 在绘制不同区域不同样式时为了不了会互相干扰,
绘制之前调用CanvasRenderingContext2D#beginPath()方法重置路径, 绘制结束调用 CanvasRenderingContext2D#closePath()方法结束绘制区间, 以下为示例

-
需求拆解: 参照原型图示, 按动静模型可以拆分为背景表盘和旋转的分秒指针. 背景表盘由由外框和时间刻度组成, 主要使用绘制弧形和文字接口, 时分秒指针需要根据时间计算出旋转角度, 对画布旋转对应角度后再绘制上即可
-
封装RectF: Canvas绘制时经常需要用到坐标点和大小, 我们可以定义一个class RectF, 维护表盘绘制区域的上下左右4个点的位置, 以便计算绘制区域大小和中心点
开始绘制
1. 绘制Canvas组件
在页面build函数下添加Canvas组件, 初始化CanvasRenderingContext2D对象, 确定绘制区域
2. 绘制表盘外圈和内圈
表盘外圈由一个深灰色#4C4C4E
的外层和和黑色#252529
内层组合, 使用绘制弧形再填充即可实现. 但还需要镂空中部使用canvas.globalCompositeOperation属性可以实现

3. 绘制时间刻度线和小时数
绘制时间刻度使用lineTo
方法可以实现, 关键点在如何确定起点和钟点位置, 这里需要用到直角三角形边长和角度关系公式, 使用Math.con和sin函数可以计算出任意角度的坐标. 绘制文字也同理, 实现效果如下


4. 绘制时分秒指针
指针可以使用Canvas提供的连线能力完成, 主要工作在计算各个点的位置, 难点不大

/**
* 绘制小时指针
*/
drawHourPointer(canvas: Canvas2, innerRect: RectF) {
canvas.canvas.lineJoin = 'round'
let topPoint = new Point(innerRect.centerX() - 40 /**预览用**/, innerRect.centerY() - innerRect.radius() * 0.13)
let bottomPoint = new Point(topPoint.x, topPoint.y + innerRect.radius() * 0.78)
// 底部灰色区域
canvas.save()
.beginPath()
.fillStyle('#A5A7A7')
.moveToPoint(topPoint)
.lineTo(topPoint.x + 6, innerRect.centerY())
.lineTo(topPoint.x + 6, innerRect.centerY() + innerRect.radius() * 0.1)
.lineToPoint(bottomPoint)
.lineTo(topPoint.x - 6, innerRect.centerY() + innerRect.radius() * 0.1)
.lineTo(topPoint.x - 6, innerRect.centerY())
.lineToPoint(topPoint)
.fill()
.closePath()
// 黑色指针区域
let innerTopPoint = new Point(topPoint.x, topPoint.y + innerRect.radius() * 0.19)
canvas.fillStyle(Color.Black.toString())
.beginPath()
.moveTo(innerTopPoint.x, innerTopPoint.y) // 顶部凹点
.lineTo(innerTopPoint.x + 4.5, innerTopPoint.y - 4) // 右上
.lineTo(innerTopPoint.x + 4.5, innerTopPoint.y + 4) // 右中
.lineToPoint(bottomPoint)
.lineTo(innerTopPoint.x - 4.5, innerTopPoint.y + 4) // 右中
.lineTo(innerTopPoint.x - 4.5, innerTopPoint.y - 4) // 右上
.fill()
.closePath()
.restore()
}
/**
* 绘制秒钟指针
*/
drawSecondPointer(canvas: Canvas2, innerRect: RectF) {
canvas.canvas.lineWidth = 1
// 秒钟顶部三角形
let topPoint = new Point(innerRect.centerX() + 40 /**预览用**/, innerRect.centerY() - innerRect.radius() * 0.30)
let topRightPoint = new Point(topPoint.x + 3, topPoint.y + 3)
let topLeftPoint = new Point(topPoint.x - 3, topPoint.y + 3)
let lineRightPoint = new Point(topRightPoint.x - 1.5, topRightPoint.y)
let lineLeftPoint = new Point(topLeftPoint.x + 1.5, topLeftPoint.y)
let bottomPoint = new Point(topPoint.x, innerRect.centerY() + innerRect.radius() * 0.75)
canvas.save()
.beginPath()
.fillStyle(Color.Black.toString())
.moveToPoint(topPoint)
.lineToPoint(topRightPoint)
.lineToPoint(lineRightPoint)
.lineToPoint(bottomPoint)
.lineToPoint(lineLeftPoint)
.lineToPoint(topLeftPoint)
.lineToPoint(topPoint)
.fill()
.closePath()
.restore()
}
/**
* 绘制分钟指针
*/
drawMinutePointer(canvas: Canvas2, innerRect: RectF) {
canvas.canvas.lineJoin = 'round'
let topPoint = new Point(innerRect.centerX(), innerRect.centerY() - innerRect.radius() * 0.15)
let bottomPoint = new Point(topPoint.x, topPoint.y + innerRect.radius() * 0.90)
canvas.save()
.beginPath()
.fillStyle('#A5A7A7')
.fillStyle('#ff0bdbaa')
.beginPath()
.moveToPoint(topPoint)
.lineTo(topPoint.x + 5, innerRect.centerY())
.lineTo(topPoint.x + 5, innerRect.centerY() + innerRect.radius() * 0.1)
.lineToPoint(bottomPoint)
.lineTo(topPoint.x - 5, innerRect.centerY() + innerRect.radius() * 0.1)
.lineTo(topPoint.x - 5, innerRect.centerY())
.lineToPoint(topPoint)
.fill()
.closePath()
let innerTopPoint = new Point(topPoint.x, topPoint.y + innerRect.radius() * 0.22)
canvas.fillStyle(Color.Black.toString())
.beginPath()
.moveTo(innerTopPoint.x, innerTopPoint.y) // 顶部凹点
.lineTo(innerTopPoint.x + 4, innerTopPoint.y - 4) // 右上
.lineTo(innerTopPoint.x + 4, innerTopPoint.y + 4) // 右中
.lineToPoint(bottomPoint)
.lineTo(innerTopPoint.x - 4, innerTopPoint.y + 4) // 右中
.lineTo(innerTopPoint.x - 4, innerTopPoint.y - 4) // 右上
.fill()
.closePath()
.restore()
}
/**
* 绘制指针中心点
*/
drawClockCenter(canvas: CanvasRenderingContext2D, innerRect: RectF) {
let centerRadius = 4
canvas.save()
canvas.beginPath()
canvas.shadowBlur = 2
canvas.shadowColor = 'rgba(12, 12, 12, 1.00)'
canvas.moveTo(innerRect.centerX(), innerRect.centerY())
canvas.arc(innerRect.centerX(), innerRect.centerY(), centerRadius, toCanvasAngle(0), toCanvasAngle(360))
canvas.fill()
canvas.closePath()
canvas.restore()
for (let i = 0; i < 360; i += 90) {
let startAngle = i + 90 / 2
canvas.beginPath()
canvas.fillStyle = i / 90 % 2 == 0 ? '#A9A8AD' : '#F3F3F7'
canvas.moveTo(innerRect.centerX(), innerRect.centerY())
canvas.arc(innerRect.centerX(), innerRect.centerY(), centerRadius, toCanvasAngle(startAngle), toCanvasAngle(startAngle + 90))
canvas.fill()
canvas.closePath()
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
5. 时分秒指针联动
到现在我们绘制的主要工作均已完成, 还剩下最后一个工作, 启动一个定时器, 定时计算出时分秒各指针的旋转角度, 在绘制的指针前对画布做旋转操作即可

结束语
至此一块石英钟表组件已经完成,总体来说技术难点不大,主要使用Canvas绘制弧形和Path方法。迫于篇幅原因, 本篇只贴出了关键代码, 完整代码请前往Gitee查看: OpenHarmony - 基于ArkUI(eTS开发石英钟表自定义组件
入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。
Canvas是做动画必不可少的组件,感谢楼主通过案例对Canvas组件的讲解。
非常nice,由这个项目可以衍生很多其他的时钟主题
可以,马上去git下载下来看看
特别不错的主题,以后可以用自己写的钟表程序了
一路读作者的文章可以提高自己的编程和审美水平
很棒,学习了。
从设计到讲解看的出来很用心
对大家有帮助就好
心动不如行动,马上码起来
码起来
确实很用心哦