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

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

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

Компонент 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 в выбранной точке составной иерархии:

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

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

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

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

Для нестандартных сценариев используйте свойство расширения 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.
Рисунок 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 по умолчанию имеют размер small, AlertDialog — medium, а ModalDrawer — large — см. справочник по схемам форм для получения полной информации о соответствии.

Используйте формы

Элементы Shape доступны через MaterialTheme.shapes . Получить элементы Shape можно с помощью следующего кода:

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

Скриншот приложения, использующего фигуры Material Design для отображения состояния элемента.
Рисунок 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 Views, используя вложенные компоненты MaterialTheme . Поскольку MaterialTheme по умолчанию задает цвета, типографику и формы в соответствии со значениями текущей темы, все остальные параметры сохраняют свои значения по умолчанию, если тема задает только один из этих параметров.

Кроме того, при переносе экранов на основе View в 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 (справа), с измененными значениями цвета и высоты.

Рябь

Компоненты 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 Theming в Compose, обратитесь к следующим дополнительным ресурсам.

Кодлабс

Видео

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}