Material Design 2 в Compose

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

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. Цветовая система материала.

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 , предоставленные для компоновки 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),
    // ...
) { /* ... */ }

Это позволяет не только задать цвет компонуемого элемента, но и задать цвет по умолчанию для его содержимого, то есть для всех его компонуемых элементов. Многие компонуемые элементы используют этот цвет по умолчанию. Например, цвет элемента 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 для компонуемого элемента MaterialTheme :

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

В этом примере MaterialTheme обёрнут в собственную компонуемую функцию, которая принимает параметр, указывающий, использовать тёмную тему или нет. В этом случае функция получает значение по умолчанию для 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 , используйте LocalElevationOverlayCompositionLocal , содержащий 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 в выбранной точке в компонуемой иерархии:

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

Пример нескольких различных шрифтов в разных стилях
Рисунок 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.
Рисунок 9. Система формы материала.

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

Тематические наложения

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

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

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

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

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

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

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

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

У компонуемых компонентов часто есть параметр 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 (справа) с настроенными значениями цвета и высоты.

Рябь

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

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

Вы можете расширить RippleTheme и использовать служебные функции defaultRippleColor и defaultRippleAlpha . Затем вы можете добавить свою собственную тему Ripple в свою иерархию с помощью 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. Кнопки с различными значениями пульсации, предоставленные с помощью RippleTheme .

Узнать больше

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

Codelabs

Видео

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}