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étera) se basan en los temas de Material, que son una forma sistemática de personalizar Material Design para reflejar mejor la marca de tu producto. Un tema de Material contiene atributos de color, tipografía y forma. Cuando personalizas estos atributos, los 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 diseño predeterminado de MaterialTheme, mientras que la segunda usa un diseño modificado.
Figura 1: En la primera captura de pantalla, se muestra una app que no configura `MaterialTheme` y, por lo tanto, usa el diseño predeterminado. En la segunda captura de pantalla, se muestra una app que pasa parámetros a "MaterialTheme" para personalizar el diseño.

Color

Los colores se modelan en Compose con la clase Color, una clase que contiene 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 el tema oscuro y los temas anidados.

Ejemplo de paleta de colores de un tema
Figura 2: El sistema de colores de Material.

Compose proporciona la clase Colors para modelar el sistema de colores de Material. Colors proporciona funciones de compilador para crear conjuntos de colores claros o 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,
    // ...
)

Después de definir tus Colors, puedes pasarlos a un MaterialTheme:

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

Usa los colores del 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 componible, sino también proporcionar un color predeterminado para el contenido, es decir, los elementos componibles que 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 principal, y Icon usa ese color para establecer su tono.

Dos ejemplos del mismo banner, con colores distintos
Figura 3: Si estableces diferentes colores de fondo, se producirán diferentes colores de texto y de íconos.

El método contentColorFor() recupera el color "activado" adecuado para cualquier color de tema. Por ejemplo, si estableces un color de fondo primary en Surface, se usa esta función para establecer onPrimary como el color del contenido. Si configuras un color de fondo que no pertenece a ningún tema, también debes especificar un color de contenido adecuado. Usa LocalContentColor para recuperar el color de contenido preferido para el fondo actual, en una posición determinada de 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. Las recomendaciones de legibilidad de texto de Material Design aconsejan usar diferentes niveles de opacidad para transmitir distintos niveles de importancia.

En Jetpack Compose, esto se implementa con LocalContentAlpha. Puedes especificar una versión alfa de contenido para establecer una jerarquía proporcionando un valor para este objeto CompositionLocal. Los elementos componibles anidados pueden usar este valor para aplicar el tratamiento alfa a su contenido. Por ejemplo, Text y Icon de forma predeterminada utilizan la combinación de LocalContentColor ajustada para utilizar LocalContentAlpha. 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 Datos de alcance local con CompositionLocal.

Captura de pantalla del título de un artículo, que muestra diferentes niveles de énfasis del texto
Figura 4: Aplica diferentes niveles de énfasis al texto para comunicar visualmente la jerarquía de la información. La primera línea de texto es el título y tiene la información más importante, por lo que usa ContentAlpha.high. La segunda línea contiene 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 con mayor elevación en los temas oscuros reciben superposiciones de elevación, que aclaran su fondo. Cuanto mayor sea la elevación de una superficie (acercándola a una fuente de luz implícita), más clara se volverá esa superficie.

El elemento Surface componible aplica estas superposiciones automáticamente cuando se usan colores oscuros, al igual que cualquier otro elemento componible de Material que use una superficie:

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

Captura de pantalla de una app en la que se muestran los colores sutilmente diferentes que se usan para los elementos en distintos niveles de elevación
Figura 5: Las tarjetas y la navegación inferior usan el color surface como fondo. Como las tarjetas y la navegación inferior están en diferentes niveles de elevación por encima del fondo, tienen colores ligeramente 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 involucren un Surface, usa LocalElevationOverlay, un CompositionLocal que contenga el 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, proporciona null en el punto elegido de una jerarquía de elementos que admiten composición:

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

Acentuación de color limitada

Material recomienda aplicar acentuación de color limitada para los temas oscuros, prefiriendo el uso del color surface sobre el color primary en la mayoría de los casos. Los elementos de Material que admiten composición, como TopAppBar y BottomNavigation, implementan este comportamiento de forma predeterminada.

Captura de pantalla de un tema oscuro de Material, en la que se muestra la barra superior de la app con el color de la superficie en lugar del color principal para los acentos de color limitados
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 la 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, lo que te anima a usar una pequeña cantidad de estilos con nombres semánticos.

Ejemplo de varios tipos de letra en distintos estilos
Figura 7: El sistema de tipos de Material.

Compose implementa el sistema de tipos con las clases Typography, TextStyle y relacionadas con las fuentes. El constructor Typography ofrece valores predeterminados para cada estilo, de modo que puedes omitir cualquier parámetro 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 quieres usar el mismo tipo de letra en todo el texto, especifica el parámetro defaultFontFamily y omite el fontFamily de cualquier elemento TextStyle:

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

Usa estilos de texto

Se accede a los elementos TextStyle con MaterialTheme.typography. Recupera los elementos 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: El sistema de formas de Material.

Compose implementa el sistema de formas con la clase Shapes, que te permite especificar un 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 se establecen de forma predeterminada en pequeño, AlertDialog se establece de forma predeterminada en mediano y ModalDrawer se establece de forma predeterminada en grande. Consulta la referencia del esquema de formas para ver la asignación completa.

Usa formas

Se accede a los elementos Shape con MaterialTheme.shapes. Recupera los elementos 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

No hay un concepto equivalente en Compose de los estilos predeterminados de las vistas de Android. Puedes proporcionar una funcionalidad similar creando tus propias funciones de componibilidad overload que unan los componentes de Material. Por ejemplo, para crear un estilo de botón, une un botón en tu propia función de componibilidad, configura directamente los parámetros que deseas o necesitas modificar, y expón otros como parámetros al elemento componible que los 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

Puedes lograr el equivalente de las superposiciones de temas de las vistas de Android en Compose anidando elementos componibles MaterialTheme. Debido a que MaterialTheme establece los colores, la tipografía y las formas de forma predeterminada en el valor del tema actual, todos los demás parámetros conservan sus valores predeterminados cuando un tema solo establece uno de esos parámetros.

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. En la siguiente captura de pantalla y el código, se ilustra este concepto:

Captura de pantalla de una app que muestra temas anidados, con un tema rosa para la pantalla principal y un tema azul para una sección relacionada
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.

Captura de pantalla de dos botones: uno habilitado y otro inhabilitado, que muestran sus diferentes estados visuales
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 necesitas configurar valores que se usan en diferentes estados, hay clases y funciones de conveniencia disponibles. Considera 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
    )
) { /* ... */ }

Captura de pantalla de dos botones con color y elevación ajustados para los estados habilitado e inhabilitado
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 que se está interactuando con ellos. Si usas MaterialTheme en tu jerarquía, se usa un Ripple como el Indication predeterminado dentro de modificadores como clickable y indication.

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

Puedes extender RippleTheme y usar las funciones de utilidad defaultRippleColor y defaultRippleAlpha. Luego, puedes proporcionar tu tema de ondas personalizado en tu jerarquía con 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 animado que muestra botones con diferentes efectos de ondas cuando se presionan
Figura 14: Botones con diferentes valores de ondas que se brindan a través de RippleConfiguration

Más información

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

Codelabs

Videos