新项目彻底用了compose来完成,这两天有个时刻轴的需求,搜索了一下彻底由compose完成的几个,作用都不算特别好,而且都是用canvas画的,这样的话和本来的view没什么差异,不能发挥compose可定制组合的长处,所以自己完成了一个。因为我自己平常基本不写文章,而且内容也是倾向compose新手的,所以或许写的比较烦琐,大佬们想看的能够直接跳到第三部分。欢迎指导!
在开端之前,先介绍一下这次完成的重点:Layout。
Layout用于完成自界说的布局,可用于丈量和定位其布局子项。咱们能够用这个完成之前自界说view的作用,不过这儿画的不是点线之类的东西,而是composable,而且只用核算放的方位就好,基于此咱们能够完成有多个插槽的布局。
先来看一下UI作用是什么样的
一、分化UI
通过观察UI,咱们能够将每个item分化为以下四个元素:圆点、线、时刻、内容。一个合格的组件,要允许运用者随意界说各个元素方位的完成,比如圆点或许变成方的,或许换成图片,线也或许是条实线,而且色彩是突变的。所以这儿这几个元素准确的来说,应该是四个插槽,这几个插槽供给了默许的款式是长这样。
圆点槽和时刻槽是垂直居中对齐的,圆点槽和线槽是水平居中对齐的,内容槽和时刻槽是左对齐,在圆点槽和时刻槽中心有必定间隔,咱们管他叫内容距左间隔。
每个item的最大宽度是圆点槽的宽+内容距左间隔+内容的宽。每个item的最大高度是圆点或许时刻槽的最大高度+内容的高度,不直接用时刻槽的高度是因为圆点槽假如放个图片的话,或许高度比时刻槽的高度要高。
因为这个线应该是连接两个圆点槽的,所以它的最大高度和最小高度其实都是一个,取决于两个圆点之间的间隔,正好是一个item的高度。
在多个item时,第一个元素的线从点开端往下,而最终一个则没有线(说高度为0也行)
二、完成每个插槽的默许UI
- 圆点
这个很简略,任意一个空的组件设置下修饰符就能够了。
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape) // 变圆
.background(MaterialTheme.colorScheme.primary)
)
- 线
实线很好完成,也通过background就能够
// 实线单色
Box(modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.background(MaterialTheme.colorScheme.primary)
)
// 突变也简略
Box(
modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.background(
Brush.linearGradient(
listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.primaryContainer
)
)
)
)
虚线略微麻烦一点,Brush中没有直接完成虚线的方法,所以我用drawBehind来完成了。drawBehind这儿的作用和Canvas()
是相同的,你能够直接用canvas来完成,重点便是里面的pathEffect。
Box(modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.drawBehind {
drawLine(
color = Color.LightGray,
strokeWidth = size.width,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = size.height),
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(8.dp.toPx(), 4.dp.toPx())
)
)
}
)
- 时刻
简略一个Text就能够。
Text("2023年9月28日")
- 内容
依据详细的内容来完成。
三、通过自界说的Layout将小UI拼装起来
现在咱们依据第一步的思路,来界说一个组件。
@Composable
fun TimelineItem(
modifier: Modifier = Modifier,
dot: @Composable () -> Unit, // 圆点槽
line: @Composable () -> Unit, // 线槽
time: @Composable () -> Unit,// 时刻槽
content: @Composable () -> Unit, // 内容槽
contentStartOffset: Dp = 8.dp // 内容距左间隔
)
然后咱们将第二步中的插槽的默许UI放上去。主要是圆点槽和线槽。
@Composable
fun TimelineItem(
modifier: Modifier = Modifier,
dot: @Composable () -> Unit = {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
)
},
line: @Composable () -> Unit = {
Box(modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.drawBehind {
drawLine(
color = Color.LightGray,
strokeWidth = size.width,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = size.height),
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(8.dp.toPx(), 4.dp.toPx())
)
)
}
)
},
time: @Composable () -> Unit,
content: @Composable () -> Unit,
contentStartOffset: Dp = 8.dp
)
界说好今后就能够开端做完成了,上面现已说过,咱们是通过自界说Layout来完成的,那么先看一下Layout的构成。
@UiComposable
@Composable inline fun Layout(
content: @Composable @UiComposable () -> Unit, // 可组合子项。
modifier: Modifier = Modifier, // 布局的修饰符
measurePolicy: MeasurePolicy //布局的丈量和定位的策略
)
这其间的content,便是指咱们这四个槽的内容。
Layout(
modifier = modifier,
content = {
dot()
// 通过ProvideTextStyle给时刻槽供给了一个默许字体色彩。
ProvideTextStyle(value = LocalTextStyle.current.copy(color = Color(0xff999999))) {
time()
}
content()
line()
},
measurePolicy = ...
咱们能够看到在content中,咱们将四个槽的内容全放进去了,那他们的方位和巨细是怎么决议的呢,便是在measurePolicy
中界说的。
MeasurePolicy
类要求咱们有必要完成measure
方法。
fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult
measurables列表中的每个Measurable都对应于布局的一个布局子级,便是咱们刚才在content中传入的内容,将按先后顺序存入这个列表。能够运用Measurable.measure
方法来丈量子级的巨细。该方法需求子级自己所需求的束缚Constraints(便是这个子级的最小最大尺度)
;不同的子级能够用不同的束缚来丈量,而不是统一用给出的这个constraints参数。丈量子级会回来一个Placeable,它的属性有该子级通过对应束缚丈量后的巨细(一旦通过丈量,这个子级的巨细就确认了,不能再次丈量)。最终在MeasureResult中,设置每个子级的方位就能够。
现在咱们的代码变成了这样:
@Composable
fun TimelineItem(
modifier: Modifier = Modifier,
dot: @Composable () -> Unit = {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
)
},
line: @Composable () -> Unit = {
Box(modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.drawBehind {
drawLine(
color = Color.LightGray,//Color(0xffeeeeee)
strokeWidth = size.width,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = size.height),
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(8.dp.toPx(), 4.dp.toPx())
)
)
}
)
},
time: @Composable () -> Unit,
content: @Composable () -> Unit,
contentStartOffset: Dp = 8.dp,
position: TimelinePosition = TimelinePosition.Center
) {
Layout(
modifier = modifier,
content = {
dot()
ProvideTextStyle(value = LocalTextStyle.current.copy(color = Color(0xff999999))) {
time()
}
content()
line()
},
measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
TODO: 详细的四个子级丈量巨细的方位设置。
}
}
)
}
现在咱们来做详细的完成。
咱们先来丈量一下这儿的圆点槽的巨细。
val dot = measurables[0].measure(constraints)
因为咱们在content中第一个传入的便是dot(),所以这儿measurables[0]便是圆点槽组件,这样就得到了其对应的Placeable。
咱们先放置下这个圆点槽显现下看看作用。
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
val dot = measurables[0].measure(constraints)
return layout(constraints.maxWidth, constraints.maxHeight) {
dot.place(0, 0, 1f)
}
}
理论上咱们应该看到一个巨细8dp,主题色的圆点在左上角。咱们能够跑一下看看是不是契合预期。
要指出的是,这个方法给出的constraints并不是适宜dot的束缚,其最小宽度将或许远远大于dot的宽,这将导致丈量后dot的宽远超设定的8dp。所以这儿咱们需求运用dot正确的束缚, 而这个圆点槽理论上是不限制巨细的,所以其最小宽度应该设置为0。咱们依次将圆,时刻,和内容的巨细也丈量出来。
val constraintsFix = constraints.copy(minWidth = 0)
val dot = measurables[0].measure(constraintsFix)
val time = measurables[1].measure(constraintsFix)
val content = measurables[2].measure(constraintsFix)
之所以纷歧并把线槽的巨细也丈量了,是因为咱们在第一步中说的,线槽的高度,实际上是由圆点或许时刻槽的最大高度+内容的高度来决议的。
val topHeight = max(time.height, dot.height) // 取圆点槽和时刻槽中最大槽位的高度。
val lineHigh = topHeight + content.height // 整个组件的高度
val line = measurables[3].measure(
constraints.copy(
minWidth = 0,
minHeight = lineHeight,
maxHeight = lineHeight
)
)
至此咱们现已将四个槽位的巨细全部确认了下来。接下来就该指定每个槽位的方位,在第一步咱们现已分析过每个槽位应该地点的方位。
val height = topHeight + content.height // 整个组件的高度
// 时刻或内容的最大宽度 + 内容距左间隔 + 圆点宽度 = 整个组件的宽度
val width =
max(content.width, time.width) + contentStartOffset.roundToPx() + dot.width
return layout(width, height) { // 设置layout占有的巨细
val dotY = (topHeight - dot.height) / 2 // 核算圆点槽y轴方位
dot.place(0, dotY, 1f) // 放圆点槽
val timeY = (topHeight - time.height) / 2 // 核算时刻槽y轴方位
time.place(dot.width + contentStartOffset.roundToPx(), timeY) // 放时刻槽
content.place(dot.width + contentStartOffset.roundToPx(), topHeight) // 放内容槽,x和时刻槽相同,构成左对齐作用。
line.place(
dot.width / 2, // x在圆中心
dotY + dot.height // y从圆的最下面开端
)
}
至此咱们就有了一个时刻轴节点组件,马上在LazyColumn或许Column中试试作用吧!
四、完善作用
假如你刚才测试了作用,你会发现,在列表中最终一个节点,也有虚线,而且长度超出了列表,而最终一个节点,不应该显现虚线才对。所以咱们要来完善一下作用。
@Composable
fun TimelineItem(
modifier: Modifier = Modifier,
dot: @Composable () -> Unit = ...,
line: @Composable () -> Unit = ...,
time: @Composable () -> Unit,
content: @Composable () -> Unit,
contentStartOffset: Dp = 8.dp,
isEnd: Boolean = false, // 添加是否为最终一个节点的参数
)
...
//在最终依据是否是最终一个节点来设置是否放置线槽内容。
if (!isEnd){
line.place(
dot.width / 2,
dotY + dot.height
)
}
而在调用时,只要简略的依据是否坐落列表最终就能够了,调用示例:
LazyColumn(
Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
itemsIndexed(list.itemSnapshotList) { index, item ->
item?.let {
TimelineItem(
modifier = Modifier.fillMaxWidth(),
time = {
Text(text = it.time)
},
content = {
Column {
// 最好在Column最上面和最下面也添加个spacer来间离隔
}
},
isEnd = index == list.itemCount - 1
)
}
}
}
最终
至此本文就完毕啦,因为内容比较简略,且所以的代码均有体现,为了不占篇幅,就不再张贴完好代码内容了。假如本文有错误之处或许能够改善的地方,请咱们必定回复指正;假如文章的内容也对你有帮忙,也请回复鼓舞我,谢谢咱们!