En este tutorial, aprenderás acerca del uso de botones en Compose. Los botones le permiten a los usuarios realizar acciones y tomar decisiones con un tap en su superficie. Verás que en el Material Design existen cuatro tipos de botones y múltiples opciones de configuración de su contenido.
Antes de comenzar, si eres nuevo en Compose, te recomiendo leer:
Con esto en claro, te dejo una tabla con todos los temas que veremos del componente Button:
Ejemplo De Botones En Compose
Puedes encontrar los ejemplos de código de botones en Compose en el paquete p7_componentes
del repositorio en GitHub.
Al interior del paquete examples/Button
encontrarás un archivo Kotlin por cada sección descrita en este tutorial. El archivo ButtonsScreen.kt
contiene la función componible donde puedes invocar el ejemplo que desees probar o una previsualización de dicho contenido.
1. Tipos De Botones
Text Button
Usa un text button para expresar poco énfasis de una acción, es decir, acciones cuyo pronunciamiento es menor debido a la superficie en que se encuentra (comúnmente cards y dialogs) o a su baja frecuencia de uso. Un text button no tiene contenedor, ya que no es su fin distraer al usuario del contenido cercano.
En Compose este tipo de botones son representados por la función de alto nivel TextButton()
, la cual recibe los siguientes parámetros:
@Composable
fun TextButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = null,
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.textButtonColors(),
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
content: RowScope.() -> Unit
): @Composable Unit
Ejemplo:
Crear un botón de texto que cuando sea presionado actualice su texto con la cantidad de veces que ha ocurrido este evento.
@Composable
fun TextButtonExample() {
var counter by remember { mutableStateOf(0) }
TextButton(onClick = { counter++ }) {
Text("CLICS: $counter")
}
}
Los atributos principales de la configuración serán content
para definir el contenido, onClick
para controlar los eventos de clic y modifier
para redimensionar, posicionar al botón.
El ejemplo anterior guarda el estado del contador en counter
, el cual es actualizado en onClick
y se refleja en el texto del componente Text
.
Outlined Button
El outlined button (botón delineado) representa una acción con un énfasis medio, lo que quiere decir que esta es importante, pero no lo suficiente para tratarse de una acción principal.
Para crear uno, invoca a la función OutlinedButton()
. Sus parámetros son idénticos a los de TextButton
, por lo que procedes con el mismo criterio.
Ejemplo:
Crear un OutlinedButton
que al ser cliqueado muestre en un Text
la fecha y hora actual.
@Composable
fun OutlinedButtonExample() {
var date by remember { mutableStateOf(LocalDateTime.now()) }
Column {
OutlinedButton(
onClick = { date = LocalDateTime.now() },
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text("INFORMAR")
}
Spacer(Modifier.size(16.dp))
Text(
"Fecha actual -> $date",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
Contained Button
El contained button representa alto énfasis para las acciones primarias en tu App. Visualmente tienen elevación y su contenedor está relleno por un color sólido, de esta forma logran destacar de otros elementos alrededor.
Inclúyelos en tu UI a través de la función Button()
, que a su vez posee los mismos parámetros que los dos tipos de botones anteriores.
Ejemplo:
Crear un Contained Button que cambie sus dimensiones entre 150, 200 y 300dp cada que es presionado:
@Composable
fun ContainedButtonExample() {
val WIDTH1 = 150.dp
val WIDTH2 = 200.dp
val WIDTH3 = 300.dp
var width by remember { mutableStateOf(WIDTH1) }
Button(
onClick = {
width = when (width) {
WIDTH1 -> WIDTH2
WIDTH2 -> WIDTH3
else -> WIDTH1
}
},
modifier = Modifier.width(width)
) {
Text("CAMBIAR")
}
}
Toggle Button
Los iconos pueden actuar como toggle buttons en el momento que desees presentar una sola opción seleccionable.
Para ello compose provee el componente IconToggleButton
, que te permite crear un ejemplar con los siguientes parámetros:
@Composable
fun IconToggleButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: () -> Unit
): @Composable Unit
Donde checked
determina si el toggle button está marcado y onCheckedChange
te permite especificar las acciones a realizar cuando sea seleccionado.
Ejemplo:
El icono de la ilustración anterior se crea con la siguiente función componible:
@Composable
fun ToggleButtonExample() {
var checked by remember { mutableStateOf(false) } //1
IconToggleButton(checked = checked, onCheckedChange = { checked = it }) { //2
Icon(
painter = painterResource( //3
if (checked) R.drawable.ic_bookmark
else R.drawable.ic_bookmark_border
),
contentDescription = //4
if (checked) "Añadir a marcadores"
else "Quitar de marcadores",
tint = Color(0xFF26C6DA) //5
)
}
}
La implementación se basa en:
- Declarar un estado booleano que recuerde la activación/desactivación del toggle button
- Crear el elemento
IconToggleButton
pasándole la variablechecked
en el parámetro del mismo nombre y actualizando dicho estado desdeonCheckedChange
- Dibujar un componente
Icon
que usa achecked
para determinar el recurso drawable a pintar - La descripción del contenido también cambiará el texto de asistencia según el estado
- El color del icono será
Blue 400
IconButton
El componente IconButton
te permite crear iconos cliqueables, para representar acciones en otros componentes. Su tamaño es de 48x48dp y el icono en su interior debería ser de 24x24dp para mantener los lineamientos del Material Design.
Sus parámetros son simples:
@Composable
fun IconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: () -> Unit
): @Composable Unit
El valor de content
típicamente es un elemento Icon
que especifique la imagen a dibujar en el contenido.
Ejemplo:
Aplicar un tinte aleatorio cuando un IconButton
es presionado.
@Composable
fun IconButtonExample() {
var color by remember { mutableStateOf(Color.LightGray) }
IconButton(
onClick = {
val randomColor = Color(Random.nextLong(0xFF000000, 0xFFFFFFFF))
color = randomColor
}) {
Icon(
Icons.Filled.Home,
contentDescription = "Cambiar color",
tint = color
)
}
}
2. Configuración De Botones
En esta sección veremos el uso de varios parámetros de los componentes de botón que actúan como opciones de configuración general.
Añadir icono
Si deseas comunicar el significado de la acción de un botón a través de un icono, entonces añade un elemento Icon
antes de la etiqueta de texto como se muestra en el siguiente ejemplo:
Button(onClick = { }) {
Icon(
imageVector = Icons.Default.ShoppingCart,
contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize)
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text("BOTÓN")
}
Empleamos al objeto de utilidad ButtonDefaults
para acceder a dos propiedades clave: IconSize
para el tamaño estándar del icono dentro de un botón e IconSpacing para el correcto espaciado entre el icono y la etiqueta de texto.
Desactivar Un Botón
Si quieres comunicar que el botón no es interactivo y retirar todo su enfasis de la UI, entonces pasa el valor de false
al parámetro enabled
.
Ejemplo:
Desactivar un botón de guardado cuando un campo de texto no tiene al menos tres caracteres.
@Composable
fun DisabledButtonExample() {
var tag by remember { mutableStateOf("") }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
TextField(
value = tag,
onValueChange = { tag = it },
label = { Text("Etiqueta") }
)
Spacer(Modifier.size(8.dp))
Button(onClick = { }, enabled = tag.length > 2) {
Text("GUARDAR")
}
}
}
Como ves, determinamos el valor de desactivación en enabled
. Su valor cambia a partir del condicional asociado al tamaño del String en el campo de texto.
Añadir Borde A Un Botón
Utiliza el atributo border
de las funciones *Button
para especificar un borde de tipo BorderStroke
. Este recubrirá el contorno del botón según el tamaño y color que especifiques.
Ejemplo:
Aplicar un borde de 2dp de grosor y con un gradiente horizontal como color, a un botón con la etiqueta de texto «JUGAR»:
@Composable
fun BorderExample() {
OutlinedButton(
onClick = { }, border = BorderStroke(
width = 2.dp,
brush = Brush.horizontalGradient(
listOf(
Color(0xFF42A5F5),
Color(0xFFFFA726)
)
)
)
) {
Text("JUGAR", color = Color(0xFF5C6BC0))
}
}
3. Estilizar Botones
Cambiar Forma
Los botones hacen parte de la categoría de small components al momento de describir la figura de sus esquinas. Por esta razón, el parámetro shape
de los componentes de botón usa como valor por defecto a MaterialTheme.shapes.small
.
Personalizar la forma de un botón solo consiste en pasar una instancia del tipo BaseCornerShape
.
Por ejemplo:
La siguiente función componible muestra varios tipos de formas aplicadas a las esquinas de un botón de inicio de sesión:
@Composable
fun ButtonShapeExample() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Formas Personalizadas", style = MaterialTheme.typography.h6)
Spacer(Modifier.size(8.dp))
Button(onClick = { }, shape = CutCornerShape(0)) {
Text("BOTÓN")
}
Spacer(Modifier.size(8.dp))
Button(onClick = { }, shape = CutCornerShape(4.dp)) {
Text("BOTÓN")
}
Spacer(Modifier.size(8.dp))
Button(onClick = { }, shape = RoundedCornerShape(50)) {
Text("BOTÓN")
}
}
}
Cambiar Colores
De manera similar a TextField
, las funciones de botones poseen al parámetro colors
del tipo ButtonColors
. Esta interfaz especifica los colores del fondo y contenido del botón.
Para producir instancias de ella usaremos las funciones textButtonColors()
, outlinedButtonColors()
y buttonColors()
de ButtonDefaults
.
Ejemplo:
Cambiar los colores de cada tipo de botón con valores de la paleta del Material Design.
@Composable
fun ColorsButtonExample() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Colores Personalizados", style = MaterialTheme.typography.h6)
Spacer(Modifier.size(8.dp))
TextButton(
onClick = { },
colors = ButtonDefaults.textButtonColors(
contentColor = Color(0xFFEC407A)
)
) {
Text("TEXT BUTTON")
}
Spacer(Modifier.size(8.dp))
OutlinedButton(
onClick = { },
colors = ButtonDefaults.outlinedButtonColors(
contentColor = Color(0xFFAB47BC)
)
) {
Text("OUTLINED BUTTON")
}
Spacer(Modifier.size(8.dp))
Button(
onClick = { },
colors = ButtonDefaults.buttonColors(
backgroundColor = Color(0xFF7E57C2),
contentColor = Color(0xFFFFEE58)
)
) {
Text("CONTAINED BUTTON")
}
}
}
backgroundColor
corresponde al color del fondo del contenedor y contentColor
al color del contenido (los componentes al interior de content
).
Aunque también podrás asignar colores en el caso de la desactivación del botón con disabledBackgroundColor
y disabledContentColor
.
4. Grupo De Toggle Buttons
En el caso de que desees agrupar opciones relacionadas y sólo una pueda ser seleccionada, los toggle buttons pueden ser configurados en un grupo para resolver este problema. Estos se ubican en un mismo contenedor, donde la activación de uno de los botones, significa la desactivación de los demás.
En el sistema de views, la librería de materiales dispone de la clase MaterialButtonToggleGroup
para representar un grupo de toggle buttons. Pero en Compose debes implementarlo con botones delineados.
Ejemplo:
Crear un grupo de toggle buttons que ofrezcan las opciones «AUTO», «OSCURO» y «CLARO» como se vio en la ilustración pasada:
@Composable
private fun ButtonToggleGroup( // 1
options: List<String>,
selectedOption: String,
onOptionSelect: (String) -> Unit,
modifier: Modifier = Modifier
) {
Row(modifier = modifier) { // 2
options.forEachIndexed { index, option -> // 3
val selected = selectedOption == option // 4
val border = if (selected) BorderStroke( // 5
width = 1.dp,
color = MaterialTheme.colors.primary
) else ButtonDefaults.outlinedBorder
val shape = when (index) { // 6
0 -> RoundedCornerShape(
topStart = 4.dp,
bottomStart = 4.dp,
topEnd = 0.dp,
bottomEnd = 0.dp
)
options.size - 1 -> RoundedCornerShape(
topStart = 0.dp, bottomStart = 0.dp,
topEnd = 4.dp,
bottomEnd = 4.dp
)
else -> CutCornerShape(0.dp)
}
val zIndex = if (selected) 1f else 0f
val buttonModifier = when (index) { // 7
0 -> Modifier.zIndex(zIndex)
else -> {
val offset = -1 * index
Modifier
.offset(x = offset.dp)
.zIndex(zIndex)
}
}
val colors = ButtonDefaults.outlinedButtonColors( // 8
backgroundColor = if (selected) MaterialTheme.colors.primary.copy(alpha = 0.12f)
else MaterialTheme.colors.surface,
contentColor = if (selected) MaterialTheme.colors.primary else Color.DarkGray
)
OutlinedButton( // 9
onClick = { onOptionSelect(option) },
border = border,
shape = shape,
colors = colors,
modifier = buttonModifier.weight(1f)
) {
Text(option) // 10
}
}
}
}
En el código anterior:
- Creamos la función componible con cuatro parámetros producto del state hoisting. Donde
options
es la lista de textos de las opciones,selectedOption
la opción seleccionada,onOptionSelect
el controlador cuando es seleccionada una opción ymodifier
el modificador para permitir reutilizar configuraciones del componente completo - Usamos un layout Row para organizar los botones en fila y le pasamos el modificador entrante
- Iteramos sobre la lista de opciones con la función
forEachIndexed()
- Verificamos si la opción de la iteración está seleccionada
- Desde aquí comenzamos a crear todos los parámetros para el botón. El primero será el borde, el cual cambia su color al primario si está seleccionado. De lo contrario usamos el borde por defecto
outlinedBorder
- Creamos la forma del botón. Los botones interiores no tendrán esquinas afectadas, pero el primero y el último solo tendrán redondeo en las esquinas de los extremos
- Preparamos el modificador para eliminar el doble borde que se presentará entre dos botones contiguos. En este caso usamos a
zIndex()
para determinar el orden del dibujado. Quiere decir que un botón con mayor valor, será dibujado encima de uno con menor. Adicionalmente aplicamosoffset()
negativo en el eje X en todos los botones que no sean el primero - Definimos los colores de selección y deselección de los botones
- Creamos el
OutlinedButton
y pasamos todos los parámetros producidos. Transmitimos el click sobre cada botón, invocando aonOptionSelect(
) con la opción actual - El texto del botón será la opción actual
Finalmente, creamos un ButtonToggleButton
desde otra función componible que maneje el estado:
@Composable
fun ButtonToggleGroupExample() {
val auto = "AUTO"
val dark = "OSCURO"
val light = "LIGHT"
val options = listOf(auto, dark, light)
var selectedOption by remember { mutableStateOf(options[0]) }
ButtonToggleGroup(
options = options,
selectedOption = selectedOption,
onOptionSelect = { selectedOption = it },
)
}
Grupo De Toggle Buttons Con Solo Iconos
En el caso que desees solo iconos en cada opción del grupo de toggle buttons, entonces reemplaza el componente Text
del contenido en el OutlinedButton
por un Icon
.
Por ejemplo:
Cambiar la alineación de un texto a partir de un grupo de toggle buttons que especifican las siguientes acciones: Centrar, alinear a la izquierda y alinear a la derecha:
@Composable
private fun ButtonToggleGroup(
options: List<Int>,
selectedOption: Int,
onOptionSelect: (Int) -> Unit,
modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
options.forEachIndexed { index, option ->
//...
OutlinedButton(
onClick = { onOptionSelect(option) },
border = border,
shape = shape,
colors = colors,
contentPadding = PaddingValues(12.dp),
modifier = buttonModifier.defaultMinSize(48.dp)
) {
Icon(
painter = painterResource(option),
contentDescription = null
)
}
}
}
}
Las opciones ahora entran como una lista de identificadores de drawables para especificar el icono a dibujar. Además el OutlinedButton
reduce su contentPadding
a 12dp y se establece el tamaño mínimo a 48dp.
Ahora bien, para crear la pantalla que alinea el texto creamos la siguiente función:
@Composable
fun ButtonToggleGroupIconsExample() {
val options = listOf(
R.drawable.ic_format_align_center,
R.drawable.ic_format_align_left,
R.drawable.ic_format_align_right
)
var selectedOption by remember { mutableStateOf(options[0]) }
var align by remember { mutableStateOf(TextAlign.Center) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(LoremIpsum(10).values.joinToString(), textAlign = align)
Spacer(modifier = Modifier.size(16.dp))
ButtonToggleGroup(
options = options,
selectedOption = selectedOption,
onOptionSelect = { option ->
selectedOption = option
align = when (option) {
options[0] -> TextAlign.Center
options[1] -> TextAlign.Start
else -> TextAlign.End
}
}
)
}
}
Como ves, determinamos el valor TextAlign
según el parámetro option
percibido por la lambda en onOptionSelect
. Lo que permite actualizar el estado del componente Text en su parámetro textAlign
y la variable align
.
Únete Al Discord De Develou
Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!