Material Design 2 в Compose

Jetpack Compose предлагает реализацию Material Design – комплексной системы проектирования для создания цифровых интерфейсов. Компоненты Material Design (кнопки, карточки, переключатели и т. д.) созданы на основе тематического оформления Material Design, которое позволяет систематически настраивать Material Design, чтобы лучше отражать бренд вашего продукта. Тема Material содержит атрибуты цвета, типографики и формы. Когда вы настраиваете эти атрибуты, изменения автоматически отражаются в компонентах, которые вы используете для создания приложения.

В Jetpack Compose эти концепции реализованы с помощью composable-функции MaterialTheme:

MaterialTheme(
    colors = // ...
    typography = // ...
    shapes = // ...
) {
    // app content
}

Настройте параметры, которые вы передаете в MaterialTheme, чтобы задать тему приложения.

Два контрастных скриншота. На первом скриншоте показан стиль MaterialTheme по умолчанию, а на втором – измененный стиль.
Рисунок 1. На первом скриншоте показано приложение, в котором не настроена тема `MaterialTheme`, поэтому в нем используется стиль по умолчанию. На втором скриншоте показано приложение, которое передает параметры в `MaterialTheme` для настройки стиля.

Цвет

Цвета моделируются в Compose с помощью класса Color, который содержит данные.

val Red = Color(0xffff0000)
val Blue = Color(red = 0f, green = 0f, blue = 1f)

Вы можете организовать их как угодно (в виде констант верхнего уровня, в рамках синглтона или встроенных элементов), но мы настоятельно рекомендуем указать цвета в теме и извлекать их оттуда. Такой подход позволяет поддерживать тёмную тему и вложенные темы.

Пример цветовой палитры темы
Рисунок 2. Цветовая система Material.

Compose предоставляет класс Colors для моделирования системы цветов Material. Colors предоставляет функции для создания наборов светлых или темных цветов:

private val Yellow200 = Color(0xffffeb46)
private val Blue200 = Color(0xff91a4fc)
// ...

private val DarkColors = darkColors(
    primary = Yellow200,
    secondary = Blue200,
    // ...
)
private val LightColors = lightColors(
    primary = Yellow500,
    primaryVariant = Yellow400,
    secondary = Blue700,
    // ...
)

Определив Colors, вы можете передать их в MaterialTheme:

MaterialTheme(
    colors = if (darkTheme) DarkColors else LightColors
) {
    // app content
}

Использовать цвета темы

Вы можете получить Colors, предоставленный composable-функции MaterialTheme, с помощью MaterialTheme.colors.

Text(
    text = "Hello theming",
    color = MaterialTheme.colors.primary
)

Цвет поверхности и контента

Многие компоненты принимают пару цветов: цвет фона и цвет контента.

Surface(
    color = MaterialTheme.colors.surface,
    contentColor = contentColorFor(color),
    // ...
) { /* ... */ }

TopAppBar(
    backgroundColor = MaterialTheme.colors.primarySurface,
    contentColor = contentColorFor(backgroundColor),
    // ...
) { /* ... */ }

Это позволяет не только задать цвет composable-функции, но и указать цвет по умолчанию для контента, то есть composable-функций, содержащихся в ней. Многие composable-функции используют этот цвет по умолчанию. Например, цвет Text основан на цвете контента родительского элемента, а Icon использует этот цвет для установки оттенка.

Два примера одного и того же баннера в разных цветах
Рисунок 3. Цвет текста и значков зависит от выбранного цвета фона.

Метод contentColorFor() извлекает подходящий цвет для любого цвета темы. Например, если вы зададите цвет фона primary для Surface, эта функция установит onPrimary в качестве цвета контента. Если вы задали цвет фона, не соответствующий теме, укажите подходящий цвет контента. Используйте LocalContentColor, чтобы получить предпочтительный цвет контента для текущего фона в заданной позиции в иерархии.

Альфа-канал контента

Часто вам нужно варьировать, насколько вы подчеркиваете контент, чтобы сообщить о важности и обеспечить визуальную иерархию. В рекомендациях по удобочитаемости текста в Material Design советуется использовать разные уровни непрозрачности для передачи разных уровней важности.

В Jetpack Compose для этого используется LocalContentAlpha. Вы можете указать альфа-версию контента для иерархии, задав значение для этого CompositionLocal. Вложенные composable-функции могут использовать это значение, чтобы применить альфа-обработку к своему контенту. Например, в Text и Icon по умолчанию используется комбинация LocalContentColor, настроенная для использования LocalContentAlpha. В Material Design есть стандартные значения альфа-канала (high, medium, disabled), которые моделируются объектом ContentAlpha.

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(
        // ...
    )
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(
        // ...
    )
    Text(
        // ...
    )
}

Подробнее о CompositionLocal рассказывается в статье Локальные данные с CompositionLocal.

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

Тёмная тема

В Compose светлая и тёмная темы реализуются путем передачи разных наборов Colors в composable-функцию MaterialTheme:

@Composable
fun MyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
        /*...*/
        content = content
    )
}

В этом примере MaterialTheme заключен в собственную composable-функцию, которая принимает параметр, указывающий, использовать ли тёмную тему. В этом случае функция получает значение по умолчанию для darkTheme, запрашивая настройку темы устройства.

Вы можете использовать следующий код, чтобы проверить, какая тема используется в Colors:

val isLightTheme = MaterialTheme.colors.isLight
Icon(
    painterResource(
        id = if (isLightTheme) {
            R.drawable.ic_sun_24
        } else {
            R.drawable.ic_moon_24
        }
    ),
    contentDescription = "Theme"
)

Наложения высот

В Material Design поверхности в тёмных темах с более высокими уровнями получают наложения уровней, которые осветляют их фон. Чем выше уровень поверхности (чем ближе она к источнику света), тем светлее она становится.

Функция Surface автоматически применяет эти наложения при использовании темных цветов, как и любая другая функция Material, использующая поверхность:

Surface(
    elevation = 2.dp,
    color = MaterialTheme.colors.surface, // color will be adjusted for elevation
    /*...*/
) { /*...*/ }

Скриншот приложения, на котором показаны едва заметные различия в цветах элементов на разных уровнях высоты
Рисунок 5. Карточки и нижняя панель навигации используют цвет surface в качестве фона. Поскольку карточки и нижняя панель навигации находятся на разных уровнях высоты над фоном, они имеют немного разные цвета: карточки светлее фона, а нижняя панель навигации светлее карточек.

Для пользовательских сценариев, не связанных с Surface, используйте LocalElevationOverlay – CompositionLocal, содержащий ElevationOverlay, который используется компонентами Surface:

// Elevation overlays
// Implemented in Surface (and any components that use it)
val color = MaterialTheme.colors.surface
val elevation = 4.dp
val overlaidColor = LocalElevationOverlay.current?.apply(
    color, elevation
)

Чтобы отключить наложение высоты, укажите null в выбранной точке иерархии composable-функций:

MyTheme {
    CompositionLocalProvider(LocalElevationOverlay provides null) {
        // Content without elevation overlays
    }
}

Ограниченное количество цветовых акцентов

В тёмных темах Material Design рекомендуется использовать ограниченное количество цветовых акцентов. В большинстве случаев лучше выбирать цвет surface, а не primary. Такие composable-функции Material, как TopAppBar и BottomNavigation, реализуют это поведение по умолчанию.

Скриншот тёмной темы Material, на котором показана верхняя панель приложения, использующая цвет поверхности вместо основного цвета для ограниченных цветовых акцентов
Рисунок 6. Тёмная тема Material с ограниченным количеством цветовых акцентов. В светлой теме верхняя панель приложения окрашена в основной цвет, а в тёмной теме – в цвет поверхности.

Для особых сценариев используйте свойство расширения primarySurface:

Surface(
    // Switches between primary in light theme and surface in dark theme
    color = MaterialTheme.colors.primarySurface,
    /*...*/
) { /*...*/ }

Параметры текста

В Material Design используется система типов, которая позволяет использовать небольшое количество стилей с семантическими названиями.

Пример нескольких разных гарнитур в разных стилях
Рисунок 7. Система типов Material.

Compose реализует систему типов с помощью классов Typography, TextStyle и классов, связанных со шрифтами. Конструктор Typography предлагает значения по умолчанию для каждого стиля, поэтому вы можете пропустить те, которые не хотите настраивать:

val raleway = FontFamily(
    Font(R.font.raleway_regular),
    Font(R.font.raleway_medium, FontWeight.W500),
    Font(R.font.raleway_semibold, FontWeight.SemiBold)
)

val myTypography = Typography(
    h1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    )
    /*...*/
)
MaterialTheme(typography = myTypography, /*...*/) {
    /*...*/
}

Если вы хотите использовать одну и ту же гарнитуру, укажите параметр defaultFontFamily и опустите fontFamily для всех элементов TextStyle:

val typography = Typography(defaultFontFamily = raleway)
MaterialTheme(typography = typography, /*...*/) {
    /*...*/
}

Как использовать стили текста

Доступ к элементам TextStyle осуществляется с помощью MaterialTheme.typography. Получите элементы TextStyle следующим образом:

Text(
    text = "Subtitle2 styled",
    style = MaterialTheme.typography.subtitle2
)

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

Фигура

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

Различные фигуры в стиле Material Design.
Рисунок 9. Система фигур Material.

В Compose система фигур реализована с помощью класса Shapes, который позволяет указать CornerBasedShape для каждой категории размера:

val shapes = Shapes(
    small = RoundedCornerShape(percent = 50),
    medium = RoundedCornerShape(0f),
    large = CutCornerShape(
        topStart = 16.dp,
        topEnd = 0.dp,
        bottomEnd = 0.dp,
        bottomStart = 16.dp
    )
)

MaterialTheme(shapes = shapes, /*...*/) {
    /*...*/
}

Многие компоненты используют эти формы по умолчанию. Например, для Button, TextField и FloatingActionButton по умолчанию используется малый размер, для AlertDialog – средний, а для ModalDrawer – большой. Полный список сопоставлений можно найти в справочнике по схемам фигур.

Как использовать фигуры

Доступ к элементам Shape осуществляется с помощью MaterialTheme.shapes. Получите элементы Shape с помощью следующего кода:

Surface(
    shape = MaterialTheme.shapes.medium, /*...*/
) {
    /*...*/
}

Скриншот приложения, в котором используются фигуры Material Design, чтобы показать состояние элемента
Рисунок 10. Используйте формы, чтобы выразить бренд или состояние.

Стили по умолчанию

В Compose нет эквивалента стилей по умолчанию из Android Views. Вы можете создать аналогичную функцию, создав собственные composable-функции overload, которые будут включать компоненты Material. Например, чтобы создать стиль кнопки, оберните кнопку в свою собственную composable-функцию, напрямую задав параметры, которые вы хотите или должны изменить, и предоставив другие в качестве параметров для содержащей composable-функции.

@Composable
fun MyButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

Наложения тем

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

Кроме того, при переносе экранов на основе View в Compose обратите внимание на использование атрибута android:theme. Вероятно, вам нужен новый MaterialTheme в этой части дерева Compose UI.

В этом примере на экране сведений используется PinkTheme для большей части экрана, а затем BlueTheme для связанного раздела. Это показано на скриншоте и в коде ниже.

Скриншот приложения, демонстрирующий вложенные темы: розовая тема для главного экрана и синяя тема для связанного раздела.
Рисунок 11. Вложенные темы.

@Composable
fun DetailsScreen(/* ... */) {
    PinkTheme {
        // other content
        RelatedSection()
    }
}

@Composable
fun RelatedSection(/* ... */) {
    BlueTheme {
        // content
    }
}

Состояния компонентов

Компоненты Material, с которыми можно взаимодействовать (нажимать, переключать и т. д.), могут иметь разные визуальные состояния. Состояния включают включено, отключено, нажато и т. д.

У composable-функций часто есть параметр enabled. Если задать для него значение false, взаимодействие будет невозможно, а свойства, такие как цвет и высота, изменятся, чтобы визуально передать состояние компонента.

Скриншот двух кнопок: одна включена, другая отключена. Показаны их разные визуальные состояния.
Рисунок 12. Кнопка с enabled = true (слева) и enabled = false (справа).

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

Button(
    onClick = { /* ... */ },
    enabled = true,
    // Custom colors for different states
    colors = ButtonDefaults.buttonColors(
        backgroundColor = MaterialTheme.colors.secondary,
        disabledBackgroundColor = MaterialTheme.colors.onBackground
            .copy(alpha = 0.2f)
            .compositeOver(MaterialTheme.colors.background)
        // Also contentColor and disabledContentColor
    ),
    // Custom elevation for different states
    elevation = ButtonDefaults.elevation(
        defaultElevation = 8.dp,
        disabledElevation = 2.dp,
        // Also pressedElevation
    )
) { /* ... */ }

Скриншот двух кнопок с настроенными цветом и высотой для включенного и отключенного состояний
Рисунок 13. Кнопка с enabled = true (слева) и enabled = false (справа) со скорректированными значениями цвета и высоты.

Рябь

Компоненты Material Design используют рябь, чтобы показать, что с ними взаимодействуют. Если вы используете MaterialTheme в иерархии, Ripple используется как Indication по умолчанию внутри модификаторов, таких как clickable и indication.

В большинстве случаев можно использовать значение по умолчанию – Ripple. Если вам нужно настроить их внешний вид, вы можете использовать RippleTheme, чтобы изменить такие свойства, как цвет и альфа-канал.

Вы можете расширить RippleTheme и использовать служебные функции defaultRippleColor и defaultRippleAlpha. Затем вы можете указать свою тему пульсации в иерархии с помощью LocalRippleTheme:

@Composable
fun MyApp() {
    MaterialTheme {
        CompositionLocalProvider(
            LocalRippleTheme provides SecondaryRippleTheme
        ) {
            // App content
        }
    }
}

@Immutable
private object SecondaryRippleTheme : RippleTheme {
    @Composable
    override fun defaultColor() = RippleTheme.defaultRippleColor(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )

    @Composable
    override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )
}

Анимированное GIF-изображение, на котором показаны кнопки с разными эффектами ряби при нажатии
Рисунок 14. Кнопки с разными значениями пульсации, заданными с помощью RippleConfiguration.

Подробнее…

Чтобы узнать больше о тематическом оформлении Material Design в Compose, ознакомьтесь со следующими дополнительными ресурсами.

Практические работы

Видео