Material Design 2 в Compose

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

В Jetpack Compose эти концепции реализованы с помощью функции 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-функций, которые в ней содержатся. Многие компоненты по умолчанию используют этот цвет. Например, цвет элемента Text определяется цветом контента родительского элемента, а элемент Icon использует этот цвет для настройки оттенка.

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

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

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

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

В Jetpack Compose это реализовано с помощью LocalContentAlpha. Вы можете задать альфа-канал контента для иерархии, указав значение для CompositionLocal. Вложенные композиции могут использовать это значение, чтобы применить к своему контенту альфа-канал. Например, Text и Icon по умолчанию используют сочетание LocalContentColor, скорректированное для использования LocalContentAlpha. В атрибуте "материал" [material] задаются стандартные альфа-значения (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 элементы интерфейса в темных темах с более высоким уровнем получают наложения высоты, которые осветляют их фон. Чем выше поверхность (чем ближе она к воображаемому источнику света), тем светлее она становится.

Компонуемый элемент 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 рекомендуется использовать ограниченное количество цветовых акцентов для темных тем, отдавая предпочтение цвету surface, а не primary в большинстве случаев. Компоненты 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. Система типов материалов.

В 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 Design определена система форм, позволяющая задавать формы для больших, средних и маленьких компонентов.

Показывает различные фигуры в стиле 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 используются для обозначения состояния элемента
Рисунок 10. Используйте фигуры, чтобы выразить особенности бренда или штата.

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

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

@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
    )
}

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

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

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

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

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

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

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

Статусы компонентов

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

У многих функций, которые можно компоновать, есть параметр 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, ознакомьтесь с дополнительными ресурсами.

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

Видео