Estou participando do Concurso de Contribuição Criativa "Coelho, um Coelho". Para obter detalhes, consulte: Concurso de Contribuição Criativa "Coelho, um Coelho" . Este ano chegou ao Ano do Coelho. Lembro-me de ter visto muitos desenvolvedores desenharem com o Compose ano passado. Tigre, existem todos os tipos de tigres. Agora que é o ano do coelho, de repente quero usar o Compose para desenhar um coelho e experimentá-lo. A propósito, não toco no Compose há muito tempo .
Preparação
O coelho é desenhado principalmente na tela, então primeiro temos que gerar uma tela e depois determinar a largura, altura e cor do pincel da tela
val drawColor = colorResource(id = R.color.color_EC4126)
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
}
复制代码
Largura e altura são apenas dois valores codificados. Também podemos usar a API do sistema para obter a largura e a altura reais da tela. A cor do pincel é avermelhada.
cabeça
A cabeça é na verdade uma elipse. Usamos o método drawPath da tela para desenhar. O que precisamos fazer é determinar as coordenadas do ponto central da elipse e as coordenadas superior esquerda e inferior direita da elipse.
val startX = screenWidth() / 4
val startY = screenHeight() / 3
val headPath = Path()
headPath.moveTo(screenWidth() / 2, screenHeight() / 2)
headPath.addOval(Rect(startX, startY, screenWidth() - startX, screenHeight() - startY))
headPath.close()
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawPath(path = headPath, color = drawColor, style = Stroke(width = 12f))
}
复制代码
As coordenadas dos eixos x e y do ponto central da cabeça são metade da largura e altura da tela, a coordenada x no canto superior esquerdo é um quarto da largura da tela, a coordenada y é um terço da altura da tela e a coordenada x no canto inferior direito é a largura da tela Subtraia a coordenada x do canto superior esquerdo e a coordenada y do canto inferior direito é subtrair a coordenada y do canto superior esquerdo e, finalmente, desenhe o caminho disso elipse no Canvas, vamos ver a renderização
orelha
Depois de desenhar a cabeça, vamos desenhar as orelhas. As duas orelhas são na verdade duas elipses, que são simétricas em relação à linha central. A ideia do desenho é a mesma do desenho da cabeça. Determine as coordenadas dos pontos centrais dos dois caminhos , e as coordenadas xy do canto superior esquerdo e inferior direito de cada
val leftEarPath = Path()
val leftEarPathX = screenWidth() * 3 / 8
val leftEarPathY = screenHeight() / 6
leftEarPath.moveTo(leftEarPathX, leftEarPathY)
leftEarPath.addOval(
Rect(
leftEarPathX - 60f,
leftEarPathY / 2,
leftEarPathX + 60f,
startY + 30f
)
)
leftEarPath.close()
val rightEarPath = Path()
val rightEarPathX = screenWidth() * 5 / 8
val rightEarPathY = screenHeight() / 6
rightEarPath.moveTo(rightEarPathX, rightEarPathY)
rightEarPath.addOval(
Rect(
rightEarPathX - 60f,
rightEarPathY / 2,
rightEarPathX + 60f,
startY + 30f
)
)
rightEarPath.close()
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawPath(path = leftEarPath, color = drawColor, style = Stroke(width = 10f))
drawPath(path = rightEarPath, color = drawColor, style = Stroke(width = 10f))
}
复制代码
Veja as renderizações
ouvido interno
Dessa forma, as orelhas não são muito tridimensionais e parecem um pouco planas. Afinal, as orelhas de coelho parecerão um pouco afundadas, então adicionamos uma orelha interna a esse par de orelhas para aumentar o efeito tridimensional. O ouvido interno é realmente muito simples, e o motivo é o mesmo do ouvido externo. As coordenadas xy do ponto central e do ponto superior esquerdo e do ponto inferior direito serão menores. Podemos alterar o caminho do ouvido externo a um pouquinho.
val leftEarSubPath = Path()
val leftEarSubPathX = screenWidth() * 3 / 8
val leftEarSubPathY = screenHeight() / 4
leftEarSubPath.moveTo(leftEarSubPathX, leftEarSubPathY)
leftEarSubPath.addOval(
Rect(
leftEarSubPathX - 30f,
screenHeight() / 6,
leftEarSubPathX + 30f,
startY + 30f
)
)
leftEarSubPath.close()
val rightEarSubPath = Path()
val rightEarSubPathX = screenWidth() * 5 / 8
val rightEarSubPathY = screenHeight() / 4
rightEarSubPath.moveTo(rightEarSubPathX, rightEarSubPathY)
rightEarSubPath.addOval(
Rect(
rightEarSubPathX - 30f,
screenHeight() / 6,
rightEarSubPathX + 30f,
startY + 30f
)
)
rightEarSubPath.close()
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawPath(path = leftEarSubPath, color = drawColor, style = Stroke(width = 6f))
drawPath(path = rightEarSubPath, color = drawColor, style = Stroke(width = 6f))
}
复制代码
Veja as renderizações
Há um sabor interno e a espessura do pincel da orelha interna é ligeiramente reduzida. Para destacar o quase grande muito pequeno hahaha, vamos para a próxima etapa
Olho
Depois de desenhar as orelhas, começamos a desenhar os olhos. Os olhos também são muito fáceis de desenhar. O principal é encontrar a posição do ponto central. A coordenada x do ponto central é na verdade a mesma que a coordenada x de a orelha, e a coordenada y está ligeiramente mais próxima da coordenada y do ponto central da cabeça. posição anterior
val leftEyePath = Path()
val leftEyePathX = screenWidth() * 3 / 8
val leftEyePathY = screenHeight() * 11 / 24
leftEyePath.moveTo(leftEyePathX, leftEyePathY)
leftEyePath.addOval(
Rect(
leftEyePathX - 35f,
leftEyePathY - 35f,
leftEyePathX + 35f,
leftEyePathY + 35f
)
)
leftEyePath.close()
val rightEyePath = Path()
val rightEyePathX = screenWidth() * 5 / 8
val rightEyePathY = screenHeight() * 11 / 24
rightEyePath.moveTo(rightEyePathX, rightEyePathY)
rightEyePath.addOval(
Rect(
rightEyePathX - 35f,
rightEyePathY - 35f,
rightEyePathX + 35f,
rightEyePathY + 35f
)
)
rightEyePath.close()
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawPath(path = leftEyePath, color = drawColor, style = Stroke(width = 10f))
drawPath(path = rightEyePath, color = drawColor, style = Stroke(width = 10f))
}
复制代码
As renderizações são as seguintes
眼神有点空洞,无神是不,缺个眼珠子,那我们再给小兔子画上眼珠吧,眼珠就在眼睛的中心点位置,画一个圆点,圆点就要用到drawCircle,它有这些属性
fun drawCircle(
color: Color,
radius: Float = size.minDimension / 2.0f,
center: Offset = this.center,
/*@FloatRange(from = 0.0, to = 1.0)*/
alpha: Float = 1.0f,
style: DrawStyle = Fill,
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
)
复制代码
我们不需要用到全部,只需要用到颜色color,也就是红色,圆点半径radius,肯定要比眼睛的半径要小一点,我们就设置为10f,圆点中心坐标center,就是眼睛的中心点坐标,知道了以后我们开始绘制眼珠
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawCircle(color = drawColor, radius = 10f, center = Offset(leftEyePathX,leftEyePathY))
drawCircle(color = drawColor, radius = 10f, center = Offset(rightEyePathX,rightEyePathY))
}
复制代码
我们再看下效果图
鼻子
接下去我们画鼻子,鼻子肯定在脑袋的中间,所以中心点x坐标就是脑袋中心点的x坐标,那鼻子的y坐标就设置成比中心点y坐标稍微高一点的位置,代码如下
val nosePath = Path()
val nosePathX = screenWidth() / 2
val nosePathY = screenHeight() * 13 / 24
nosePath.moveTo(nosePathX, nosePathY)
nosePath.addOval(Rect(nosePathX - 15f, nosePathY - 15f, nosePathX + 15f, nosePathY + 15f))
nosePath.close()
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawPath(path = nosePath, color = drawColor, style = Stroke(width = 10f))
}
复制代码
我们看下效果图
兔唇
兔子的样子逐渐出来了,画完鼻子我们接着画啥呢?没错,兔子最有特点的位置也就是兔唇,我们脑补下兔唇长啥样子,首先位置肯定是在鼻子的下面,然后从鼻子开始往两边分叉,也就是两个扇形,扇形怎么画呢,我们也有现成的api,drawArc,我们看下drawArc都提供了哪些属性
fun drawArc(
color: Color,
startAngle: Float,
sweepAngle: Float,
useCenter: Boolean,
topLeft: Offset = Offset.Zero,
size: Size = this.size.offsetSize(topLeft),
/*@FloatRange(from = 0.0, to = 1.0)*/
alpha: Float = 1.0f,
style: DrawStyle = Fill,
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
)
复制代码
我们需要用到的就是颜色color,这个扇形起始角度startAngle,扇形终止的角度sweepAngle,是否扇形两端跟中心点连接起来的布尔值useCenter,扇形的左上位置topLeft以及扇形的大小size也就是设置半径,知道这些以后我们开始逐个代入参数吧
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawArc(
color = drawColor,
0f,
120f,
style = Stroke(width = 10f),
useCenter = false,
size = Size(120f, 120f),
topLeft = Offset(nosePathX - 120f, nosePathY)
)
drawArc(
color = drawColor,
180f,
-120f,
style = Stroke(width = 10f),
useCenter = false,
size = Size(120f, 120f),
topLeft = Offset(nosePathX + 10f, nosePathY)
)
}
复制代码
画兔唇的时候其实就是在鼻子的两端各画一个坐标轴,左边的兔唇起始角度就是从x轴开始也就是0度,顺时针旋转120度,左上位置的x坐标刚好离开鼻子一个半径的位置,右边的兔唇刚好相反,逆时针旋转120度,起始角度是180度,左上位置的x坐标刚好在鼻子的位置那里,稍微加个10f让兔唇可以对称一些,我们看下效果图
胡须
脸上好像空了点,兔子的胡须还没有呢,胡须其实就是两边各画三条线,用drawLine这个api,起始位置的x坐标跟眼睛中心点的x坐标一样,中间胡须起始位置的y坐标跟鼻子的y坐标一样,上下胡须的y坐标各减去一定的数值
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawLine(
color = drawColor,
start = Offset(leftEyePathX, nosePathY - 60f),
end = Offset(leftEyePathX - 250f, nosePathY - 90f),
strokeWidth = 5f,
cap = StrokeCap.Round
)
drawLine(
color = drawColor,
start = Offset(leftEyePathX, nosePathY),
end = Offset(leftEyePathX - 250f, nosePathY),
strokeWidth = 5f,
cap = StrokeCap.Round
)
drawLine(
color = drawColor,
start = Offset(leftEyePathX, nosePathY + 60f),
end = Offset(leftEyePathX - 250f, nosePathY + 90f),
strokeWidth = 5f,
cap = StrokeCap.Round
)
drawLine(
color = drawColor,
start = Offset(rightEyePathX, nosePathY - 60f),
end = Offset(rightEyePathX + 250f, nosePathY - 90f),
strokeWidth = 5f,
cap = StrokeCap.Round
)
drawLine(
color = drawColor,
start = Offset(rightEyePathX, nosePathY),
end = Offset(rightEyePathX + 250f, nosePathY),
strokeWidth = 5f,
cap = StrokeCap.Round
)
drawLine(
color = drawColor,
start = Offset(rightEyePathX, nosePathY + 60f),
end = Offset(rightEyePathX + 250f, nosePathY + 90f),
strokeWidth = 5f,
cap = StrokeCap.Round
)
}
复制代码
很简单的画了六条线,线的粗细也稍微设置的小一点,毕竟胡须还是比较细的,我们看下效果图
就这样兔子脑袋部分所有元素都画完了,我们接着给兔子画身体
Corpo
O corpo é na verdade uma elipse, sua posição é apenas um terço da parte inferior da tela, a coordenada x do canto superior esquerdo é um pouco maior que a coordenada x do canto superior esquerdo da cabeça, o y- A coordenada é dois terços da tela e a coordenada x do canto inferior direito é menor que a coordenada x da cabeça As coordenadas são um pouco menores e a coordenada y é a parte inferior da tela. vai desenhar o corpo como a cabeça
val bodyPath = Path()
val bodyPathX = screenWidth() / 2
val bodyPathY = screenHeight() * 5 / 6
bodyPath.moveTo(bodyPathX, bodyPathY)
bodyPath.addOval(
Rect(
startX + 50f,
screenHeight() * 2 / 3,
screenWidth() - startX - 50f,
screenHeight()
)
)
bodyPath.close()
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawPath(path = bodyPath, color = drawColor, style = Stroke(width = 10f))
}
复制代码
As renderizações são as seguintes
duas garras
Depois de desenhar o corpo, vamos desenhar as garras do coelho. As garras são, na verdade, duas elipses. A coordenada x do centro da elipse é a mesma que a coordenada x dos dois olhos, e a coordenada y está em cinco sextos da tela.
val leftHandPath = Path()
val leftHandPathX = screenWidth() * 3 / 8
val leftHandPathY = screenHeight() * 5 / 6
leftHandPath.moveTo(leftHandPathX, leftHandPathY)
leftHandPath.addOval(
Rect(
leftHandPathX - 35f,
leftHandPathY - 90f,
leftHandPathX + 35f,
leftHandPathY + 90f
)
)
leftHandPath.close()
val rightHandPath = Path()
val rightHandPathX = screenWidth() * 5 / 8
val rightHandPathY = screenHeight() * 5 / 6
rightHandPath.moveTo(rightHandPathX, rightHandPathY)
rightHandPath.addOval(
Rect(
rightHandPathX - 35f,
rightHandPathY - 90f,
rightHandPathX + 35f,
rightHandPathY + 90f
)
)
rightHandPath.close()
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawPath(path = leftHandPath, color = drawColor, style = Stroke(width = 10f))
drawPath(path = rightHandPath, color = drawColor, style = Stroke(width = 10f))
}
复制代码
Vamos ver a renderização
Cauda
Ainda é a última etapa. Vamos desenhar uma cauda para o coelho. A coordenada x do ponto central da cauda é a largura da tela menos a coordenada do eixo x no lado direito da cabeça. A coordenada y do ponto central da cauda é a altura da tela menos um certo valor. Vejamos o código
val tailPath = Path()
val tailPathX = screenWidth() - startX
val tailPathY = screenHeight() - 200f
tailPath.moveTo(tailPathX, tailPathY)
tailPath.addOval(Rect(tailPathX - 60f, tailPathY - 90f, tailPathX + 60f, tailPathY + 90f))
tailPath.close()
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawPath(path = tailPath, color = drawColor, style = Stroke(width = 10f))
}
复制代码
Acabei de desenhar um coelho assim, vamos dar uma olhada na renderização final
Parece que sim, vamos enfeitar um pouco. Achamos o fundo um pouco monótono. Afinal, é o Ano Novo Chinês. Embora fogos de artifício não sejam permitidos em muitos lugares, ainda podemos dar uma olhada. Encontre uma foto dos fogos de artifício na Internet e dê para o Coelho Vamos usá-lo como plano de fundo. Há também uma API como drawImage que pode desenhar figuras na tela. O código é o seguinte
val bgBitmap = ImageBitmap.imageResource(id = R.drawable.firework_night)
Canvas(
modifier = Modifier
.size(screenWidth().dp, screenHeight().dp)
.background(color = Color.White)
) {
drawImage(image = bgBitmap,
srcOffset = IntOffset(0,0),
srcSize = IntSize(bgBitmap.width,bgBitmap.height),
dstSize = IntSize(screenWidth().toInt()*5/4,screenHeight().toInt()*5/4),
dstOffset = IntOffset(0,0)
)
}
复制代码
Vamos ver como isso funciona
Hmm~~ Pronto~~ Não parece muito bonito hahaha, mas a questão não é a estética, mas sim uma foto do Ano Novo Chinês, e a outra é usar as APIs do Canvas no Compose. Apesar de tudo, à medida que o kotlin amadurece gradualmente, eu pessoalmente sinto que o Compose provavelmente se tornará o modo de desenvolvimento de IU convencional depois do Android
Finalmente, gostaria de desejar a todos um feliz ano novo. Desejo a todos um feliz Ano do Coelho, e o "coelho" fará grandes progressos~~