[KtJC] Drawer 和 Tabber

Posted by rarnu on 12-19,2019

Drawer

在 JC 里要实现侧滑抽屉(Drawer)是比较方便的,代码非常直观,其函数定义为:

fun ModalDrawerLayout(
    drawerState: DrawerState,                // 指出当前抽屉的状态
    onStateChange: (DrawerState) -> Unit,    // 改变抽屉状态的函数
    gesturesEnabled: Boolean = true,         // 是否允许手势
    drawerContent: @Composable() () -> Unit, // 抽屉的布局
    bodyContent: @Composable() () -> Unit    // 主界面的布局
)

所以我们可以在代码中直接写:

enum class ScreenType { first, second, third }

@Composable
fun mainUI(ctx: Context?, name: String, color: Color = Color.Black) {
    val (drawerState, onDrawerStateChange) = +state { DrawerState.Closed }
    var uiState by +state { ScreenType.first }
    MaterialTheme {
        ModalDrawerLayout(
            drawerState = drawerState,
            onStateChange = onDrawerStateChange,
            gesturesEnabled = true,
            drawerContent = {
                Column(modifier = Expanded) {
                    Row(modifier = Spacing(16.dp)) {
                        VectorImage(id = R.drawable.ic_launcher_foreground)
                    }
                    Divider(color = Color.LightGray)
                    Button(text = "main", onClick = {
                        uiState = ScreenType.first
                        onDrawerStateChange(DrawerState.Closed)
                    })
                    HeightSpacer(height = 4.dp)
                    Button(text = "second", onClick = {
                        uiState = ScreenType.second
                        onDrawerStateChange(DrawerState.Closed)
                    })
                    HeightSpacer(height = 4.dp)
                    Button(text = "third", onClick = {
                        uiState = ScreenType.third
                        onDrawerStateChange(DrawerState.Closed)
                    })
                }
            }
        ) {
            MainContent(uiState) {
                onDrawerStateChange(DrawerState.Opened)
            }
        }
    }
}

@Composable
fun MainContent(uiState: ScreenType, onOpen: () -> Unit) {
    when(uiState) {
        ScreenType.first -> ui1(onOpen = onOpen)
        ScreenType.second -> ui2(onOpen = onOpen)
        ScreenType.third -> ui3(onOpen = onOpen)
    }
}

这里定义了在抽屉内有三个按钮,并且点击后更换主界面布局,这里有一个同样关于 State 的小技巧。

在上一篇里已经介绍过数据绑定或者说状态保存的内容,使用 @Model 注解来标记即可,但是这需要有一个类型来承载,这里有一个更方便的方法,即是把变量委托给 state 对象,这样当变量发生改变时,也会自动刷新界面。此处的 uiState 也因此变成了一个可以用于刷新界面的成员。

这里有一个小概念可能比较绕,简单说一下,这里有两行代码:

val (drawerState, onDrawerStateChange) = +state { DrawerState.Closed }
var uiState by +state { ScreenType.first }

虽然第一句也使用了 state 方法,但是只是从中获取了两个 component,但是并没委托读写,换言之,就是一次性读取并且使用,这个时候对于 component 的改变并不会引起界面的刷新。

而第二句使用了委托,即是把 uiState 变量的读写交给了 state,此时针对写入的操作将引起界面刷新。

Tabber

Tabber 即是常说的页签,之前通常用 Fragment + ViewPager 实现,但是在 JC 里显然不那么麻烦,直接上代码了:

enum class Sections(val title: String) {
    Sec1("Section 1"),
    Sec2("Section 2"),
    Sec3("Section 3")
}

@Composable
fun tabSample(onOpen: () -> Unit) {
    var section by +state { Sections.Sec1 }
    val sectionTitles = Sections.values().map { it.title }
    Column {
        TopAppBar(title = {
            Text(text = "JPSample")
        }) {
            VectorImageButton(id = R.drawable.ic_launcher_foreground) {
                onOpen()
            }
        }
        TabRow(items = sectionTitles, selectedIndex = section.ordinal) { index, text ->
            Tab(text = text,selected = section.ordinal == index) {
                section = Sections.values()[index]
            }
        }
        Container(modifier = Flexible(1f), alignment = Alignment.Center, expanded = true) {
                when(section) {
                    Sections.Sec1 -> Text(text = "Tab 1")
                    Sections.Sec2 -> Text(text = "Tab 2")
                    Sections.Sec3 -> Text(text = "Tab 3")
                }
        }
    }
}

此处的代码是与上面的 Drawer 联合使用的,单独使用则不需要传入 onOpen 参数。

Container 函数内加入 modifier = Flexible(1f),可以实现将该 Container 按高度填充的效果,所以在其间添加 Text 可以被 alignment 影响到从而居中显示。

问题

目前 Drawer 还存在的问题是,不能对弹出层的宽度作出调整,它将永远占据屏幕80%左右的宽度,另外,滑动出 Drawer 的范围比较偏向屏幕中间,在屏幕边缘滑动将触发 Back 键(或许这是故意设计以应对全面屏的)。

Tabber 存在的问题是,无法通过手势来滑动切换,只能点击切换,并且在 Tab 较多且设为 Tab 可滚动时,会产生一定的卡顿,当然目前作为玩具来说,它已经较为完善了。

补充

在上一篇文章中,讲到了吸底的 Container 的制作,采用了手动计算的方法来确定用于填充的 Container 的高度。而上面也提到了使用 modifier = Flexible(1f) 可以实现按高度填充,因此不再需要手动计算了,可以将代码修改一下以简单适应:

Column {
    TopAppBar(title = { Text(text = "Sample")})
    Container(
                modifier = Flexible(1f),
                expanded = true) {
                ... ...
    }
    Container(height = 40.dp, alignment = Alignment.BottomCenter, expanded = true) {
                ... ...
    }
}