Cómo cambiar el comportamiento del enfoque

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:

Animación de una app que muestra una navegación horizontal superior y una lista de elementos debajo.
Figura 1: Animación de una app que muestra una navegación horizontal superior y una lista de elementos debajo

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:

Animación de una app que muestra una navegación horizontal superior y una lista de elementos debajo.
Figura 2: Animación de una app que muestra una navegación horizontal superior y una lista de elementos debajo

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:

Animación de una pantalla que muestra dos columnas de botones colocadas una al lado de la otra y animando el enfoque de una columna a la otra.
Figura 3: Animación de una pantalla que muestra dos columnas de botones colocadas una al lado de la otra y animando el enfoque de una columna a la otra

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:

Animación de una pantalla que muestra dos columnas de botones colocadas una al lado de la otra y animando el enfoque de una columna a la otra.
Figura 4: Animación de una pantalla que muestra dos columnas de botones colocadas una al lado de la otra y animando el enfoque de una columna a la otra

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.