似曾相识的标题,没错,今年年初就发布过一篇《兔年了,一起用Compose来画兔子吧》的文章,那是我刚开始学习Compose发布的第一篇文章,没放链接的原因是兔子太丑就别点进去看了,辣眼睛辣眼睛,那篇文章主要是针对Compose里面的Canvas做针对性练习,了解一下如何绘制各种形状的几何图形,而在前不久华为推出了HarmonyOS NEXT之后,对于我们Android开发者来讲,所要掌握的技术栈里面毫无疑问又多了一个开发语言,那就是ArkTS,所以跟Compose一样,我学习ArkTS的第一步也从学习它的Canvas开始,今天我也用ArkTS来画一只跟年初差不多画风的兔子,眼睛就再辣一次吧,学习基础知识最重要
准备工作
首先来创建我们的自定义组件,命名为Rabbit
@Entry
、@Component
都是装饰器,@Component
表示这个是自定义组件,@Entry
表示该组件为入口组件,在build()
方法中我们就可以写主要的UI代码,这一点与Flutter类似,至于如何选择我们界面上的父组件,由于最后这里只会有一个Canvas
,所以父组件如何选择没有太大要求,我直接将官方api文档里面的样式代码复制过来了
父组件用的是弹性组件Flex
,这里设置的是主轴方向是FlexDirection.Column
,alignItems
表示里面子组件在交叉轴方向上的对齐方式,这里设置的是ItemAlign.Center
居中对齐,justifyContent
表示子组件在主轴方向上的对齐方式,这里设置的是FlexAlign.Center
居中对齐,然后在组件后面是设置的组件本身的一些属性,这里设置的是宽高,都是占满全屏,这种属性的设置方式类似于Compose里面的Modifier
,创建好父组件之后,我们接下来就是创建我们子组件Canvas
这里Canvas
的创建方式不得不让我有点槽点不吐不快
- 槽点一 :这里创建了一个
RenderingContextSettings
对象和一个CanvasRenderingContext2D
,可以理解为就是Canvas
的画布对象,但是对于Canvas
来说最主要的东西为什么不在Canvas
组件内部创建完成以后通过onReady
函数回调出来呢,非得让开发者手动new出来再传到Canvas
里面去,这个声明式ui也不是特别的"声明" - 槽点二 :在槽点一里面说了
Canvas
的创建过程需要new一个CanvasRenderingContext2D
对象后传到Canvas
的构造函数里面去,但是我发现如果不传参数的话,也不会编译报错,甚至可以run起来,那会我还没看Canvas
源码,琢磨着难道Canvas
里面还有一个默认的CanvasRenderingContext2D
对象吗,结果一看模拟器界面,一片空白。。。好吧,我知道了,必须传~ - 槽点三 :在组件内部使用组件的局部变量时候,一定要用
this
去指向这个变量,不然编译器会报错,说找不到它了,反正最后整只兔子画完我是this
麻了
创建完Canvas
之后,由于绘制的过程基本上是计算各种坐标的过程,所以需要知道整个画布的宽和高,我们在增加两个变量来表示画布的宽高
这里screenX
与screenY
分别表示画布的宽和高,我们看到在这俩变量前面也有一个装饰器@State
修饰,被这个装饰器修饰的变量我们叫它状态变量,当它的值发生变化的时候会触发ui刷新,类似于Flutter的setState
,Compose的remember
,所以我们在onReader
函数里面将画布的宽高设置到screenX
与screenY
里面去
开始绘制
头
这里的头部我们就用一个圆圈来表示,在ArkTS里面使用arc
函数来绘制圆圈,这个函数既可以绘制圆圈,也可以画圆弧,提供如下几个参数
前两个参数代表圆心坐标,radius
表示圆弧或者圆的半径大小,最后一个参数couterclockwise
表示绘制圆弧的方向,默认是逆时针,最关键的是startAngle
和endAngle
这俩个参数,我们注意看这俩参数后面的描述写的是弧线的起始弧度于终止弧度,单位是弧度而不是角度,所以你如果想画个半圆,传的是0和180的话,出来的依然还是个圆,正确的做法是利用计算弧度的公式来做
弧度=度数*π/180
知道关键点以后就要开始画这个圆了,先设置一下画笔的粗细与颜色
接着就调用arc
函数来画这个圆,我们圆的中心点就定在了画布的中心点位置,也就是screenX/2
与screenY/2
绘制圆弧的代码就这一行结束了,但是我们运行一遍代码后发现模拟器上什么都没有,原因是我们少加了一行代码,在ArkTS中你需要显式的表明你画的图形是空心的还是实心的,分别用下面两个函数来表明
这看似是一句配置类型的代码,但你在后面会发现每调用一次绘制函数就要写一下stroke()
或者fill()
,不调用的话绘制的效果就不出来,如果你不信我们接下来就做个试验,来画两个圆,一个在调用好arc
函数以后调用了stroke
,另一个没有调用,代码是这个样子的
我们在原本兔头的圆心位置下面又增加了一个略小一点的圆,我们运行下代码看看实际效果如何
可以看见屏幕上确实只有一个圆,只有当我们把下面那个圆的stroke
函数补充好,那个略小一点的圆才出现了
一个好消息,一个坏消息,好消息是俩圆都出来了,坏消息是,咋俩圆之间还有根黑线呢?咱也没画啥多余的线啊,查了下api文档才知道,我们又少加代码了,文档里面说,如果一个路径画完了要开始下一个路径了,需要调用beginPath
函数,不然就是像我们这俩圆一样,两个路径还有一处相连的地方,它认为你第一笔还没有画完,最后当我把beginPath
函数加到代码里面之后,最终效果才正常了
好了,槽点再多,这个也毕竟是语法,我们将小一点的圆去调继续来画兔子
鼻子
现在开始画兔子的鼻子,这里我打算使用三角形来表示鼻子,但是在现有的api中没有现成的函数来画三角,我们只能通过画path来实现,画path同我们以前在Android里面画path基本相同,先调用moveTo
确定起点,然后调用lineTo
来画路径,最终画完以后调用closePath
结束当前路径形成一个封闭路径,代码如下所示
鼻子很简单,我们接下来画兔唇
兔唇
兔唇分左兔唇与右兔唇,其实就是两个圆弧,我们可以使用之前用过的arc
函数来实现,但是我们这里使用另一个函数来画圆弧,那个就是arcTo
,用法如下所示
这个函数是根据圆弧经过的点和半径大小来确定整个圆弧的样式,在调用这个函数之前还必须调用moveTo
来确定圆弧的起点,所以画一个圆弧需要确定好三个点,至于如何找出这三个点,这个同绘制二阶贝塞尔曲线的思路是一样的,moveTo
是第一个固定点,arcTo
函数前两个参数表示曲线控制点的xy坐标,后面两个参数表示曲线终止固定点的xy坐标,这样子应该理解起来会方便一些,所以我们左右兔唇的绘制代码和效果图如下所示
眼珠与眼眶
然后我们在兔子脑袋偏上位置画眼珠,眼珠就是两个实心圆,圆的绘制方法已经知道了,这里是需要注意一下如果希望兔子眼珠的颜色同线条颜色不一样,就需要设置一下画笔实心的颜色,同时绘制结束需要调用fill
函数来表明当前绘制的是实心图形,下面就是绘制左右眼珠的代码
运行一下看看效果
只有眼珠子的话看起来较显呆滞,所以需要在眼珠子上面画上眼眶,眼眶现在画起来就容易多了,那就是个半圆,起点与终点的y坐标与眼珠子一致,然后x坐标左右各加上点偏移量就好了,中间的控制点的x坐标与眼珠子的x坐标一致,y坐标向上偏移一点就好,代码如下
现在再看看效果
现在看起来有神了,我们接着下一步
耳朵
记得在之前用Compose画兔子的时候,耳朵是用两个椭圆来完成的,然后椭圆的底部反复调整才跟脑袋连在一起,方式有点太sa了,这次不打算这么做,首先兔子耳朵不可能直溜溜的竖在脑袋上的,肯定会倾斜一点,其次兔子的耳朵严格来讲并不能用椭圆来表示,毕竟贴近脑袋部分并不相连在一起,如果连在一起了,我们拽兔子耳朵的时候,就很容易拽断了。。。更形象的应该是兔子脑袋上找出两个点,然后各自向上画曲线,最终在一个点连在一起形成一只耳朵,所以一只耳朵其实是由两根三阶贝塞尔曲线构成,那么第一步需要先找出两根曲线的起点,这个起点在脑袋上,那么就需要根据半径与角度计算出一个圆周上某一个点的坐标,下面是左耳的两个起点坐标的计算代码
这里就是利用数学公式sin与cos来计算xy坐标,从传值上就能看出,sin与cos函数接收的也是以弧度为单位的数值,至于这里的取值,sin与cos函数里面的角度是按照顺时针来计算的,起点在圆的最左侧,而我们之前在使用arc
函数画圆弧的时候,里面传的角度默认按照逆时针方向旋转的,起点在圆的最右侧,这里是存在差异的,要注意一下,所以在代码中,leftx
与lefty
是比较靠近脑袋顶部的点,而leftOutX
与leftOutY
是比较靠外侧的点,然后我们以这两个点为参照点,就能将贝塞尔曲线其余几个点找出来,最终左耳朵的绘制代码如下所示
运行一下代码,左耳朵就有了
我们再以同样的方式将右耳的坐标算出来,代码与效果图如下所示
这只兔子可算开始萌起来了~
身体
兔子的身体在之前Compose的文章里面也是用椭圆来表示的,但是同样感觉好像一拽兔子就能把兔子拽断一样,所以这里身体的绘制同耳朵一样,也是用一个三阶贝塞尔曲线从兔子脑袋下边的一头连到另一头,剩下两个点就选择画布靠下一点的位置,代码如下
效果图就是上面这个样子了,算是达到预期要的效果了吧,然后给兔子加上两只爪子
兔爪
兔爪我们用两个椭圆型表示,在ArkTS里面绘制椭圆的api我们使用ellipse
函数来实现,它需要的参数如下所示
前两个参数同绘制圆弧一样,是中心点坐标,第三个参数radiusX
是x轴的半径,radiusY
是y轴半径,rotation
是旋转角度,startAngle
与endAngle
是起始点与终止点坐标,最后一个参数同样也是绘制的方向,默认是fasle
,逆时针,都是比较容易填的参数,其中中心点的x坐标我们参考之前画身体时候第二,三个点的x坐标,再往里偏移一些,而椭圆的y坐标我们就选择screenY * 3/4
的高度,这样绘制两只爪子的代码就有了
我们看到这里还将爪子设置成实心的,实心填充颜色设置成纯黑,由于之前兔子眼珠是红色的,所以这里要注意下眼珠与爪子的绘制顺序,如果眼珠在爪子的下面绘制,那么眼珠也变成黑色的了,除非在绘制眼珠之前重新设置下fillStyle
,再更改下填充色值,现在我们运行下代码,两个爪子就出来了
尾巴
最后一个就是画尾巴,尾巴打算用一个圆来表示,圆的话我们上面已经讲过如何绘制了,主要还是寻找中心点坐标,这边中心点的x坐标就用之前画身体用到的第三个控制点的x坐标,也就是变量endX
,y坐标的高度就确定为四分之三的画布高度,代码如下所示
这里圆的半径经过反复调试最终确定为20,我们现在跑下代码,尾巴就有了
现在我们在优化下,给这个圆加点分割线,让它看起来毛茸茸一点,分割线我们使用setLineDash
函数来完成
这个函数接收一个数组,数组里面第一个值表示实线长度,第二个值表示间隙长度,我们随意设置两个值来看看效果
现在看起来是不是更像一只毛茸茸的尾巴
总结
到这里我们已经完成了使用ArkTS来绘制一只兔子的整个过程,其实兔子本身颜值如何到不是很重要,这篇文章的关键主要还是熟悉一下ArkTS里面的Canvas
组件,以及这个组件里面提供的api,虽说仅仅一个组件就有不少槽点,但每个语言都有每个语言的特性,作为一个技术人,对新出来的技术,尤其这个还是国内自己的技术,应该更多的是去挖掘这个技术的优点,以及它与其他语言都有什么异同,这个才是应该做的事情。