携手创造,共同生长!这是我参加「日新计划 8 月更文应战」的第6天,点击查看活动详情

前言

前段时间谷歌开发者大众号发布了一个 compose 进阶应战,应战内容是彻底运用 compose 编写一个核算器 APP

考虑了一下准备做一个“仿真”形式的核算器。

那么,既然想要做“仿真”,自然少不了显现作用的复原,经典的核算器都是运用的 LCD 显现屏,经过操控不同显象区域的显现与躲藏达到显现 0-9 的数字的意图。

显现作用大致如下:

使用 compose 的 Canvas 自定义绘制实现 LCD 显示数字效果

本文的内容便是经过运用 compose 的自界说制作(Canvas),完成上图作用。

最终完成作用如图:

使用 compose 的 Canvas 自定义绘制实现 LCD 显示数字效果

开端编写

运用直线制作

制作主体

仔细分析上图,不难发现,其实不过便是一个由3条短横线,4条长竖线构成的 “8” 字形显现区域,经过改换不同的线段显现躲藏来生成不同的数字。

既然如此,肯定想到的便是运用 drawLine 来完成。

首要界说一个 composable 函数:

@Composable
fun LcdNumber(
    number: Int,
    modifier: Modifier = Modifier,
    defaultColor: Color = Color.Gray,
    numberColor: Color = Color.Black,
    numberSize: IntSize = IntSize(10, 30)
)

其中, number 表明要显现的数字,这儿只允许传入 0-9;defaultColor 表明线段没有显现时的默许色彩; numberColor 表明线段需求显现时的色彩; numberSize 表明制作数字的区域大小。

在开端正式完成之前,咱们需求先写几个辅佐办法。

首要,咱们需求判别某条线段是否应该显现,为了便利阐明,咱们把不同线段编号如下:

使用 compose 的 Canvas 自定义绘制实现 LCD 显示数字效果

然后,编写判别办法如下:

private fun isNeedShow(index: Int, number: Int): Boolean {
    return when (index) {
        0 -> {
            number != 1 && number != 4
        }
        1 -> {
            number != 5 && number != 6
        }
        2 -> {
            number != 2
        }
        3-> {
            number != 1 && number != 4 && number != 7
        }
        4-> {
            number != 1 && number != 3 && number != 4 && number != 5 && number != 7 && number != 9
        }
        5-> {
            number != 1 && number != 2 && number != 3
        }
        6 -> {
            number != 0 && number != 1 && number != 7
        }
        else -> {
            false
        }
    }
}

办法写的很简略粗犷,一看就懂,例如,参阅上面的图示索引,关于编号为 0 的直线,除了数字 1 和 4 ,其他数字都需求显现。

有了判别是否需求显现的办法,再简略加两个办法:

private fun getLcdNumberColor(defaultColor: Color, numberColor: Color, isNeedShow: Boolean): Color {
    return if (isNeedShow) numberColor else defaultColor
}
private fun getLcdNumberAlpha(isNeedShow: Boolean): Float {
    return if (isNeedShow) 1f else 0.35f
}

一个用来获取直线色彩,由于却决于直线是否显现,它们的色彩是不同的;一个用来获取直线的透明度,当某条直线不显现时,不仅要运用淡色色彩,还应该把透明度降低,不然不好看。

完成上面的辅佐办法后,就能够开端制作直线了:

@Composable
fun LcdNumber(
    number: Int,
    modifier: Modifier = Modifier,
    defaultColor: Color = Color.Gray,
    numberColor: Color = Color.Black,
    numberSize: IntSize = IntSize(10, 30)
) {
    Canvas(modifier = modifier.size(numberSize.width.dp, numberSize.height.dp)) {
        if (number !in 0..9) return@Canvas
        val shortLineSize = numberSize.width.toFloat()
        val longLineSize = numberSize.height / 2f
        val strokeWidth = shortLineSize / 3f
        var isNeedShow = isNeedShow(0, number)
        drawLine(
            color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
            start = Offset(0f, 0f),
            end = Offset(shortLineSize, 0f),
            strokeWidth = strokeWidth,
            alpha = getLcdNumberAlpha(isNeedShow)
        )
        isNeedShow = isNeedShow(1, number)
        drawLine(
            color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
            start = Offset(shortLineSize, 0f),
            end = Offset(shortLineSize, longLineSize),
            strokeWidth = strokeWidth,
            alpha = getLcdNumberAlpha(isNeedShow)
        )
        isNeedShow = isNeedShow(2, number)
        drawLine(
            color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
            start = Offset(shortLineSize, longLineSize),
            end = Offset(shortLineSize, longLineSize * 2),
            strokeWidth = strokeWidth,
            alpha = getLcdNumberAlpha(isNeedShow)
        )
        isNeedShow = isNeedShow(3, number)
        drawLine(
            color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
            start = Offset(0f, longLineSize*2),
            end = Offset(shortLineSize, longLineSize*2),
            strokeWidth = strokeWidth,
            alpha = getLcdNumberAlpha(isNeedShow)
        )
        isNeedShow = isNeedShow(4, number)
        drawLine(
            color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
            start = Offset(0f, longLineSize),
            end = Offset(0f, longLineSize*2),
            strokeWidth = strokeWidth,
            alpha = getLcdNumberAlpha(isNeedShow)
        )
        isNeedShow = isNeedShow(5, number)
        drawLine(
            color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
            start = Offset(0f, 0f),
            end = Offset(0f, longLineSize),
            strokeWidth = strokeWidth,
            alpha = getLcdNumberAlpha(isNeedShow)
        )
        isNeedShow = isNeedShow(6, number)
        drawLine(
            color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
            start = Offset(0f, longLineSize),
            end = Offset(shortLineSize, longLineSize),
            strokeWidth = strokeWidth,
            alpha = getLcdNumberAlpha(isNeedShow)
        )
    }
}

同样是简略粗犷的直接制作,咱们看一下预览作用:

@Preview(showSystemUi = true)
@Composable
fun PreviewLcdNumber() {
    Column(modifier = Modifier
        .fillMaxSize()
        .padding(8.dp)
    ) {
        Row {
            LcdNumber(number = 0)
            LcdNumber(number = 1)
            LcdNumber(number = 2)
            LcdNumber(number = 3)
            LcdNumber(number = 4)
            LcdNumber(number = 5)
            LcdNumber(number = 6)
            LcdNumber(number = 7)
            LcdNumber(number = 8)
            LcdNumber(number = 9)
        }
    }
}

使用 compose 的 Canvas 自定义绘制实现 LCD 显示数字效果

如同也还行?可是仔细一看,如同不行拟真啊?再看看原图和仿写的对比:

使用 compose 的 Canvas 自定义绘制实现 LCD 显示数字效果

发现了吗?没错,原图每个直线的两端都是有不同的斜角的,并且直线之间并不是直接连在一起的,而是有一定的距离的。

距离这个还好调整,修正一下 drawLinestartend 参数就行了。

可是斜角要怎样完成呢?

查看 drawLine 办法参数,发现有一个 cap 参数:

cap treatment applied to the ends of the line segment

能够运用这个参数更改线段的末尾款式,可是,只供给了三种改换办法:

companion object {
    /**
     * Begin and end contours with a flat edge and no extension.
     */
    val Butt = StrokeCap(0)
    /**
     * Begin and end contours with a semi-circle extension.
     */
    val Round = StrokeCap(1)
    /**
     * Begin and end contours with a half square extension. This is
     * similar to extending each contour by half the stroke width (as
     * given by [Paint.strokeWidth]).
     */
    val Square = StrokeCap(2)
}

貌似还不支撑自己编写,反正我翻了一圈文档和源码,没有发现能自己编写的地方。

也便是说,这个也行不通。

那怎样办呢?

或许咱们能够略微变通一下,运用制作矩形来完成斜角作用?

制作两端斜角

咱们能够经过 rotate 办法,旋转制作的内容。

因而或许咱们能够制作一个特定尺度的矩形,然后旋转,以此完成斜角作用:

@Preview(showSystemUi = true)
@Composable
fun PreviewLine() {
    Canvas(modifier = Modifier.size(100.dp, 100.dp)) {
        drawLine(
            color = Color.Black,
            start = Offset(10f, 20f),
            end = Offset(70f, 20f),
            strokeWidth = 20f
        )
        withTransform({
            rotate(45F, Offset(80f, 20f))
        }) {
            drawRect(
                Color.Black,
                topLeft = Offset(65f, 20f),
                size = Size(15f, 15f)
            )
        }
    }
}

上面的代码中,咱们先制作了一个直线,然后制作一个矩形,并应用旋转改换,得出作用如下:

使用 compose 的 Canvas 自定义绘制实现 LCD 显示数字效果

emmm,怎样说呢,确实是完成了斜角的作用,可是这也和原图的不符合啊,并且原图是单面斜角,不是这种箭头啊。 假如能够制作一个三角形,直接把三角形拼上去就简略多了,可是很显然, compose 没有供给制作三角形的办法, 可是能够经过 drawPath 自己完成,不过都运用 drawPath 了,为什么还要选用拼接直线和三角形的办法呢?直接用 drawPath 一把梭哈不是更香?

直接运用 drawPath 制作

先用量角器量一下斜角的视点:

使用 compose 的 Canvas 自定义绘制实现 LCD 显示数字效果

很显然,是 45 ,其它的斜角我也量过了,都是 45 ,那就好说了,至少不用算三角函数了。

接下来便是运用 drawPath 制作直线,这儿咱们以 0 号直线为例:

@Composable
private fun Line0(width: Float, length: Float) {
    Canvas(modifier = Modifier.size(100.dp, 100.dp)) {
        val path = Path()
        path.moveTo(0f, 0f)
        path.lineTo(length, 0f)
        path.lineTo(length-width, width)
        path.lineTo(width, width)
        drawPath(path = path, color = Color.Black)
    }
}

上面代码中 width 表明线宽, length 表明线长。

由于斜角是 45 所以不需求做坐标核算,直接运用 path.lineTo(length-width, width) 即可制作出右边斜角,而左边斜角则直接运用 path.lineTo(width, width) 制作。

让咱们来看看作用:

使用 compose 的 Canvas 自定义绘制实现 LCD 显示数字效果

唔,总算对味了,那接下来便是把其他几条直线也画出来就 OK 了:

@Composable
fun LcdNumber(
    number: Int,
    modifier: Modifier = Modifier,
    defaultColor: Color = Color.Gray,
    numberColor: Color = Color.Black,
    numberSize: IntSize = IntSize(10, 30)
) {
    Canvas(modifier = modifier.size(numberSize.width.dp, numberSize.height.dp)) {
        if (number !in 0..9) return@Canvas // 假如不是数字 0-9 就直接退出
        val path = Path()
        var shortLineSize = numberSize.width.toFloat() // 运用画布宽度作为横线长度
        val longLineSize = numberSize.height / 2f // 运用画布高度的一半作为竖线长度
        val strokeWidth = shortLineSize / 3f // 运用横线的 1/3 作为线段宽度
        val spacing = 1f // 线段间隔 1 像素
        var isNeedShow = false
        // draw line 0
        isNeedShow = isNeedShow(0, number)
        path.moveTo(0f, 0f) // 移动画笔至画布原点
        path.lineTo(shortLineSize, 0f) // 从上一个点向右直线移动到横线长度方位
        path.lineTo(shortLineSize - strokeWidth, strokeWidth) // 从上一个点向左偏移线段宽度并向下偏移线段宽度,直线移动
        path.lineTo(strokeWidth, strokeWidth) // 从上一个偏移至xy轴至线段宽度,直线移动
        // 依照上面的途径制作图形,闭合最终一个坐标和第一个坐标,并且填充图形
        drawPath(
            path = path,
            color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
            alpha = getLcdNumberAlpha(isNeedShow)
        )
        // draw line3
        isNeedShow = isNeedShow(3, number)
        // 直接经过旋转 0 号直线的 path 制作 3 号直线
        // 旋转视点为顺时针 180 ,旋转中心为 shortLineSize/2, longLineSize+strokeWidth+spacin
        // 即整个数字的中心点
        rotate(180f, Offset(shortLineSize/2, longLineSize+strokeWidth+spacing)) {
            drawPath(
                path = path,
                color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
                alpha = getLcdNumberAlpha(isNeedShow)
            )
        }
        // draw line1
        path.reset() // 清除上次对 Path 的操作,重新开端新的偏移
        isNeedShow = isNeedShow(1, number)
        path.moveTo(shortLineSize+spacing, spacing)
        path.lineTo(shortLineSize+spacing, spacing + longLineSize)
        path.lineTo(shortLineSize+spacing-strokeWidth/2, longLineSize+spacing+strokeWidth)
        path.lineTo(shortLineSize+spacing-strokeWidth, longLineSize+spacing)
        path.lineTo(shortLineSize+spacing-strokeWidth, strokeWidth+spacing)
        drawPath(
            path = path,
            color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
            alpha = getLcdNumberAlpha(isNeedShow)
        )
        // draw line4
        isNeedShow = isNeedShow(4, number)
        rotate(180f, Offset(shortLineSize/2, longLineSize+strokeWidth+spacing)) {
            drawPath(
                path = path,
                color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
                alpha = getLcdNumberAlpha(isNeedShow)
            )
        }
        // draw line2
        isNeedShow = isNeedShow(2, number)
        path.reset()
        path.moveTo(shortLineSize+spacing-strokeWidth/2, longLineSize+spacing*2+strokeWidth)
        path.lineTo(shortLineSize+spacing, longLineSize+spacing*2+strokeWidth*2)
        path.lineTo(shortLineSize+spacing, longLineSize*2+spacing*2+strokeWidth*2)
        path.lineTo(shortLineSize+spacing-strokeWidth, longLineSize*2+spacing*2+strokeWidth)
        path.lineTo(shortLineSize+spacing-strokeWidth, longLineSize+spacing*2+strokeWidth*2)
        drawPath(
            path = path,
            color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
            alpha = getLcdNumberAlpha(isNeedShow)
        )
        // draw line5
        isNeedShow = isNeedShow(5, number)
        rotate(180f, Offset(shortLineSize/2, longLineSize+strokeWidth+spacing)) {
            drawPath(
                path = path,
                color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
                alpha = getLcdNumberAlpha(isNeedShow)
            )
        }
        // draw line6
        isNeedShow = isNeedShow(6, number)
        shortLineSize -= strokeWidth
        path.reset()
        path.moveTo(strokeWidth+spacing, longLineSize+spacing*2)
        path.lineTo(strokeWidth+spacing+shortLineSize-strokeWidth*2, longLineSize+spacing*2)
        path.lineTo(strokeWidth+spacing+shortLineSize-strokeWidth, longLineSize+spacing*2+strokeWidth/2)
        path.lineTo(strokeWidth+spacing+shortLineSize-strokeWidth*2, longLineSize+spacing*2+strokeWidth)
        path.lineTo(strokeWidth+spacing, longLineSize+spacing*2+strokeWidth)
        path.lineTo(strokeWidth+spacing-strokeWidth, longLineSize+spacing*2+strokeWidth/2)
        translate(left = strokeWidth/2, top = strokeWidth/2) {
            drawPath(
                path = path,
                color = getLcdNumberColor(defaultColor, numberColor, isNeedShow),
                alpha = getLcdNumberAlpha(isNeedShow)
            )
        }
    }
}

上面代码我已经添加了注释。

这儿说一下, 3 号直线能够由 0 号直线旋转得到、4 号 可由 1 号旋转得来、5 号可由 2 号旋转得来。

其实,理论上来说,横向的直线,除了六号,其他悉数能够由 0 号旋转得来,竖向直线悉数能够由 1 号旋转得来,可是我翻遍了文档和源码没有找到 Z 轴旋转,只有 X,Y 轴旋转,所以导致有些线无法直接旋转得到。

(ps:其实看源码找到一个经过矩阵变形能够完成 Z 轴旋转,可是引进矩阵反而会更麻烦了,索性多写几个算了)

咱们来看看作用怎样样:

使用 compose 的 Canvas 自定义绘制实现 LCD 显示数字效果

哈哈,总算像了!

虽然由于某些尺度核算可能不太完美,导致字体有点偏“瘦长”了,可是总体来说仍是挺复原的。

对了,上面的代码,我没有对单位进行换算,各位运用时别忘了换算一下单位

总结

compose 的 Canvas 的自界说制作比较于原生 view 的制作简略的多,由于少了许多模板代码,也不用去考虑生命周期的问题。

可是简略也有简略的劣势,那便是可定制性比较于原生 view 没有那么多,少了一些办法。

对了,写完这个“仿真”显现界面,我突然觉得如同“仿真”核算器并没有什么意思,所以决议不做这个类型的了(笑