在实操前,我们先来了解下Jetpack Compose图形绘制。
热身
Canvas
Canvas是自定义图形的核心可组合项。在布局中放置 Canvas 的方式与放置其他 Compose 界面元素相同。在 Canvas 中,您可以通过精确控制元素的样式和位置来绘制元素。
Canvas
可组合项使用特殊的 Compose Canvas
对象,与Android View系统的Canvas不同。
Canvas(modifier = Modifier.fillMaxSize()) {
}
Canvas
会自动提供 DrawScope
(一个维护自身状态且限定了作用域的绘图环境)。这让您可以为一组图形元素设置参数。DrawScope
提供了一些有用的字段。
Compose中的Canvas可
DrawScop
每个Canvas都维护了一个DrawScop
,DrawScop辅助图形绘制,提供了包括:center
(中心点坐标)、size
(可绘制区域大小)、drawContext
,以及很多常用图形绘制方法(drawLine
、drawCircle
、drawRect
、drawArc
、drawPath
、drawImage
、drawOval
、drawPoints
、drawRoundRect
、rotate
、translate
等)。
Paint
画笔,使用的是androidx.compose.ui.graphics
包中的类,注意与Android 原生画笔Paint区分。
创建对象时实际实例化的是AndroidPaint类,该类继承Paint接口,并实现了属性的get、set方法,其包含的属性与原生Paint基本一致。
可通过Compose Paint 获取原生Paint:
val paint = Paint()
//获取成原生Paint
val nativePaint = paint.asFrameworkPaint()
查看AndroidPaint的源码你会发现根本使用的还是原生的Paint,如下:
//给原生类定义一个NativePaint的别名
actual typealias NativePaint = android.graphics.Paint
actual fun Paint(): Paint = AndroidPaint()
//实现androidx.compose.ui.graphics.Paint接口,
class AndroidPaint : Paint {
private var internalPaint = makeNativePaint()
private var _blendMode = BlendMode.SrcOver
private var internalShader: Shader? = null
private var internalColorFilter: ColorFilter? = null
override fun asFrameworkPaint(): NativePaint = internalPaint
override var alpha: Float
get() = internalPaint.getNativeAlpha()
set(value) {
internalPaint.setNativeAlpha(value)
}
......
}
好了到这里图像绘制基本结束完成了,下面开始进行实战。
实战
随着冬奥的火热进行,某物也被大众喜爱,出现一墩难求的局面。程序员界也不例外,出现各种语言版本的手绘一个,如:js版、python版等,最近正好学到Jetpack Compose 图形绘制,也来追个墩。
好了不啰嗦了,开始进入正题(可能涉及到侵权问题,这里就不上图了,自己脑补下吧)
通过分析可以划分出各身体部位:身体、耳朵、手、脚、面部、眼睛、鼻子和嘴巴、Logo文字五环、光环。
下面我们就按各部位作为步骤一一绘制。
画布坐标变换
为了方便绘制在画布中心,不用每个参数都与center坐标进行相加/减处理,这里我们将坐标原点设置在画笔中心。如下:
Canvas(modifier = modifier){
//转换成原生canvas绘制,替换DrawScope
drawIntoCanvas { canvas->
val paint = Paint().apply {
color = Color.Black
strokeWidth = 3f
isAntiAlias = true
style = PaintingStyle.Stroke
}
//先保存,方便绘制完成后在恢复
canvas.save()
//将坐标原点转换到屏幕中心
canvas.translate(center.x,center.y)
//原点上绘制一个位置参考圆形
canvas.drawCircle(Offset(0f,0f),5f,paint)
//这里绘制墩
canvas.restore()
}
}
步骤一、绘制身体
使用一个椭圆替代身体,椭圆大小为宽500x高600,为了保证椭圆的中心点在坐标原点,需要设置一个偏移量,偏移量为:x轴 -宽/2, y轴 -高/2。
核心代码:
//步骤一:绘制一个宽x高=500x600的椭圆
canvas.drawOval(Rect(-ovalSize.width/2,-ovalSize.height/2,ovalSize.width/2,ovalSize.height/2),paint)
步骤二、绘制两个耳朵
左耳:绘制实心圆弧,从-215度开始旋转185角度,大小为:Size(90f,110f),偏移量为:Offset(-180f,-310f)
右耳:实心圆弧,从95度开始旋转205角度,大小为:Size(90f,110f),偏移量为:Offset(95f,-310f)
注意要设置不以圆弧的中心点为参考
核心代码如下:
//绘制左耳
canvas.drawArc(
rect = Rect(Offset(-180f,-310f),Size(90f,110f)),
startAngle = -215f,//起始角度
sweepAngle = 185f,//旋转的角度
useCenter = false,//不以圆弧的中心点为参考
paint = paint
)
//绘制右耳
canvas.drawArc(
rect = Rect(Offset(95f,-310f),Size(90f,110f)),
startAngle = -160f,
sweepAngle = 205f,
useCenter = false,
paint = paint
)
步骤三、绘制两只手
先确定两只手与身体连接部位的角度,计算出在椭圆上的点的坐标。如下图:
坐标点可以通过椭圆是任意点坐标公式计算,不知道的可以参考求圆和椭圆上任意角度的点的坐标
开始和结束坐标点确定后,通过drawPath绘制赛贝尔曲线实现效果,这里要注意左右手的方向。
核心代码:
//左,起始点和结束点夹角为15°
val leftPath = Path().apply {
val coordinate1 = getOvalSideCoordinate(ovalSize,170)
//计算椭圆上坐标点
val sx = coordinate1.x
val sy = coordinate1.y
val coordinate2 = getOvalSideCoordinate(ovalSize,155)
val ex = coordinate2.x
val ey = coordinate2.y
moveTo(-sx,-sy)
cubicTo(-sx,-sy,-sx-40,-sy+30,-sx-80,-sy+65)
cubicTo(-sx-82,-sy+66,-sx-80-40,-sy+65+70,-sx-50,-sy+60+85)
cubicTo(-sx-50,-sy+60+85,-ex-20,-ey+60+25,-ex,-ey)
close()
}
canvas.drawPath(path = leftPath,paint)
同理,右手的爱心绘制,根据右手的位置大致可以确定爱心的起始位置:
val hearPath = Path().apply {
moveTo(ex+50,ey-70)
//左半部分爱心
cubicTo(ex+50,ey-70,ex+50,ey-90,ex+35,ey-80)
cubicTo(ex+35,ey-80,ex+20,ey-60,ex+40,ey-40)
//右半部分爱心
cubicTo(ex+40,ey-40,ex+80,ey-60,ex+60,ey-78)
cubicTo(ex+60,ey-78,ex+50,ey-80,ex+45,ey-70)
close()
}
paint.color = Color.Red
canvas.drawPath(path = hearPath,paint)
步骤四、绘制两只脚
首先为了保证两只脚的对称,要以y轴为对称线,并量只脚与身体椭圆交点的夹角要相同,否则两只脚粗细会不相同,参考绘制手。
这里设置的夹角为:23°,如下图(图画的比较丑,将就看吧):
//步骤四:画脚
private fun drawLegs(canvas: Canvas, paint: Paint, oval1Size: Size) {
val angle = 90
val height = 100 //腿高
val offsetAngle1 = 30
val offsetAngle2 = 7
paint.style = PaintingStyle.Fill
//左脚
val leftPath = Path().apply {
//左边起始点
val point1 = getOvalSideCoordinate(oval1Size,angle+offsetAngle1)
//右边结束点
val point2 = getOvalSideCoordinate(oval1Size,angle+offsetAngle2)
//从左边起始点开始
moveTo(-point1.x,-point1.y)
lineTo(-point1.x,-point1.y + height-10)
cubicTo(-point1.x,-point1.y + height-10,-point1.x,-point1.y+height+10,-point1.x+15,-point1.y+height+10)
lineTo(-point2.x,-point1.y+height+10)
cubicTo(-point2.x,-point1.y+height+10,-point2.x+10,-point1.y+height+10,-point2.x,-point1.y + height-5)
lineTo(-point2.x,-point2.y)
close()
}
canvas.drawPath(leftPath,paint)
...
}
步骤五、绘制面部
这个比较简单,绘制5个紧挨着的椭圆就可以了,从最里面开始颜色依次为:蓝->红->紫->黄->绿。
//步骤五:面部,绘制5色仅贴着的椭圆,颜色依次:蓝->红->紫->黄->绿
private fun drawFiveOvals(canvas: Canvas, paint: Paint){
paint.strokeWidth = 7f
//最里面椭圆宽度和高度
var width = 360f
var height = 300f
//从最里面圆环向外依次width和height需要增加的值
val offValue = paint.strokeWidth * 2
val colors = arrayOf(Color(0xff87CEEB),Color(0xff8B0000),Color(0xff6A5ACD),Color(0xffFFD700),Color(0xff32CD32))
//不包含5
for (i in 0 until 5){
paint.color = colors[i]
canvas.drawOval(Rect(Offset(-width/2,-(height-70 - paint.strokeWidth * i)), Size(width,height)),paint)
width += offValue
height += offValue
}
//恢复
paint.strokeWidth = 5f
paint.color = Color.Black
}
步骤六、绘制眼睛
眼睛拆分为:实心黑色椭圆 + 白色圆环 + 白色实心小圆
依次绘制出后,对画布进行旋转和平移达到需要的效果,不知道话可以通过修改参数值逐步尝试。
save()
rotate(40f)
translate(-80f,100f)
paint.style = PaintingStyle.Fill
drawOval(Rect(Offset(-120f,-200f), Size(width,height)),paint)
paint.style = PaintingStyle.Stroke
paint.color = Color.White
drawCircle(Offset(-55f,-140f),35f,paint)
paint.style = PaintingStyle.Fill
drawCircle(Offset(-50f,-155f),10f,paint)
restore()
步骤七、绘制鼻子和嘴巴
鼻子:类似绘制爱心,注意要以y轴对称
嘴巴:先画嘴巴最顶上的一个弧形(无填充),接着绘制带填充的弧度为290f,最后绘制一个红色填充的椭圆。
//步骤七:鼻子和嘴巴
private fun drawNoseAndMouth(canvas: Canvas, paint: Paint){
canvas.apply {
val noseWidth = 30f
val noseHeight = 30f
val path = Path().apply {
moveTo(-noseWidth/2,-100f)
cubicTo(-noseWidth/2,-100f,-noseWidth,-(100-noseHeight/2),0f,-(100 - noseHeight))
cubicTo(0f,-(100 - noseHeight),noseWidth,-(100-noseHeight/2),noseWidth/2,-100f)
close()
}
paint.style = PaintingStyle.Fill
drawPath(path,paint)
paint.style = PaintingStyle.Fill
drawArc(-(noseWidth+100)/2,-70f,(noseWidth+100)/2,40f,-55f,290f,false,paint)
paint.color = Color.White
drawArc(-(noseWidth+40)/2,-80f,(noseWidth+40)/2,-50f,0f,180f,false,paint)
paint.color = Color(0xffA52A2A)
drawOval(-(noseWidth+60)/2,-20f,(noseWidth+60)/2,36f,paint)
paint.color = Color.Black
}
}
步骤八、绘制Logo文字和五环
先用drawImage绘制logo图片,这里注意:获取图片的时候要用到resource
,该值使用LocalContext.current.resource
获取,要放在Canvas
的外部。
//绘制logo
drawImage(imageBitmap, Offset(-imageBitmap.width/2f,imageBitmap.height/2f + 90),paint)
绘制文字:BEIJING 2022,在Compose封装的Canvas中没有直接提供绘制文字的方式,但提供了nativeCanvas(原生canvas),通过nativeCanvas可以去绘制文字,paint也需要使用原生Paint类。
canvas.apply {
...
//绘制文字
val text="BEIJING 2022"
//创建原生Paint对象
val nativePaint = android.graphics.Paint().apply {
color = android.graphics.Color.BLACK
strokeWidth = 2f
textSize = 22f
textSkewX = -0.5f
typeface = Typeface.create(Typeface.SANS_SERIF,Typeface.BOLD)
}
//测量文字宽度
val width = nativePaint.measureText(text)
//注意:这里绘制文字要使用nativeCanvas,即使用原生的canvas绘制,Compose提供的Canvas不支持绘制文字
nativeCanvas.drawText(text,-width/2,220f,nativePaint)
...
}
绘制奥运五环,五环特点:上面三个圆环,下面两个,环环相扣。
这里先绘制上面三个圆环,两环之间距离为5,紧接着沿Y轴向下偏移圆环半径长度,继续绘制两个圆环。
//绘制五个圆环叠在一起,上面3个,下面2个,
//下面两个相对上面三个的y轴偏移量:圆环的半径
//上面三个圆环间距为5
val colors = arrayOf(Color(0xff87CEEB),Color(0xff000000),Color(0xff8B0000),Color(0xffFFD700),Color(0xff32CD32))
val radius = 12f //圆环半径
val offsetY = 240f //上面圆环距离绘图中心点y轴上的偏移量
val space = 5f //圆环间距
paint.style = PaintingStyle.Stroke
paint.strokeWidth = 3f //圆环线条粗细
for (i in 0 until 5){
paint.color = colors[i]
if (i < 3){//上层圆环
drawCircle(Offset((radius * 2 + space) * (i-1),offsetY),radius, paint)
}else{//下层圆环
val sel = if(i <= 3) -1 else 1
drawCircle(Offset(sel * (radius + space/2),offsetY + radius),radius, paint)
}
}
由于需要环环相扣,所以在绘制几个小圆弧达到环环相扣的效果。
//画出交叉感觉
paint.color = colors[0]
//画出蓝环扣在黄环中
drawArc(Rect(Offset(-radius * 2 - space,offsetY),radius = radius),-10f,30f,false,paint)
paint.color = colors[1]
//黑环 一部分扣在黄环上 一部分扣在绿环上
drawArc(Rect(Offset(0f,offsetY),radius = radius),90f,30f,false,paint)
drawArc(Rect(Offset(0f,offsetY),radius = radius),-10f,30f,false,paint)
paint.color = colors[2]
//绘制红环扣在绿环中
drawArc(Rect(Offset(radius * 2 + space,offsetY),radius = radius),90f,30f,false,paint)
步骤九、绘制光环效果
最后这个光环效果,同样采用drawPath,绘制贝塞尔曲线的方式实现,多修改下数值可以达到想要的效果。
设置了很多参考点,代码比较多不贴了。
有想看效果图的,自己从后面代码下载吧。
后续后来想了下针对多个贝塞尔曲线坐标点的可以把各个点放到一个list中,然后对这个list进行以step为2进行遍历,再设置贝塞尔曲线,就没那么多代码了,这里就不折腾了。
欢迎留言,一起学习,共同进步!