上一篇讲了 KtJC 的入门(点此进入),这一篇就讲一些好玩的吧,毕竟真要上项目了就完全不是那些 Demo 里面搞的东西了,虽然官方一再强调千万不要用来做为生产用 :(
一、ListView
可能你直接就发现了,在 JC 里面是没有 ListView 的,要实现的话只能自己去搭,先套一个垂直滚动容器,再套一个 Column,然后再按列表内容来套一大堆的声明,代码类似于这样:
VerticalScroller {
Column {
(0 until 20).forEachIndexed { index, _ ->
FlexRow(crossAxisAlignment = CrossAxisAlignment.Center) {
expanded(1.0f) {
Text("Item $itemIndex")
}
inflexible {
Button(
"Button $itemIndex",
style = ContainedButtonStyle(),
onClick = { }
)
}
}
Divider(color = Color.Blue, height = 1.dp)
}
}
}
总感觉很怪,嵌套层次过深,我希望将它写得更简单并且好理解一些,如:
@Composable
fun ListView(
itemCount: Int,
children: @Composable() (index: Int) -> Unit) {
VerticalScroller {
Column {
for (index in 0 until itemCount) {
children(index)
}
}
}
}
这样我就可以直接使用 ListView 组件了:
ListView(itemCount = 20) { index ->
FlexRow(crossAxisAlignment = CrossAxisAlignment.Center) {
expanded(1.0f) {
Text("Item $index")
}
inflexible {
Button("Button $index",
style = ContainedButtonStyle(),
onClick = { }
)
}
}
Divider(color = Color.LightGray, height = 1.dp)
}
二、数据绑定
在上面的代码中,用了一个循环来代替真实的数据列表,我们也可以很轻松的将真实数据绑定上去。在 JC 中已经提供了简便的数据绑定方案:
@Model
class State(
val list: MutableList = mutableListOf()
)
val state = State(list = mutableListOf(
"a", "b","c","d","e","f","g"
))
看出来了没,只要把数据标识为 @Model
,然后在界面的任意地方使用它,都可以实现数据绑定,当数据被修改时,界面也会自动刷新。
所以现在可以把上面的 ListView 代码改成这样了:
ListView(itemCount = state.list.size) { index ->
FlexRow(crossAxisAlignment = CrossAxisAlignment.Center) {
expanded(1.0f) {
Text("Item ${state.list[index]}")
}
inflexible {
Button("Button ${state.list[index]}",
style = ContainedButtonStyle(),
onClick = { }
)
}
}
Divider(color = Color.LightGray, height = 1.dp)
}
三、GridView
之前就说了没有 ListView,一试之下果然也没有 GridView,查一了圈文档发现只能用 Table
来实现,非常的蛋疼,代码像这样:
Table(columns = 8) {
repeat(8) { i ->
tableRow {
repeat(8) { j ->
// Cell
Text("${i * 8 + j}")
}
}
}
}
可以看到,其中还必须使用 tableRow
来标识出每一行,这个实现与 Android 原生的 GridView 差得太远了!
所以也必须找一个办法来解决之,所幸知道以上代码后,要搞定并不难。
首先我们需要一个将 itemCount
转换为二维数组的方法,即需要知道一个 itemCount
可以被渲染成多少行,以及每一行内有多少列:
fun Int.toGridData(columns: Int = 1) = mutableListOf>().apply {
var count = 0
var sub = mutableListOf()
for (item in 0 until this@toGridData) {
if (count == columns) {
add(sub.toList())
sub = mutableListOf()
sub.add(item)
count = 1
continue
}
sub.add(item)
count++
}
add(sub.toList())
}.toList()
随手写一下就这样吧,最终得到一个二维数组。然后就可以完成 GridView 了:
@Composable
fun GridView(
columns: Int,
itemCount: Int,
alignment: (columnIndex: Int) -> Alignment = { Alignment.TopLeft },
columnWidth: (columnIndex: Int) -> TableColumnWidth = { TableColumnWidth.Flex(1f) },
children: @Composable() (index: Int) -> Unit) {
VerticalScroller {
Table(columns = columns, alignment = alignment, columnWidth = columnWidth) {
val gridIndex = itemCount.toGridData(columns)
gridIndex.forEach { list ->
tableRow {
list.forEach { index ->
children(index)
}
}
}
}
}
}
此时要生成一个 GridView 就变得简单了:
GridView(columns = 3, itemCount = state.list.size) { index ->
Text(text = state.list[index])
}
四、组件位于 Activity 底部
到目前为止,还没有找到如何使组件对于页面做底部对齐的方法,唯有自己计算,计算方法如下:
@Composable
fun sampleUI(ctx: Context?, safeHeight: Dp = Dp(0f)) {
val hhDp = if (safeHeight == Dp(0f)) { ctx?.safeHeightDp() ?: Dp(0f) } else safeHeight
MaterialTheme {
Column {
Container(height = 40.dp, alignment = Alignment.CenterLeft, expanded = true) {
Text(text = "times: ${state.count}")
}
// 这个 Container 撑满剩余空间
Container(height = hhDp - 80.dp, expanded = true) {
}
// 这个 Container 底部对齐
Container(height = 40.dp, alignment = Alignment.BottomCenter, expanded = true) {
Button(text = "Click",
onClick = {
ctx?.toast("${state.itemIndex}", dark = false)
}
)
}
}
}
}
关键来了,这个 safeHeightDp
怎么来的呢?
fun Context.safeHeightDp(): Dp {
val nav = if (hasNavigationBar()) navigationBarHeight() else 0
val space = UI.height - actionBarHeight() - statusBarHeight() - nav
return Dp(space.px2dip())
}
由此我们就可以算出用于撑开空间的 Container 有多高了。
对于这类 Activity,JC 无法在预览时就计算出高度,只能手动传一个,否则高度会变 0,经过实际测试,预览界面的 safeHeight
值为 603.dp
,一个挺奇怪的数字,记住就好了,这样就可以正常的预览界面了:
@Preview
@Composable
fun samplePreview() {
sampleUI(null, safeHeight = 603.dp)
}
简单探了一些,下面该探的可能就是下刷上滑刷新这类的实现了,要实际用于项目还需要很多的储备,慢慢探完吧。本篇先到此结束了 :)