手记

用 Compose 和 Canvas 画星星

我一直觉得在Canvas上作画很困难,不仅是在Compose的Canvas上,而且在任何其他技术上也一样。我已经能完成我需要做的任务,但尽量避免使用它来做这些任务。

在一个周日,我想做些有创意的事。我也想编程,所以我决定用Canvas做一个加载指示器。结果事情越做越多,最后我做了一个更像是插画而不是加载指示器的动画。

在这篇和接下来的博客文章中,我将分享我的方法以及我在过程中学到的一些经验。第一篇博客文章将介绍如何在画布上绘制元素,接下来的一篇将讲述动画的相关内容。

想法

太空主题插图最初的想法来自于我有一件T恤。就是这件来自Spark公司的I Need Space 女权T恤。我最初的想法是尝试模仿同样的图片,但在画完土星后,我决定自己来。

我决定添加其他元素,并让星星看起来更独特。我对最终效果非常满意!让我们开始绘制插图吧。在下面这个代码片段里可以找到这篇博客文章的[完整代码]。

部件

在讨论各个部分之前,我想指出一点:我在这里展示的具体数字只适用于这张特定的图纸,大多数情况下不能泛化。我根据设定的画布尺寸计算了位置和大小,而不是适用于可以调整大小的画布。

第一个组件是背景和画布。Column作为父组件,Canvas则是作为子组件。这使我们能够定义一些在Canvas中无法定义的动画和其他特性。

为了绘制背景,我们将使用父 ColumndrawBehind 修饰符,绘制一个圆角矩形,并定义一个径向渐变刷,包含一些颜色。

    Modifier.drawBehind {  
        drawRoundRect(  
            size = size,  
            cornerRadius = CornerRadius(44f),  
            brush =  
            Brush.radialGradient(  
                colors = listOf(  
                    Color(0xFF02010a),  
                    Color(0xFF04052e),  
                    Color(0xFF140152),  
                    Color(0xFF22007c),  
                    Color.Transparent,  
                ),  
                radius = size.width * 0.75f,  
            ),  
        )  
    },

到这个时候,画看起来是这样的。

你知道吗?土星,你知道它的环是怎么形成的吗?

我们首先添加的行星是土星。为了画它,我们需要三个部分:行星的轮廓、边缘和行星内部的线条。我们来定义一个扩展函数drawSaturn,它接收行星的中心偏移和轮廓样式,然后画出轮廓:

    // 绘制土星,给定中心点、偏移量和轮廓样式。
    fun DrawScope.drawSaturn(  
        center: Offset,  
        outlineStyle: Stroke,  
    ) {  
        drawCircle(  
            center = center,  
            color = Colors.white,  
            radius = 100f,  
            style = outlineStyle,  
        )  
    }

这是一个圆,它使用了中心坐标和轮廓样式(顺便说一下,这是一个宽度为4f的描边)。我们还为它设置了半径,这里值被硬编码为100f。

接下来,我们画行星的内部线条:

// 绘制一个弧形
drawArc(  
    topLeft = Offset(  
        center.x - 80f,   
        center.y - 80f  
    ),  
    color = Colors.white,  
    startAngle = 180f,  
    sweepAngle = 90f,  
    useCenter = false,  
    style =  
        Stroke(  
            width = 2f,  
        ),  
    size = Size(160f, 160f),  
)

为此,我们使用drawArc函数。这个函数与drawCircle函数有些不同,drawCircle函数需要中心坐标和半径作为参数。而对于弧形,我们需要定义矩形区域的左上角坐标和弧形的弧度和范围。

startAngle 定义绘制开始的角度,而 sweepAngle 则定义绘制继续的角度范围。角度的0位置在正下方,所以我们从 180f 度开始,并且使用 90f 的弧度角度来实现我们想要的效果。

我们还必须将 useCenter 设为 true;否则会在中心多画一条线。

最后一个完成Saturn的是轮圈。因为它不是完整的圆形,我们使用drawArc函数。代码如下所示:

    // 旋转40度, 以center为中心点
    rotate(40f, center) {  
        // 开始角度为217度,扫掠角度为285度
        drawArc(  
            color = Colors.white,  
            startAngle = 217f,  
            sweepAngle = 285f,  
            useCenter = false,  
            topLeft = Offset(  
                center.x - 50f,   
                center.y - 150f  
            ),  
            style = 轮廓样式,  
            size =  
                尺寸(100f, 300f),  
        )  
    }

需要注意的是,绘制弧线的区域不是正方形——因为轮缘的形状,我们需要使用非正方形尺寸来让其更像椭圆而不是圆。

此时,画大概就像这样等。

星球

接下来要添加的是另一颗行星。我不打算给这颗行星命名,而是让它更通用。所以我们就简单地叫它行星好了。

这颗行星有两个组成部分:行星本身和围绕它运转的月亮。我们先来看看行星的轮廓,它是一个圆:

# 假设这里的代码是表示绘制圆形的伪代码,保持原文格式不变。
绘制圆(
    中心点 = 中心,
    颜色 = Colors白色,
    半径 = 80f,
    样式 = 边框样式,
)

这段代码很简单;我们以 center 变量定义的位置为中心,画一个半径为 80 的圆。接下来,我们画星球上的线。为此,我们使用三条路径,其中两条是虚线,一条是实线路径。第一条线(最上面的那条)的代码如下所示:

绘制路径(从中心点向左移动82f并沿y轴保持不变,然后通过一个二次贝塞尔曲线移动到中心点位置,颜色为白色,并且样式为虚线,虚线的间隔为60f、10f、50f、10f,线宽为3f)。

其他人的情况差不多;数字和路径效果有些许差异。你可以从这篇博客文章的开头找到完整的代码链接。

所以,我们从圆的边缘向中心画线。因为这条线有点弯曲,我们用quadraticTo来完成这个效果。

行星的另一部分是月球。首先,它把月球画成一个圆。

画圆(  
    中心 = 偏移量(  
            中心.x - 100f,   
            中心.y + 80f  
         ),  
    颜色 = 颜色白色,  
    半径 = 15f,  
    轮廓样式 = 轮廓样式,  
)

然后,对于使月亮看起来像围绕行星运行的线条,我们使用 drawArc 方法:

绘制弧线(  
    左上角 = 偏移(center.x - 125f, center.y - 125f),  
    颜色 = Colors.white,  
    起始角度 = 160f,  
    扫描角度 = 200f,  
    使用中心点 = false,  
    描边 =  
        描边(  
            宽度 = 2f,  
            路径效果 =  
                路径效果虚线路径效果(  
                    浮点数组 = floatArrayOf(160f, 70f, 50f, 80f, 40f, 40f),  
                    相位 = 0f,  
                ),  
            ),  
        大小 = 大小(250f, 250f),  
    )

关于这个弧没有什么新的东西——它使用的是相对于月亮左上角的坐标。从160度开始,弧度延续到200度。为了避开通过中心的第三条线,它没有使用中心点,并且它使用了dashedPathEffect来产生这种效果。

这时,画看起来是这样。

星星们

最终的插图元素是星星部分。让我们为 DrawScope 添加一个扩展函数来画星星,然后用它来画所有的星星。

该函数接受星星的大小、中心偏移、星星的颜色和轮廓样式。我们来定义一下它:

    fun DrawScope.drawStar(  
        starSize: Size,  
        center: Offset,  
        color: Color,  
        outlineStyle: Stroke,  
    ) {  
       // 绘制星星的函数,参数包括星星的尺寸、中心点、颜色和描边样式。  
       ...  
    }

我们使用了一条带有二次贝塞尔曲线的路径,从一点到另一点。这个星形有四个角,如果我们把空间想象成一个正方形,那么这些角位于正方形边线的中点。让我们从顶部的中点开始:

val path = Path()  

path.moveTo(center.x, center.y - starSize.height * 0.5f) // 将路径移动到中心点的下方,距离为星形大小的一半高度

接下来,我们想从一个点到另一个点绘制一条二次贝塞尔曲线,并使用星形中心作为曲线控制点。从顶部到底部再到右侧的线看起来会是这样:

从中心点向路径添加一个二次贝塞尔曲线,终点的x坐标为星形宽度的一半加上中心点的x坐标,终点的y坐标与中心点相同。

前两个参数是控制点的坐标,后两个参数是线段的终点坐标。其余三条线看起来一样,只是终点坐标基于目标坐标而变化。

画完那四条线之后,我们就可以画路径了。

绘制路径(
路径 (绘制的路径) = path, 
颜色 (线条的颜色) = color, 
轮廓样式 (线条的样式) = outlineStyle, 
)

现在我们有了drawStar函数,我们可以绘制星星。我们希望把它们放在行星周围,并让它们有不同的颜色。我们还需要记录星星的大小。接下来,我们定义一个数据类来处理这些。

data class 星(  
    val 大小值: Float,  
    val 左上角坐标: Offset,  
    val 颜色值: Color,  
)

然后,我们可以定义一个星体列表。它看起来是这样的,列表中有一个星体的例子。

这里定义了一个名为starsList的列表,列表中有一个Star对象,其大小为30f,左上角位于(size.width 0.5f, size.height 0.5f),并且颜色是Colors.stars数组中的第一个颜色。

我们可以定义不同大小的星星,并使用几种不同的颜色。每颗星星的位置是手动计算的。

然后我们可以使用恒星列表,并用该函数把它们画出来。

starsList.forEach { (starSize, offset, color) ->  
    drawStar(  
        starSize = Size(starSize, starSize),  
        color = color,  
        center = offset,  
        outlineStyle =  
            Stroke(  
                width = 2f,  
            ),  
    )  
}

这些代码更改后,这幅图看起来是这样的:

结尾了

在这篇博客文章中,我们使用Canvas创建了一个图。目前还是静态的,但在下一篇文章中,我们将让图中的元素动起来。

你觉得你在使用Canvas方面的技能怎么样?你对自己有信心还是完全不想碰它?

博客文章里的链接

0人推荐
随时随地看视频
慕课网APP