携手创造,共同生长!这是我参加「日新计划 8 月更文应战」的第6天,点击查看活动详情
前言
前段时间谷歌开发者大众号发布了一个 compose 进阶应战,应战内容是彻底运用 compose 编写一个核算器 APP。
考虑了一下准备做一个“仿真”形式的核算器。
那么,既然想要做“仿真”,自然少不了显现作用的复原,经典的核算器都是运用的 LCD 显现屏,经过操控不同显象区域的显现与躲藏达到显现 0-9 的数字的意图。
显现作用大致如下:
本文的内容便是经过运用 compose 的自界说制作(Canvas),完成上图作用。
最终完成作用如图:
开端编写
运用直线制作
制作主体
仔细分析上图,不难发现,其实不过便是一个由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
表明制作数字的区域大小。
在开端正式完成之前,咱们需求先写几个辅佐办法。
首要,咱们需求判别某条线段是否应该显现,为了便利阐明,咱们把不同线段编号如下:
然后,编写判别办法如下:
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)
}
}
}
如同也还行?可是仔细一看,如同不行拟真啊?再看看原图和仿写的对比:
发现了吗?没错,原图每个直线的两端都是有不同的斜角的,并且直线之间并不是直接连在一起的,而是有一定的距离的。
距离这个还好调整,修正一下 drawLine
的 start
和 end
参数就行了。
可是斜角要怎样完成呢?
查看 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)
)
}
}
}
上面的代码中,咱们先制作了一个直线,然后制作一个矩形,并应用旋转改换,得出作用如下:
emmm,怎样说呢,确实是完成了斜角的作用,可是这也和原图的不符合啊,并且原图是单面斜角,不是这种箭头啊。
假如能够制作一个三角形,直接把三角形拼上去就简略多了,可是很显然, compose 没有供给制作三角形的办法,
可是能够经过 drawPath
自己完成,不过都运用 drawPath
了,为什么还要选用拼接直线和三角形的办法呢?直接用 drawPath
一把梭哈不是更香?
直接运用 drawPath 制作
先用量角器量一下斜角的视点:
很显然,是 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)
制作。
让咱们来看看作用:
唔,总算对味了,那接下来便是把其他几条直线也画出来就 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 的自界说制作比较于原生 view 的制作简略的多,由于少了许多模板代码,也不用去考虑生命周期的问题。
可是简略也有简略的劣势,那便是可定制性比较于原生 view 没有那么多,少了一些办法。
对了,写完这个“仿真”显现界面,我突然觉得如同“仿真”核算器并没有什么意思,所以决议不做这个类型的了(笑