假如现在有一个需求如下:
我们观察后,可以得到如下需求点:
假如我们使用传统View开发,自然而然的想到可以使用RecyclerView
来实现,但是有两个点比较棘手:
RecyclerView
回收机制出现问题,不能正确的回收 Item首先对于第一点,我们需要自定义RecyclerView
使用的LayoutManager
:
但这着实有一点点的复杂和困难(对于大多数普通开发者而言,大神请忽略),因为我们需要彻底理解LayoutManager
的回收策略到底是如何工作的,才能下手修改代码,不然有可能自己造出来的轮子有bug,那就得不偿失了。
其次,对于六边形的 Item,我们需要通过自定义View+Canvas绘制
来实现:
但是这样做会对扩展性有一定的限制,因为我们使用的是继承 FrameLayout 来实现的,假如将来有其他的需求可能需要不断的修改这个自定义View。
在解决了上面两个棘手的点之后,我们终于可以下手来写代码解决这个需求了,但是我们仍然需要创建一大堆的相关准备文件,例如 Adapter、xml布局文件…
总之,如果使用传统 View 开发,为了使用RecyclerView
来实现该需求,我们需要写一大堆的乱七八糟的东西:
假如我们使用 Jetpack Compose 来开发这个需求将会非常简单,我们需要做的最大工作可能就是创建一个六边形的Shape
来给Modifier.clip()
方法使用:
我们需要自定义一个六边形的 HexagonShape
(本文后面会给出实现), 然后接下来就可以直接写业务代码了,我们可以使用 LazyColumn
来实现滚动列表,对于 Item 的重叠交错功能,还记得LazyColumn
有一个verticalArrangement
属性吗?我们通过Arrangement.spacedBy()
为该属性设置一个负的dp
值即可:
对于多类型的Item,其实 Jetpack Compose 中的 LazyList
系列组件的item
DSL语法可以天然的很好的支持多类型的Item设置,例如:
当然,我们也可以选择直接在 items
这个DSL函数中显示的指定contentType
来区分不同的Composable组件的展示:
LazyColumn(...) {
items(list, contentType = { it.type }) { item ->
if (item.type == 1) {
// 一种类型的Composable组件
......
} else {
// 另一种类型的Composable组件
......
}
}
}
更多关于 LazyList
系列组件的使用可以参考我的另一篇文章:Jetpack Compose中的列表 。
总之,如果使用 Jetpack Compose 来开发这个需求,相比使用View开发而言,所做的工作是极少的:
使用 Jetpack Compose 来开发还具备以下优点:
以下是实现上面需求的完整示例代码:
@Composable
fun HexagonItemList() {
LazyColumn(
verticalArrangement = Arrangement.spacedBy((-100).dp),
contentPadding = PaddingValues(20.dp)
) {
items(4) { Hexagon(it) }
item { RecommendedText() }
items(50) { Hexagon(it) }
}
}
@Composable
fun Hexagon(index: Int) {
val arrangement = if (index % 2 == 0) Arrangement.Start else Arrangement.End
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = arrangement,
) {
Box(
modifier = Modifier
.fillMaxWidth(0.55f)
.height(200.dp)
.clip(HexagonShape)
.background(OrangeColor),
contentAlignment = Alignment.Center
) {
Text(text = "Item $index", fontSize = 15.sp)
}
}
}
@Composable
fun RecommendedText() {
Column(
modifier = Modifier.height(300.dp).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Recommended items", fontSize = 20.sp, fontWeight = FontWeight.Bold)
Text(text = "Based on your interests")
}
}
val OrangeColor = Color(255,197,2)
val HexagonShape = Polygon(6, 0f)
/**
* 根据边数sides创建指定多边形的Shape, 可以用于Modifier.clip()方法中
*/
class Polygon(private val sides: Int, private val rotation: Float = 0f) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
return Outline.Generic(
Path().apply {
val radius = if (size.width > size.height) size.width / 2f else size.height / 2f
val angle = 2.0 * Math.PI / sides
val cx = size.width / 2f
val cy = size.height / 2f
val r = rotation * (Math.PI / 180)
moveTo(
cx + (radius * cos(0.0 + r).toFloat()),
cy + (radius * sin(0.0 + r).toFloat())
)
for (i in 1 until sides) {
lineTo(
cx + (radius * cos(angle * i + r).toFloat()),
cy + (radius * sin(angle * i + r).toFloat())
)
}
close()
}
)
}
}
实际运行效果如下:
细心的同学可能发现了开头提到的需求的示意图中的六边形是带有圆角的,很遗憾的是上面这种直接继承Shape
来创建自定义的方式是无法指定多边形的圆角,但是,Compose中能创建多边形的方式可不止这一种呢,比如我们可以选择在 Modifier.drawBehind{ }
中使用Canvas
来绘制一个六边形,这时就可以自己指定圆角相关的属性了:
/**
* 绘制带圆角的六边形,关键点是设置绘制的style的pathEffect,通过PathEffect.cornerPathEffect指定圆角半径
*/
fun DrawScope.drawHexagon(color: Color, cornerRadius: Float = 0f, fill: Boolean = true) {
val canvasWidth = size.width
val canvasHeight = size.height
val cx = canvasWidth / 2
val cy = canvasHeight / 2
val radius = (canvasHeight - 20.dp.toPx()) / 2
val path = createPolygonPath(cx, cy, 6, radius)
if (fill) {
// 这种方式绘制可以填充整个多边形的内容区域
drawIntoCanvas { canvas ->
canvas.drawOutline(
outline = Outline.Generic(path),
paint = Paint().apply {
this.color = color
pathEffect = PathEffect.cornerPathEffect(cornerRadius)
}
)
}
} else {
// 但是也可以通过两遍绘制来实现填充的效果
// drawPath(
// color = color,
// path = path,
// style = Fill
// )
// 这种方式只会绘制边框
drawPath(
color = color,
path = path,
style = Stroke(
width = 4.dp.toPx(),
pathEffect = PathEffect.cornerPathEffect(cornerRadius)
)
)
}
}
/**
* 根据边数sides创建多边形
*/
fun createPolygonPath(cx: Float, cy: Float, sides: Int, radius: Float): Path {
val angle = 2.0 * Math.PI / sides
return Path().apply {
moveTo(
cx + (radius * cos(0.0)).toFloat(),
cy + (radius * sin(0.0)).toFloat()
)
for (i in 1 until sides) {
lineTo(
cx + (radius * cos(angle * i)).toFloat(),
cy + (radius * sin(angle * i)).toFloat()
)
}
close()
}
}
这里将drawHexagon
定义为DrawScope
的一个扩展函数,这样就可以在任何可以使用Canvas
API的地方调用它。
接下来就可以在 Hexagon
组件上应用这个函数来绘制六边形了:
@Composable
fun Hexagon(index: Int) {
val arrangement = if (index % 2 == 0) Arrangement.Start else Arrangement.End
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = arrangement,
) {
Box(
modifier = Modifier
.fillMaxWidth(0.55f)
.height(200.dp)
.drawBehind { drawHexagon(OrangeColor, 30f) },
contentAlignment = Alignment.Center
) {
Text(text = "Item $index", fontSize = 15.sp)
}
}
}
当然这里你也可以直接把drawBehind { drawHexagon(OrangeColor, 30f) }
这一句定义为一个Modifier
的扩展函数。
调用方式跟前面一样,可以稍微调整一下间接:
@Composable
fun HexagonItemList() {
LazyColumn(
verticalArrangement = Arrangement.spacedBy((-115).dp),
contentPadding = PaddingValues(30.dp)
) {
items(4) { Hexagon(it) }
item { RecommendedText() }
items(50) { Hexagon(it) }
}
}
实际运行效果:
可以看到现在的六边形具有圆角了,跟开头的图中的一样。并且这种方式定义的形状可以在几乎任何Composable组件上通过Modifier
进行应用,这比传统View中写一个自定义View要舒服多了。
更多关于 Canvas 绘制相关的内容,可以参考我之前的文章:Jetpack Compose中的Canvas 。
Jetpack Compose相比传统View开发真的是太香了,能以更少、可读性更好、维护性更好的代码来实现以前更复杂的业务需求。如果你是一个Android开发老鸟,相信会对本文所表达的二者之间的天差地别有更深刻的体会。所以,是时候该将鸟枪换炮了!请转发给你的团队开发决策者。
如果对关于 Jetpack Compose 的更多内容感兴趣,请参考我的另一篇对这方面的整理:Jetpack Compose 学习汇总 。
参考:Fun design with Lazy layouts: Community tip - MAD Skills