A veces, es necesario anular el comportamiento del enfoque predeterminado de los elementos en la pantalla. Por ejemplo, es posible que quieras agrupar elementos componibles, evitar el enfoque en un elemento componible determinado, solicitar el enfoque explícitamente en uno, capturar o liberar el enfoque, o redireccionar el enfoque al entrada o la salida. En esta sección, se describe cómo cambiar el comportamiento del enfoque cuando los valores predeterminados no son lo que necesitas.
Proporciona una navegación coherente con grupos focales
A veces, Jetpack Compose no adivina de inmediato el siguiente elemento correcto para la navegación con pestañas, en especial cuando un Composables
superior complejo, como las pestañas y las listas.
Si bien la búsqueda de enfoque suele seguir el orden de declaración de Composables
, esto es imposible en algunos casos, como cuando uno de los Composables
de la jerarquía es un elemento desplazable horizontal que no es completamente visible. Esto se muestra en el siguiente ejemplo.
Jetpack Compose puede decidir enfocar el siguiente elemento más cercano al inicio de la pantalla, como se muestra a continuación, en lugar de continuar en la ruta que esperas para la navegación unidireccional:
En este ejemplo, está claro que los desarrolladores no querían que el enfoque vaya de la pestaña Chocolates a la primera imagen que se muestra a continuación y, luego, vuelva a la pestaña Repostería. En cambio, quería que el enfoque continuara en las pestañas hasta la última y, luego, se enfocara en el contenido interno:
En situaciones en las que es importante que un grupo de elementos componibles obtenga el enfoque de forma secuencial, como en la fila Tab del ejemplo anterior, debes unir el Composable
en un elemento superior que tenga el modificador focusGroup()
:
LazyVerticalGrid(columns = GridCells.Fixed(4)) { item(span = { GridItemSpan(maxLineSpan) }) { Row(modifier = Modifier.focusGroup()) { FilterChipA() FilterChipB() FilterChipC() } } items(chocolates) { SweetsCard(sweets = it) } }
La navegación bidireccional busca el elemento componible más cercano para la dirección determinada. Si un elemento de otro grupo está más cerca de un elemento no totalmente visible en el grupo actual, la navegación elegirá el más cercano. Para evitar este comportamiento, puedes aplicar el modificador focusGroup()
.
FocusGroup
hace que un grupo completo parezca una sola entidad en términos de enfoque, pero el grupo en sí no obtendrá el enfoque. En su lugar, el elemento secundario más cercano lo obtendrá. De esta manera, la navegación sabrá que debe dirigirse al elemento que no es completamente visible antes de abandonar el grupo.
En este caso, las tres instancias de FilterChip
se enfocarán antes que los elementos SweetsCard
, incluso cuando SweetsCards
sean completamente visibles para el usuario y algunos FilterChip
podrían estar ocultos. Esto sucede porque el modificador focusGroup
le indica al administrador de enfoque que ajuste el orden en el que se enfocan los elementos para que la navegación sea más fácil y coherente con la IU.
Sin el modificador focusGroup
, si FilterChipC
no estaba visible, la navegación de enfoque lo recogería en último lugar. Sin embargo, agregar un modificador de este tipo hace que no solo sea detectable, sino que también adquirirá el enfoque inmediatamente después de FilterChipB
, como esperarían los usuarios.
Cómo hacer que un elemento componible sea enfocable
Algunos elementos componibles son enfocables por diseño, como un botón o un elemento componible con el modificador clickable
adjunto. Si quieres agregar específicamente un comportamiento enfocable a un elemento componible, usa el modificador focusable
:
var color by remember { mutableStateOf(Green) } Box( Modifier .background(color) .onFocusChanged { color = if (it.isFocused) Blue else Green } .focusable() ) { Text("Focusable 1") }
Cómo hacer que un elemento componible no se pueda enfocar
Puede haber situaciones en las que algunos de tus elementos no deberían participar en el enfoque. En esas raras ocasiones, puedes aprovechar el canFocus property
para excluir un Composable
de modo que no sea enfocable.
var checked by remember { mutableStateOf(false) } Switch( checked = checked, onCheckedChange = { checked = it }, // Prevent component from being focused modifier = Modifier .focusProperties { canFocus = false } )
Solicita el foco de teclado con FocusRequester
En algunos casos, es posible que desees solicitar de manera explícita el enfoque como respuesta a una interacción del usuario. Por ejemplo, puedes preguntarle a un usuario si quiere volver a completar un formulario y, si presiona "sí", quieres que se vuelva a enfocar el primer campo de ese formulario.
Lo primero que debes hacer es asociar un objeto FocusRequester
con el elemento componible al que quieres mover el enfoque del teclado. En el siguiente fragmento de código, se asocia un objeto FocusRequester
con un TextField
mediante la configuración de un modificador llamado Modifier.focusRequester
:
val focusRequester = remember { FocusRequester() } var text by remember { mutableStateOf("") } TextField( value = text, onValueChange = { text = it }, modifier = Modifier.focusRequester(focusRequester) )
Puedes llamar al método requestFocus
de FocusRequester para enviar solicitudes de enfoque reales. Debes invocar este método fuera de un contexto Composable
(de lo contrario, se volverá a ejecutar en cada recomposición). En el siguiente fragmento, se muestra cómo solicitar al sistema que mueva el enfoque del teclado cuando se hace clic en el botón:
val focusRequester = remember { FocusRequester() } var text by remember { mutableStateOf("") } TextField( value = text, onValueChange = { text = it }, modifier = Modifier.focusRequester(focusRequester) ) Button(onClick = { focusRequester.requestFocus() }) { Text("Request focus on TextField") }
Cómo capturar y liberar el enfoque
Puedes aprovechar el enfoque para guiar a los usuarios a fin de que proporcionen los datos correctos que tu app necesita para realizar su tarea, por ejemplo, obtener una dirección de correo electrónico o un número de teléfono válidos. Aunque los estados de error informan a los usuarios sobre lo que sucede, es posible que necesites que el campo con información errónea se mantenga enfocado hasta que se solucione.
Para capturar el enfoque, puedes invocar el método captureFocus()
y liberarlo después con el método freeFocus()
, como en el siguiente ejemplo:
val textField = FocusRequester() TextField( value = text, onValueChange = { text = it if (it.length > 3) { textField.captureFocus() } else { textField.freeFocus() } }, modifier = Modifier.focusRequester(textField) )
Prioridad de los modificadores de enfoque
Modifiers
puede verse como elementos que solo tienen un elemento secundario, por lo que cuando los pones en cola, cada Modifier
de la izquierda (o superior) une el Modifier
que sigue a la derecha (o debajo). Esto significa que el segundo Modifier
está contenido dentro del primero, de modo que, cuando se declaren dos focusProperties
, solo funcionará el superior, ya que los siguientes se encuentran en el superior.
Para aclarar más el concepto, consulta el siguiente código:
Modifier .focusProperties { right = item1 } .focusProperties { right = item2 } .focusable()
En este caso, no se usará el focusProperties
que indica item2
como el foco correcto, ya que está contenido en el anterior; por lo tanto, se usará item1
.
Con este enfoque, un elemento superior también puede restablecer el comportamiento a la configuración predeterminada mediante FocusRequester.Default
:
Modifier .focusProperties { right = Default } .focusProperties { right = item1 } .focusProperties { right = item2 } .focusable()
El elemento superior no tiene que ser parte de la misma cadena de modificadores. Un elemento componible superior puede reemplazar una propiedad de enfoque de un elemento componible secundario. Por ejemplo, considera este FancyButton
que hace que el botón no pueda enfocarse:
@Composable fun FancyButton(modifier: Modifier = Modifier) { Row(modifier.focusProperties { canFocus = false }) { Text("Click me") Button(onClick = { }) { Text("OK") } } }
Un usuario puede hacer que este botón vuelva a ser enfocable si se configura canFocus
en true
:
FancyButton(Modifier.focusProperties { canFocus = true })
Como cada Modifier
, las relacionadas con el enfoque se comportan de manera diferente según el orden en que las declaras. Por ejemplo, un código como el siguiente hace que Box
sea enfocable, pero FocusRequester
no está asociado con este enfocable, ya que se declara después del enfocable.
Box( Modifier .focusable() .focusRequester(Default) .onFocusChanged {} )
Es importante recordar que un focusRequester
está asociado con el primer elemento enfocable debajo de él en la jerarquía, por lo que este focusRequester
apunta al primer elemento secundario enfocable. Si no hay ninguno disponible, no apuntará a nada.
Sin embargo, como la Box
es enfocable (gracias al modificador focusable()
), puedes navegar hacia ella con la navegación bidireccional.
Como otro ejemplo, cualquiera de las siguientes opciones funcionaría, ya que el modificador onFocusChanged()
hace referencia al primer elemento enfocable que aparece después de los modificadores focusable()
o focusTarget()
.
Box( Modifier .onFocusChanged {} .focusRequester(Default) .focusable() ) |
Box( Modifier .focusRequester(Default) .onFocusChanged {} .focusable() ) |
Redireccionar el enfoque al entrar o salir
A veces, debes proporcionar un tipo de navegación muy específico, como el que se muestra en la siguiente animación:
Antes de analizar cómo crear esto, es importante comprender el comportamiento predeterminado de la búsqueda de enfoque. Sin ninguna modificación, una vez que la búsqueda de enfoque alcanza el elemento Clickable 3
, si presionas DOWN
en el pad direccional (o la tecla de flecha equivalente), el enfoque se moverá a lo que se muestra debajo de Column
, lo que saldrá del grupo y se ignorará el de la derecha. Si no hay elementos enfocables disponibles, el enfoque no se mueve a ningún lado, pero permanece en Clickable 3
.
Para modificar este comportamiento y proporcionar la navegación deseada, puedes aprovechar el modificador focusProperties
, que te ayuda a administrar lo que sucede cuando la búsqueda de enfoque ingresa o sale del elemento Composable
:
val otherComposable = remember { FocusRequester() } Modifier.focusProperties { exit = { focusDirection -> when (focusDirection) { Right -> Cancel Down -> otherComposable else -> Default } } }
Es posible dirigir el enfoque a un Composable
específico cada vez que entra o sale de una parte determinada de la jerarquía, por ejemplo, cuando tu IU tiene dos columnas y quieres asegurarte de que, cuando se procese la primera, el enfoque cambie a la segunda:
En este GIF, una vez que el enfoque alcanza el Clickable 3 Composable
en Column
1, el siguiente elemento que se enfoca es Clickable 4
en otro Column
. Este comportamiento se puede lograr combinando focusDirection
con los valores enter
y exit
dentro del modificador focusProperties
. Ambos necesitan una expresión lambda que tome como parámetro la dirección de la que proviene el foco y muestre un FocusRequester
. Esta lambda puede comportarse de tres maneras diferentes: mostrar FocusRequester.Cancel
evita que el enfoque continúe, mientras que FocusRequester.Default
no altera su comportamiento. En cambio, si proporcionas el FocusRequester
adjunto a otro Composable
, el enfoque saltará a ese Composable
específico.
Cambiar la dirección de avance del enfoque
Para hacer avanzar el enfoque al siguiente elemento o hacia una dirección precisa, puedes aprovechar el modificador onPreviewKey
e implicar LocalFocusManager
para avanzar el enfoque con el modificador moveFocus
.
En el siguiente ejemplo, se muestra el comportamiento predeterminado del mecanismo de enfoque: cuando se detecta una pulsación de teclas tab
, el foco avanza al siguiente elemento de la lista de enfoque. Si bien no es algo que debas configurar normalmente, es importante conocer el funcionamiento interno del sistema para poder cambiar el comportamiento predeterminado.
val focusManager = LocalFocusManager.current var text by remember { mutableStateOf("") } TextField( value = text, onValueChange = { text = it }, modifier = Modifier.onPreviewKeyEvent { when { KeyEventType.KeyUp == it.type && Key.Tab == it.key -> { focusManager.moveFocus(FocusDirection.Next) true } else -> false } } )
En este ejemplo, la función focusManager.moveFocus()
hace avanzar el enfoque al elemento especificado o a la dirección implícita en el parámetro de la función.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Reacciona para enfocarte
- Enfoque en Compose
- Cambia el orden de recorrido del enfoque