欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

18.2 组成导航抽屉和响应式用户界面导航适配

最编程 2024-05-04 08:09:00
...

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 组件

C0B52380-1F6C-43A1-BB45-1C8D19FB6A27.png

PermanentNavigationDrawer 顾名思义,它 DrawerContent 和 Content 是固定的。

ModalNavigationDrawer 和 DismissibleNavigationDrawer 都可以根据 DrawerState 动画值来设置 Offset 。

区别如上图所示: ModalNavigationDrawer 只改变 DrawerContent 的 Offset,让 DrawerContent 盖在 Content 之上,DismissibleNavigationDrawer 两个一起改变 Offset 一起移动。

Untitled.gif

Untitled.gif

官方代码

@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 中的导航 就是根据当前的屏幕尺寸选择不同的实现,大致就是下面的效果(模拟器切换起来不是那么流畅)

Untitled.gif

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 只是替换了一下依赖。