В Android экран обычно состоит как из содержимого приложения, так и из компонентов, нарисованных системой, включая верхнюю строку состояния и нижнюю навигационную строку. По умолчанию эти панели полностью управляются и отображаются системой, которая автоматически устанавливает их фон в соответствии с текущей темой, оставляя оставшуюся область для приложения, чтобы нарисовать его содержимое.
Во многих случаях мы хотим, чтобы область отрисовки приложения расширялась до системного интерфейса или динамически устанавливала их цвета для достижения лучшего пользовательского опыта.
Много лет назад для реализации иммерсивной строки состояния требовалось установить кучу флагов, XML-конфигураций и т д. Однако времена изменились, и теперь AndroidX предоставляет более удобные инструменты. В этой статье мы расскажем, как добиться эффекта погружения с помощью Jetpack Compose.
Чтобы реализовать иммерсивный пользовательский интерфейс, нам нужно выполнить два шага:
- Расширить область рисования приложения за счет части системного UI.
- Установить цвет фона системного пользовательского интерфейса и полей страницы по мере необходимости.
enableEdgeToEdge()
По умолчанию фон области системного пользовательского интерфейса рисуется системой с помощью цветных блоков, как показано на рисунке ниже.
Чтобы решить эту проблему, необходимо вызвать функцию enableEdgeToEdge()
в Activity.onCreate. Эта функция расширяет макет приложения на System UI, позволяя нам управлять фоном отрисовки системного UI. В большинстве случаев мы предпочитаем сплошной цветной блок, но иногда мы можем захотеть, чтобы он был прозрачным.
В этот момент страница будет выглядеть так, как показано на рисунке ниже, демонстрируя, что область рисования приложения расширилась на часть System UI.
После этого нам нужно решить еще одну проблему — поля страницы. На данном этапе содержимое приложения может быть заслонено системным пользовательским интерфейсом, поэтому нам необходимо добавить соответствующие поля.
WindowInsets
WindowInsets используется для определения положения и размера системного интерфейса.
@Stable interface WindowInsets { /** * The space, in pixels, at the left of the window that the inset represents. */ fun getLeft(density: Density, layoutDirection: LayoutDirection): Int /** * The space, in pixels, at the top of the window that the inset represents. */ fun getTop(density: Density): Int /** * The space, in pixels, at the right of the window that the inset represents. */ fun getRight(density: Density, layoutDirection: LayoutDirection): Int /** * The space, in pixels, at the bottom of the window that the inset represents. */ fun getBottom(density: Density): Int companion object }
Мы устанавливаем поля страницы, получая объект WindowInsets
, соответствующий системному пользовательскому интерфейсу.
WindowInsets
содержит различные типы, каждый из которых соответствует различным типам системного интерфейса.
// The insets describing the status bars. These are the top system UI bars containing notification icons and other indicators. WindowInsets.statusBars // The status bar insets for when they are visible. If the status bars are currently hidden (due to entering immersive full screen mode), then the main status bar insets will be empty, but these insets will be non-empty. WindowInsets.statusBarsIgnoringVisibility // The insets describing the navigation bars. These are the system UI bars on the left, right, or bottom side of the device, describing the taskbar or navigation icons. These can change at runtime based on the user's preferred navigation method and interacting with the taskbar. WindowInsets.navigationBars // The navigation bar insets for when they are visible. If the navigation bars are currently hidden (due to entering immersive full screen mode), then the main navigation bar insets will be empty, but these insets will be non-empty. WindowInsets.navigationBarsIgnoringVisibility // ... and more ...
Отступ от страницы
В приведенном выше примере наша страница в настоящее время расширяется на System UI, в результате чего часть содержимого оказывается скрытой.
Теперь мы можем получить отступ (Padding) через WindowInsets
и применить его.
val density = LocalDensity.current val statusBarHeight = WindowInsets.statusBars.getTop(density).pxToDp(density) val navigatorBarHeight = WindowInsets.navigationBars.getBottom(density).pxToDp(density) Box( modifier = Modifier .fillMaxSize() .padding(top = statusBarHeight, bottom = navigatorBarHeight), )
Экран теперь выглядит так:
Не правда ли, теперь намного лучше?
Адаптивный Padding в Composable
Однако настраивать каждую страницу подобным образом кажется обременительным, не так ли? К счастью, Compose предоставляет множество удобных инструментов для помощи разработчикам в настройке отступов.
Например, в приведенном выше примере вы можете сделать следующее:
Box( modifier = Modifier .fillMaxSize() .statusBarsPadding() .navigationBarsPadding(), ) // or Modifier.systemBarsPadding() // or Modifier.safeDrawingPadding()
Приведенный выше код в конечном итоге предотвращает наложение содержимого путем добавления отступов.
Но это все равно немного громоздко. Должны ли мы писать так для каждой страницы? Конечно, нет. Scaffold
от Compose также помогает нам решить эту проблему.
При использовании Scaffold
из Material Design 3 правильное использование innerPadding
помогает нам автоматически добавлять поля страницы.
Scaffold { innerPadding -> Box( modifier = Modifier .fillMaxSize() .padding(innerPadding), ) { // ... } }
Здесь innerPadding
уже включает в себя отступы для строки состояния и навигационной панели, поэтому вы можете применить его напрямую.
Кроме того, поведение Scaffold
можно изменить. Если нашей странице не нужно добавлять поля (например, просмотрщику изображений или видеоплееру), лучше не использовать innerPadding
из Scaffold
, а управлять им через параметры.
@Composable fun Scaffold( modifier: Modifier = Modifier, topBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, containerColor: Color = MaterialTheme.colorScheme.background, contentColor: Color = contentColorFor(containerColor), contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, content: @Composable (PaddingValues) -> Unit )
Как видите, Scaffold
включает в себя параметр contentWindowInsets
, который по умолчанию добавляет WindowInsets
системного пользовательского интерфейса. Если вы хотите изменить это поведение, вы можете задать пустой WindowInsets
.
TopAppBar
При использовании Scaffold
адаптивные поля получаются путем применения innerPadding
. Однако topBar
из Scaffold
не имеет innerPadding
, но нам все равно не нужно вручную задавать его отступы. Это происходит потому, что специальные компоненты, такие как TopAppBar
, также автоматически устанавливают отступы.
@Composable fun TopAppBar( title: @Composable () -> Unit, modifier: Modifier = Modifier, navigationIcon: @Composable () -> Unit = {}, actions: @Composable RowScope.() -> Unit = {}, windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), scrollBehavior: TopAppBarScrollBehavior? = null )
Параметр windowInsets
в вышеуказанных параметрах используется для установки верхнего отступа. Мы также можем изменить это поведение с помощью параметров.
NavigationBar
NavigationBar
, расположенный в нижней части страницы, также поддерживает адаптивное размещение страницы.
fun NavigationBar( modifier: Modifier = Modifier, containerColor: Color = NavigationBarDefaults.containerColor, contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), tonalElevation: Dp = NavigationBarDefaults.Elevation, windowInsets: WindowInsets = NavigationBarDefaults.windowInsets, content: @Composable RowScope.() -> Unit )
Фоновые цвета StatusBar и NavigationBar
На самом деле, после выполнения вышеуказанных действий цвет фона легко установить, потому что все это является частью содержимого страницы. В конце концов, это ваш код, поэтому вы можете управлять им как угодно.
В Compose этот цвет тоже задается автоматически. Если наша страница использует Scaffold
, то у самого Scaffold
есть цвет фона, который является backgroundColor
из Material Design.
@Composable fun Scaffold( modifier: Modifier = Modifier, topBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, containerColor: Color = MaterialTheme.colorScheme.background, contentColor: Color = contentColorFor(containerColor), contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, content: @Composable (PaddingValues) -> Unit )
containerColor
в приведенном выше коде — это цвет фона страницы. А поскольку Scaffold
по умолчанию добавляет отступы сверху и снизу, цвет фона StatusBar/NavigationBar естественным образом совпадает с цветом фона страницы.
Если вы используете TopAppBar
, он также автоматически установит цвет, и он будет меняться в зависимости от состояния прокрутки. Так что если на странице есть верхний бар, лучше использовать официальный, потому что он выглядит намного красивее, а также избавляет нас от множества проблем.
Заключение
В общем, настраивать иммерсивную строку состояния в соответствии с последними спецификациями будет очень удобно, а многие компоненты в Compose помогут нам сэкономить много работы. Даже если вы столкнетесь с какими-то особыми случаями, есть лаконичные способы их решения.
Справочная документация: https://developer.android.com/develop/ui/views/layout/edge-to-edge