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) {
... ...
}
}