Material Design 2 en Compose

Jetpack Compose ofrece una implementación de Material Design, un sistema de diseño integral para crear interfaces digitales. Los componentes de Material Design (botones, tarjetas, interruptores, etc.) se compilan sobre la aplicación de temas de Material, que es una forma sistemática de personalizar Material Design para que refleje mejor la marca de tu producto. Un tema de Material comprende atributos de color, tipografía y forma. Cuando personalizas esos atributos, tus cambios se reflejan automáticamente en los componentes que usas para compilar tu app.

Jetpack Compose implementa esos conceptos con el elemento MaterialTheme que admite composición:

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

Configura los parámetros que pasas a MaterialTheme para aplicar un tema a tu aplicación.

Dos capturas de pantalla que contrastan. La primera usa el estilo predeterminado de MaterialTheme, mientras que la segunda usa un estilo modificado.

Figura 1: La primera captura de pantalla muestra una app que no configura MaterialTheme y, por lo tanto, usa el estilo predeterminado. La segunda, muestra una app que pasa parámetros a MaterialTheme para personalizar el estilo.

Color

Los colores se modelan en Compose con la clase Color, una clase simple que retiene datos.

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

Si bien puedes organizarlos como te guste (como constantes de nivel superior, dentro de un singleton o intercalado definido), te recomendamos que especifiques los colores en tu tema y los recuperes desde allí. Este enfoque permite admitir, con facilidad, el tema oscuro y los temas anidados.

Ejemplo de paleta de colores de un tema

Figura 2: Sistema de colores de Material.

Compose proporciona la clase Colors para modelar el sistema de colores de Material. Colors ofrece funciones de compilador para crear conjuntos de colores claros u oscuros:

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,
    // ...
)

Una vez que hayas definido tus Colors, puedes pasarlos a un MaterialTheme:

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

Cómo usar colores de tema

Puedes recuperar los Colors proporcionados al elemento que admite compilación de MaterialTheme con MaterialTheme.colors.

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

Color de contenido y superficie

Muchos componentes aceptan un par de color y color del contenido:

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

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

Esto te permite no solo definir el color de un elemento que admite composición, sino también proporcionar un color predeterminado para su contenido, es decir, los elementos que admiten composición que este contiene. Muchos elementos que admiten composición usan este color de contenido de forma predeterminada. Por ejemplo, Text basa su color en el color de contenido del elemento superior, y Icon usa ese color para establecer su tono.

Dos ejemplos del mismo banner, con colores distintos

Figura 3: Cuando se establecen diferentes colores de fondo, se producen distintos colores de texto y de ícono.

El método contentColorFor() recupera el color adecuado para mostrar "arriba" de cualquier color del tema. Por ejemplo, si configuras un color de fondo primaryen Surface, se usará esta función para establecer onPrimary como color de contenido. Si configuras un color de fondo que no pertenece a ningún tema, también debes especificar un color de contenido apropiado. Usa LocalContentColor a fin de recuperar el color de contenido preferido para el fondo actual, en una posición determinada en la jerarquía.

Versión alfa de contenido

Muchas veces quieres variar la forma en que destacas el contenido para comunicar la importancia y ofrecer una jerarquía visual. En las recomendaciones de legibilidad de texto de Material Design, se recomienda usar diferentes niveles de opacidad para transmitir diferentes niveles de importancia.

En Jetpack Compose, esto se implementa mediante LocalContentAlpha. Puedes especificar una versión alfa de contenido para establecer una jerarquía proporcionando un valor para este objeto CompositionLocal. Los elementos anidados que admiten composición pueden usar este valor para aplicar el tratamiento alfa a su contenido. Por ejemplo, Text y Icon utilizan la combinación de LocalContentColor configurada como LocalContentAlpha de forma predeterminada. Material especifica algunos valores alfa estándar (high, medium y disabled), que están modelados por el objeto 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(
        // ...
    )
}

Para obtener más información sobre CompositionLocal, consulta la Guía de datos de permisos local con CompositionLocal.

Captura de pantalla del título de un artículo en el que se muestran diferentes niveles de énfasis

Figura 4: Aplica diferentes niveles de énfasis en el texto para comunicar visualmente la jerarquía de la información. La primera línea de texto es el título, incluye la información más importante y, por lo tanto, usa ContentAlpha.high. La segunda línea incluye metadatos menos importantes y, por lo tanto, usa ContentAlpha.medium.

Tema oscuro

En Compose, debes proporcionar diferentes conjuntos de Colors al elemento MaterialTheme que admite composición para implementar temas claros y oscuros:

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

En este ejemplo, MaterialTheme se une a su propia función que admite composición y que acepta un parámetro que especifica si se debe usar un tema oscuro o no. En este caso, la función consulta a la configuración de tema del dispositivo para obtener el valor predeterminado de darkTheme.

Puedes usar código como el siguiente para verificar si los objetos Colors actuales son claros u oscuros:

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

Superposiciones de elevación

En Material, las superficies en temas oscuros con mayor elevación reciben superposiciones de elevación, lo que aclara su fondo. Cuanto más alta sea la elevación de una superficie (se acerque más a una fuente de luz implícita), más se aclarará la superficie.

A esas superposiciones las aplica, automáticamente, el elemento Surface que admite composición cuando se usan colores oscuros y para cualquier otro elemento de Material que admite composición y que use una superficie:

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

Captura de pantalla de una app que muestra la diferencia sutil de los colores que se usan para los elementos que se encuentran a diferentes niveles de elevación

Figura 5: Las tarjetas y la navegación inferior usan el color surface en el fondo. Como las tarjetas y la navegación inferior se encuentran en diferentes niveles de elevación sobre el fondo, tienen colores un poco diferentes: las tarjetas son más claras que el fondo, y la navegación inferior es más clara que las tarjetas.

Para situaciones personalizadas que no impliquen un elemento Surface, usa LocalElevationOverlay, un objeto CompositionLocal que contiene el elemento ElevationOverlay que usan los componentes 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
)

Para inhabilitar las superposiciones de elevación, brinda null en el punto deseado de una jerarquía que admite composición:

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

Acentuación de color limitada

En la mayoría de los casos, para Material, te recomendamos que apliques acentuación de color limitada para temas oscuros y le des preferencia al color surface por sobre el color primary. Los elementos de Material que admiten composición, como TopAppBar y BottomNavigation, implementan este comportamiento de forma predeterminada.

Figura 6: Tema oscuro de Material con acentuación de color limitada. La barra superior de la app usa el color principal en el tema claro y el color de superficie en el tema oscuro.

Para situaciones personalizadas, usa la propiedad de extensión primarySurface:

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

Tipografía

Material define un sistema de tipos y te recomienda que uses una pequeña cantidad de estilos con nombres semánticos.

Ejemplo de varios tipos de letra en distintos estilos

Figura 7: Sistema de tipos de Material.

Compose implementa el sistema de tipos con Typography, TextStyle y clases relacionadas con la fuente. El constructor Typography ofrece valores predeterminados para cada estilo, de modo que puedes omitir cualquiera que no quieras personalizar:

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, /*...*/) {
    /*...*/
}

Si deseas usar siempre la misma fuente, especifica defaultFontFamily parameter y omite el objeto fontFamily de cualquier elemento TextStyle:

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

Cómo usar estilos de texto

Se puede acceder a los objetos TextStyle a través de MaterialTheme.typography. Recupera los TextStyle de la siguiente manera:

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

Captura de pantalla que muestra distintos tipos de letra combinados para diferentes fines

Figura 8: Usa una selección de tipos de letra y estilos para expresar tu marca.

Forma

Material define un sistema de formas, lo que te permite definir formas para componentes grandes, medianos y pequeños.

Muestra una variedad de formas de Material Design

Figura 9: Sistema de formas de Material.

Compose implementa el sistema de formas con la clase Shapes, que te permite especificar una CornerBasedShape para cada categoría de tamaño:

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, /*...*/) {
    /*...*/
}

Muchos componentes usan estas formas de manera predeterminada. Por ejemplo, Button, TextField y FloatingActionButton, de manera predeterminada, adoptan el valor pequeño, AlertDialog adopta el valor mediano y ModalDrawer el grande. Consulta la referencia del esquema de formas para ver la asignación completa.

Cómo usar formas

Se puede acceder a los objetos Shape a través de MaterialTheme.shapes. Recupera los Shape con un código como este:

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

Captura de pantalla de una app que usa formas de Material para transmitir en qué estado se encuentra un elemento

Figura 10: Usa formas para expresar la marca o el estado.

Estilos predeterminados

En Compose, no existe un concepto equivalente a estilos predeterminados de objetos View de Android. Para brindar una funcionalidad similar, puedes crear tus propias funciones de "sobrecarga" que admitan composición y que unan componentes de Material. Por ejemplo, para crear un estilo de botón, une un botón a tu propia función que admite composición, configura directamente los parámetros que deseas modificar y expón otros como parámetros al elemento que admite composición que esta contiene.

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

Superposiciones de temas

Para alcanzar el equivalente a superposiciones de temas de objetos View de Android en Compose, puedes anidar elementos MaterialTheme que admitan composición. Como MaterialTheme establece, de forma predeterminada, los colores, la tipografía y las formas en el valor de tema actual, si un tema solo establece uno de esos parámetros, los otros mantienen sus valores predeterminados.

Además, cuando migres pantallas basadas en View a Compose, presta atención a los usos del atributo android:theme. Es probable que necesites un nuevo MaterialTheme en esa parte del árbol de IU de Compose.

En este ejemplo, la pantalla de detalles usa un PinkTheme para la mayor parte de la pantalla y, luego, un BlueTheme para la sección relacionada. Consulta la captura de pantalla y el código que aparecen más adelante.

Figura 11: Temas anidados.

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

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

Estados de componentes

Los componentes de Material con los que se puede interactuar (con un clic o un botón de activación, etc.) pueden estar en diferentes estados visuales. Los estados incluyen habilitado, inhabilitado, presionado, etc.

Con frecuencia, los elementos que admiten composición tienen un parámetro enabled. Si se establece en false, se evita la interacción, y se cambian propiedades, como el color y la elevación, para transmitir, de forma visual, el estado del componente.

Figura 12: Botón con enabled = true (izquierda) y enabled = false (derecha)

En la mayoría de los casos, puedes confiar en los parámetros predeterminados para valores como el color y la elevación. Si deseas configurar los valores que se usan en diferentes estados, hay clases y funciones prácticas disponibles. Consulta el siguiente ejemplo de botón:

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
    )
) { /* ... */ }

Figura 13: Botón con enabled = true (izquierda) y enabled = false (derecha), con valores de color y elevación ajustados.

Ecos

Los componentes de Material usan ondas para indicar con qué se está interactuando. Si usas MaterialTheme en la jerarquía, se usará Ripple como un objeto Indication predeterminado dentro de los modificadores, por ejemplo, clickable y indication.

En la mayoría de los casos, puedes confiar en el objeto Ripple predeterminado. Si deseas configurar su apariencia, puedes usar RippleTheme para cambiar propiedades como color y alfa.

Puedes extender RippleTheme y aprovechar las funciones de utilidad defaultRippleColor y defaultRippleAlpha. Luego, puedes brindar el tema de ondas personalizado en la jerarquía mediante 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
    )
}

Texto alternativo

Figura 14: Botones con diferentes valores de ondas que se brindan a través de RippleTheme

Más información

Para obtener más información sobre los temas de Material en Compose, consulta los siguientes recursos adicionales.

Codelabs

Videos