Как реализовать шаблон BottomAppBar и BottomDrawer с помощью Android Jetpack Compose?

Я создаю приложение для Android с помощью Jetpack Compose. Застрял при попытке реализовать BottomAppBar с шаблоном BottomDrawer.

Нижние панели навигации - это модальные панели, которые прикреплены к нижней части экрана, а не к левому или правому краю. Они используются только с нижними панелями приложений. Эти ящики открываются при нажатии значка меню навигации на нижней панели приложения.

Описание на material.io и прямая ссылка на видео .

Я пробовал использовать Scaffold, но он поддерживает только боковой ящик. BottomDrawer, добавленный к содержимому Scaffold, отображается в области содержимого, а BottomDrawer не закрывает BottomAppBar при открытии. Перемещение BottomDrawer после функции Scaffold тоже не помогает: BottomAppBar покрыт каким-то невидимым блоком и не позволяет нажимать кнопки.

Я также пробовал использовать BottomSheetScaffold, но у него нет слота BottomAppBar.

Если Scaffold не поддерживает этот шаблон, как правильно его реализовать? Можно ли расширить компонент Scaffold? Боюсь, что неправильная реализация с нуля может создать проблемы позже, когда я попытаюсь реализовать навигацию и закусочную.

3 ответа

Они (разработчики Google) приглашают вас на путь создания макетов Jetpack, чтобы попробовать добавить другие компоненты материального дизайна, такие как BottomNavigationили в их соответствующие слоты , и все же не дают вам решения.

имеет собственный слот в (т. е. bottomBar), но нет - и, похоже, он разработан исключительно для использования с BottomAppBar явно (см. документацию API для BottomDrawer).

На этом этапе пути Jetpack Compose мы рассмотрели подъем состояния , слоты , модификаторы и были подробно объяснены, что время от времени нам придется экспериментировать, чтобы увидеть, как лучше всего складывать и организовывать Composables - что они почти всегда иметь естественно выразимый способ, которым они работают лучше всего, что практически задумано.

Позвольте мне настроить нас так, чтобы мы были на одной странице:

      class MainActivity : ComponentActivity() {
    @ExperimentalMaterialApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LayoutsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    LayoutsCodelab()
                }
            }
        }
    }
}

Это основное действие, вызывающее наш основной / основной Composable. Это точно так же, как в кодовой лаборатории, за исключением @ExperimentalMaterialApi аннотация.

Далее идет наш основной / основной Composable:

      @ExperimentalMaterialApi
@Composable
fun LayoutsCodelab() {
    val ( gesturesEnabled, toggleGesturesEnabled ) = remember { mutableStateOf( true ) }
    val scope = rememberCoroutineScope()
    val drawerState = rememberBottomDrawerState( BottomDrawerValue.Closed )

    // BottomDrawer has to be the true core of our layout
    BottomDrawer(
        gesturesEnabled = gesturesEnabled,
        drawerState = drawerState,
        drawerContent = {
            Button(
                modifier = Modifier.align( Alignment.CenterHorizontally ).padding( top = 16.dp ),
                onClick = { scope.launch { drawerState.close() } },
                content = { Text( "Close Drawer" ) }
            )
            LazyColumn {
                items( 25 ) {
                    ListItem(
                        text = { Text( "Item $it" ) },
                        icon = {
                            Icon(
                                Icons.Default.Favorite,
                                contentDescription = "Localized description"
                            )
                        }
                    )
                }
            }
        },
        // The API describes this member as "the content of the
        // rest of the UI"
        content = {
            // So let's place the Scaffold here
            Scaffold(
                topBar = {
                    AppBarContent()
                },
                //drawerContent = { BottomBar() }  // <-- Will implement a side drawer
                bottomBar = {
                    BottomBarContent(
                        coroutineScope = scope,
                        drawerState = drawerState
                    )
                            },
            ) {
                innerPadding ->
                BodyContent( Modifier.padding( innerPadding ).fillMaxHeight() )
            }
        }
    )
}

Здесь мы использовали точно так, как нам следует из codelab в пути compose. Обратите внимание на мой комментарий, который является автоматической реализацией бокового ящика. Это довольно изящный способ обойтись напрямую, используя [соответствующие] Composable (ы) (модальный ящик / лист материального дизайна)! Однако для нашего. Я думаю, что API является экспериментальным , потому что они будут вносить изменения, чтобы добавить его поддержку в Composables, как в будущем.

Я основываю это на том, насколько сложно использовать, предназначенный для использования исключительно с, с -, который явно содержит слот для.

Поддерживать BottomDrawer, мы должны понимать, что это базовый контроллер макета, который обертывает весь пользовательский интерфейс приложения, предотвращая взаимодействие с чем-либо, кроме его drawerContentкогда ящик открыт. Для этого необходимо, чтобы он включал Scaffold, а это требует, чтобы мы делегировали необходимый государственный контроль - BottomBarContent составной, который обертывает наши BottomAppBar реализация:

      @ExperimentalMaterialApi
@Composable
fun BottomBarContent( modifier: Modifier = Modifier, coroutineScope: CoroutineScope, drawerState: BottomDrawerState   ) {
    BottomAppBar{
        // Leading icons should typically have a high content alpha
        CompositionLocalProvider( LocalContentAlpha provides ContentAlpha.high ) {
            IconButton(
                onClick = {
                    coroutineScope.launch { drawerState.open() }
                }
            ) {
                Icon( Icons.Filled.Menu, contentDescription = "Localized description" )
            }
        }
        // The actions should be at the end of the BottomAppBar. They use the default medium
        // content alpha provided by BottomAppBar
        Spacer( Modifier.weight( 1f, true ) )
        IconButton( onClick = { /* doSomething() */ } ) {
            Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
        }
        IconButton( onClick = { /* doSomething() */ } ) {
            Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
        }
    }
}

Результат показывает нам:

  • TopAppBar на вершине ,
  • BottomAppBar в нижней части ,
  • Щелчок по значку меню в BottomAppBar открывает наш BottomDrawer , соответствующим образом закрывая BottomAppBar и все пространство содержимого, когда оно открыто.
  • BottomDrawer правильно скрыта, пока либо выше ссылки нажатие кнопки - или жест - не используется , чтобы открыть нижний ящик.
  • Значок меню в BottomAppBar частично открывает ящик.
  • Жест открывает нижний ящик наполовину быстрым коротким движением, но если вы направите его в противном случае.

Я думаю, что в последней версии scaffold есть параметр нижней панели приложения

Возможно, вам придется сделать что-то, как показано ниже. Обратите внимание, как Scaffold вызывается внутри BottomDrawer(). Это сбивает с толку, как в документации сказано: «Они (BottomDrawer) используются только с нижними панелями приложений». Это заставило меня подумать, что мне нужно искать слот BottomDrawer() внутри Scaffold или что я должен вызвать BottomDrawer() внутри BottomAppBar(). В обоих случаях у меня было странное поведение. Вот как я решил проблему. Я надеюсь, что это поможет кому-то, особенно если вы пытаетесь выполнить лабораторное упражнение в модуле 5 макетов в Jetpack Compose из курса Jetpack Compose.

      @ExperimentalMaterialApi
    @Composable
    fun MyApp() {
           var selectedItem by rememberSaveable { mutableStateOf(1)}

    BottomDrawer(
        modifier = Modifier.background(MaterialTheme.colors.onPrimary),
        drawerShape = Shapes.medium,
        drawerContent = {
            Column(Modifier.fillMaxWidth()) {
        for(i in 1..6) {
            when (i) {
                1 -> Row(modifier = Modifier.clickable {  }.padding(16.dp)){
                    Icon(imageVector = Icons.Rounded.Inbox, contentDescription = null)
                    Text(text = "Inbox")
                }
                2 -> Row(modifier = Modifier.clickable {  }.padding(16.dp)){
                    Icon(imageVector = Icons.Rounded.Outbox, contentDescription = null)
                    Text(text = "Outbox")
                }
                3 -> Row(modifier = Modifier.clickable {  }.padding(16.dp)){
                    Icon(imageVector = Icons.Rounded.Archive, contentDescription = null)
                    Text(text = "Archive")
                }
            }
        }
    }
        },
        gesturesEnabled = true
    ) {
        Scaffold(
            topBar = {
                TopAppBar(
                    title = {
                        Text(text = "Learning Compose Layouts" )
                    },
                    actions = {
                        IconButton(onClick = { /*TODO*/ }) {
                            Icon(Icons.Filled.Favorite, contentDescription = null)
                        }
                    }
                )
            },
            bottomBar = { BottomAppBar(cutoutShape = CircleShape, contentPadding = PaddingValues(0.dp)) {
                for (item in 1..4) {
                    BottomNavigationItem(
                        modifier = Modifier.clipToBounds(),
                        selected = selectedItem == item ,
                        onClick = { selectedItem = item },
                        icon = {
                            when (item) {
                                1 -> { Icon(Icons.Rounded.MusicNote, contentDescription = null) }
                                2 -> { Icon(Icons.Rounded.BookmarkAdd, contentDescription = null) }
                                3 -> { Icon(Icons.Rounded.SportsBasketball, contentDescription = null) }
                                4 -> { Icon(Icons.Rounded.ShoppingCart, contentDescription = null) }
                            }
                        }
                    )
                }
            } 
         }
      ) { innerPadding -> BodyContent(
                    Modifier
                    .padding(innerPadding)
                    .padding(8.dp))
        }
    }
}
Другие вопросы по тегам