前言
最近在边学边进行现有项目的鸿蒙化,需要实现一个环形进度条,摸索了一阵最后用canvas实现了出来,刚好最近感觉博客没东西可写,借这个机会又有东西可以分享给大家了。
我会先把我的摸索过程写出来,如果想直接看实现可以跳过第二部分。
预期效果和准备工作
先来一张效果图
预期效果类似一个表盘,中间区域是一个渐变色的圆,外层是带刻度的弧形进度条,要实现这个效果并不难,先拆分一下
- 圆环的浅蓝色背景
- 在圆环背景上的颜色深一点的刻度线
- 颜色深一点的进度条弧形
- 中间区域渐变圆形
- 进度条起点上的深蓝色圆点
在安卓中想要实现这个效果其实是很简单的,所以只需要按照既定思路去找鸿蒙的对应API就可以
鸿蒙的绘制API
鸿蒙的绘制API分为Canvas和绘制组件,canvas和html的canvas大同小异,而绘制组件我理解为是对canvas的封装,看起来像下面这样
看起来使用绘制组件可以非常简单地实现我们需要的效果,我一开始就是这么尝试的,但是遇到了两个问题让我放弃使用它。
首先没有提供弧形的绘制组件,虽然path可以做到类似的效果,但是未免太麻烦了,需要手写svg指令来绘制
第二个就是动画的问题,本来通过svg指令来绘制已经够麻烦了,要加动画就需要在指令语句里面再加上插值计算,完全不能接受。
最后还是选择用canvas,个人认为绘制组件只能满足一些简单的场景,特别的path这个东西非常的不好用
鸿蒙中的动画API
动画相关API同样分为两部分,一个是和组件强相关的组件动画,另一个是更加独立和抽象的animator
组件动画主要用于对组件的固有属性比如宽高、位置这些进行插值变换构成动画,而animator就跟安卓的animator大同小异了
很明显组件动画没办法适用于canvas,所以最后我们要使用canvas+animator来实现
如何实现
代码如下
typescript
import animator, { AnimatorOptions, AnimatorResult } from '@ohos.animator';
@Component
@Preview
export struct ProgressPanel {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
private arcWidth: number = 10
private anim: AnimatorResult | null = null
aboutToAppear() {
const options: AnimatorOptions = {
duration: 500,
// 插值器
easing: "ease-out",
delay: 300,
// 动画执行完后保持最后一帧的状态
fill: "forwards",
direction: "normal",
iterations: 1,
// 动画插值
begin: 0,
end: 0.5
};
this.anim = animator.create(options);
this.anim.onframe = (value) => {
this.updateProgress(value)
};
}
aboutToDisappear() {
this.anim?.cancel()
this.anim = null
}
private updateProgress(progress: number) {
this.context.clearRect(0, 0, this.context.width, this.context.height)
const centerX = this.context.width / 2;
const centerY = this.context.width / 2;
const radius = Math.min(centerX, centerY) - this.arcWidth;
this.context.save()
this.context.save()
this.context.save()
// 进度条背景
this.context.beginPath()
this.context.arc(centerX, centerY, radius, 0, Math.PI * 2)
this.context.lineWidth = this.arcWidth
this.context.strokeStyle = '#803db7fe';
this.context.stroke()
// 进度条刻度
this.context.beginPath()
this.context.arc(centerX, centerY, radius, 0, Math.PI * 2)
this.context.setLineDash([1, 5])
this.context.lineWidth = this.arcWidth
this.context.strokeStyle = '#4d3db7fe';
this.context.stroke()
// 进度条
const startAngle = -Math.PI / 2;
const endAngle = startAngle + Math.PI * 2 * progress;
this.context.restore()
this.context.beginPath()
this.context.arc(centerX, centerY, radius, startAngle, endAngle);
this.context.strokeStyle = '#3DB7FE';
this.context.lineCap = 'round'
this.context.lineWidth = this.arcWidth;
this.context.stroke();
// 中间圆形
const gradient = this.context.createLinearGradient(radius, 0, radius, radius * 2)
gradient.addColorStop(0.2, '#3DB7FE')
gradient.addColorStop(1.0, '#399BFE')
this.context.restore()
this.context.beginPath()
this.context.arc(centerX, centerY, radius - this.arcWidth, 0, Math.PI * 2);
this.context.fillStyle = gradient
this.context.fill();
// 起点圆点
this.context.restore()
this.context.beginPath()
this.context.arc(centerX, this.arcWidth, this.arcWidth / 2, 0, Math.PI * 2);
this.context.fillStyle = '#399BFE'
this.context.fill();
}
build() {
Column() {
Stack() {
Canvas(this.context)
.width(200)
.height(200)
.onReady(() => {
this.anim?.play()
})
}
.width('100%')
.alignContent(Alignment.Center)
}
.padding({ left: 20, right: 20 })
.margin({ top: 10, bottom: 10 })
.width('100%')
}
}
- 开头调用了三次
context.save()
是想偷懒,不想每绘制一个不同的部分都重新设置画笔的一些属性 - 进度条刻度在实现起来很方便,
setLineDash
,参数分别为填充宽度和空白间隔宽度 - 绘制弧形一样是使用的弧度制,圆形等于2PI
- 9点钟方向弧度为0,我们需要起点在12点方向,所以起点弧度为
-Math.PI / 2
- 中间圆形添加了一个径向渐变,通过
addColorStop
添加渐变的插值点 - 动画的end值为方便演示是写死的,需要的时候改成真实进度就可以了
本来给进度条也加上渐变,但是发现径向、放射、扫射三种渐变都没办应用在弧形上面,希望以后能支持
总结
这个组件实现起来还是比较简单的,主要的时间都花在了看文档,找api,试验上面;不得不吐槽的是绘制组件目前看来确实鸡肋,而canvas也有不足的地方,希望后面能更加完善吧。