Site icon AppTractor

Jetpack Compose: совместимость с System UI и иммерсивная строка состояния

В Android экран обычно состоит как из содержимого приложения, так и из компонентов, нарисованных системой, включая верхнюю строку состояния и нижнюю навигационную строку. По умолчанию эти панели полностью управляются и отображаются системой, которая автоматически устанавливает их фон в соответствии с текущей темой, оставляя оставшуюся область для приложения, чтобы нарисовать его содержимое.

Во многих случаях мы хотим, чтобы область отрисовки приложения расширялась до системного интерфейса или динамически устанавливала их цвета для достижения лучшего пользовательского опыта.

Много лет назад для реализации иммерсивной строки состояния требовалось установить кучу флагов, XML-конфигураций и т д. Однако времена изменились, и теперь AndroidX предоставляет более удобные инструменты. В этой статье мы расскажем, как добиться эффекта погружения с помощью Jetpack Compose.

Чтобы реализовать иммерсивный пользовательский интерфейс, нам нужно выполнить два шага:

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

Источник

Exit mobile version