18.2 组成导航抽屉和响应式用户界面导航适配
Snackbar 弹出效果我们已经知道了,就是动画 + 布局位置组合产生的弹窗效果,State 控制显示/消失,这些东西NavigationDrawer 跟 Snackbar 一样。
M3 中名字中有 Drawer 的组件有七个:PermanentNavigationDrawer、ModalNavigationDrawer、DismissibleNavigationDrawer、PermanentDrawerSheet、ModalDrawerSheet、DismissibleDrawerSheet、NavigationDrawerItem 。
我们先来归个类
- 容器:PermanentNavigationDrawer、ModalNavigationDrawer、DismissibleNavigationDrawer,包含 DrawerContent 和 Content 两部分
- DrawerContent : PermanentDrawerSheet、ModalDrawerSheet、DismissibleDrawerSheet,M3提供 DrawerContent 都是有 DrawerSheet 实现只是配色方案不同的 Column 布局
- Item : NavigationDrawerItem M3提供 DrawerContent 的Item 组件
PermanentNavigationDrawer 顾名思义,它 DrawerContent 和 Content 是固定的。
ModalNavigationDrawer 和 DismissibleNavigationDrawer 都可以根据 DrawerState 动画值来设置 Offset 。
区别如上图所示: ModalNavigationDrawer 只改变 DrawerContent 的 Offset,让 DrawerContent 盖在 Content 之上,DismissibleNavigationDrawer 两个一起改变 Offset 一起移动。
官方代码
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DrawerDemo() {
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
// icons to mimic drawer destinations
val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email)
val selectedItem = remember { mutableStateOf(items[0]) }
BackHandler(enabled = drawerState.isOpen) {
scope.launch { drawerState.close() }
}
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet()
{
Spacer(Modifier.height(12.dp))
items.forEach { item ->
NavigationDrawerItem(
icon = { Icon(item, contentDescription = null) },
label = { Text(item.name) },
selected = item == selectedItem.value,
onClick = {
scope.launch { drawerState.close() }
selectedItem.value = item
},
modifier = Modifier.padding(horizontal = 12.dp)
)
}
}
}
) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = if (drawerState.isClosed) ">>> Swipe >>>" else "<<< Swipe <<<")
Spacer(Modifier.height(20.dp))
Button(onClick = { scope.launch { drawerState.open() } }) {
Text("Click to open")
}
}
}
}
不同的 DrawerSheet 有兴趣的可以去看下源码就是配色方案形状不同,DrawerContent 中可以放任意内容 DrawerSheet 和 DrawerItem 只是为了方便开发提供给开发者使用的普通 Compose 组件,不是一定要使用。
PermanentNavigationDrawer 它是固定的,所以会占用屏幕空间。一般都会用在大尺寸屏幕中的侧边栏。还有一个场景适配就是响应式 UI ,比如说折叠屏。
我们前面在头条屏幕适配方案里有提到过屏幕尺寸分类, M3 中也添加了 window-size 库
implementation "androidx.compose.material3:material3-window-size-class:1.0.1"
官方文档中 响应式 UI 中的导航 就是根据当前的屏幕尺寸选择不同的实现,大致就是下面的效果(模拟器切换起来不是那么流畅)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WanAndroidTheme {
App()
}
}
}
}
@Composable
fun App() {
NavigationDrawer {
NavContent(selectedIndex = it)
}
}
val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NavigationDrawer(
content: @Composable (Int) -> Unit
) {
val windowWidthSizeClass =
LocalAutoWindowInfo.current.windowSizeClass.widthSizeClass
val selectedState = rememberSaveable { mutableStateOf(0) }
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
val drawerState = rememberDrawerState(DrawerValue.Closed)
ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
DrawerContent(
drawerState = drawerState,
selectedState = selectedState,
windowWidthSizeClass = windowWidthSizeClass
)
}) { content(selectedState.value) }
} else {
PermanentNavigationDrawer(drawerContent = {
DrawerContent(
selectedState = selectedState,
windowWidthSizeClass = windowWidthSizeClass
)
}) { content(selectedState.value) }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DrawerContent(
drawerState: DrawerState? = null,
selectedState: MutableState<Int>,
windowWidthSizeClass: WindowWidthSizeClass
) {
val scope = rememberCoroutineScope()
val isMedium = windowWidthSizeClass == WindowWidthSizeClass.Medium
val isCompat = windowWidthSizeClass == WindowWidthSizeClass.Compact
val sheetWidth = when{
isCompat -> 300.dp
isMedium -> 100.dp
else -> 220.dp
}
ModalDrawerSheet(
drawerShape = if (isCompat) CutCornerShape(topEnd = 16.dp, bottomEnd = 16.dp) else RectangleShape,
modifier = Modifier.width(sheetWidth)
) {
items.forEachIndexed { index, item ->
NavigationDrawerItem(
icon = { Icon(item, contentDescription = null) },
label = {
if (isMedium){ } else Text(item.name)
},
selected = index == selectedState.value,
onClick = {
scope.launch { drawerState?.close() }
selectedState.value = index
},
modifier = Modifier.padding(horizontal = 12.dp)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NavContent(selectedIndex: Int) {
Scaffold() {
Box(modifier = Modifier.fillMaxSize().padding(it).background(Color.Cyan)) {
Text(text = "$selectedIndex", modifier = Modifier.align(Alignment.Center))
}
}
}
屏幕尺寸相关的内容直接使用了我们以前实现的 WanAndroidTheme 只是替换了一下依赖。
上一篇: 也许在另一种生活中,猫是我的爱人。
下一篇: 关联预加载(关联模型 6)--延迟预加载