Compatibilidad de entrada en pantallas grandes

En dispositivos de pantalla grande, los usuarios suelen interactuar con las apps mediante un teclado, un mouse, un panel táctil, una pluma stylus o un control de juegos. Para permitir que tu app acepte entradas de dispositivos externos, haz lo siguiente:

  • Prueba la compatibilidad básica con el teclado, como Ctrl + Z para deshacer, Ctrl + C para copiar y Ctrl + S para guardar. Consulta Cómo controlar las acciones del teclado para obtener una lista de las combinaciones de teclas predeterminadas.
  • Prueba la compatibilidad avanzada del teclado, por ejemplo, la tecla Tab y la navegación con las teclas de flecha del teclado, la tecla Intro para confirmar la entrada de texto y la barra espaciadora para reproducir y pausar contenido en apps de música.
  • Prueba las interacciones básicas del mouse, como hacer clic con el botón derecho para el menú contextual, ver los cambios del ícono cuando se coloca el cursor sobre un elemento y los eventos de desplazamiento del panel táctil y de la rueda del mouse en los componentes personalizados.
  • Prueba dispositivos de entrada específicos de la app, como la pluma stylus, los controles de juegos y los controles MIDI de apps de música.
  • Considera admitir tipos de entradas más avanzadas con las que la app podría destacarse en entornos de escritorio (por ejemplo, el panel táctil como dispositivo de reproducción sin pausa para apps de DJ, la captura del mouse para juegos y combinaciones de teclas más especializadas para quienes utilizan mucho el teclado).

Teclado

La manera en que la app responde a la entrada del teclado contribuye a la experiencia del usuario en pantallas grandes. Existen tres tipos de entrada de teclado: navegación, pulsaciones de teclas y combinaciones de teclas.

La navegación con teclado casi nunca se implementa en apps centradas en pantallas táctiles, pero los usuarios la esperan cuando usan una app y tienen las manos en el teclado. La navegación con el teclado puede ser esencial en teléfonos, tablets, dispositivos plegables y dispositivos de escritorio para los usuarios con necesidades de accesibilidad.

En muchas apps, el framework de Android controla automáticamente la navegación con las teclas de flecha y Tab. Por ejemplo, algunos elementos componibles se pueden enfocar de forma predeterminada, como un Button o un elemento componible con el modificador clickable. Por lo general, la navegación con teclado debería funcionar sin ningún código adicional. Para habilitar la navegación con teclado en los elementos componibles personalizados que no son enfocables de forma predeterminada, agrega 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")
}

Para obtener más información, consulta Cómo hacer que un elemento componible sea enfocable.

Cuando se habilita el enfoque, el framework de Android crea una asignación de navegación para todos los componentes enfocables según su posición. Por lo general, funciona como se espera, y no se necesita ningún desarrollo adicional.

Sin embargo, Compose no siempre determina el siguiente elemento correcto para la navegación con pestañas de elementos componibles complejos, como pestañas y listas, por ejemplo, cuando uno de los elementos componibles es un elemento horizontal desplazable que no es completamente visible.

Para controlar el comportamiento de enfoque, agrega el modificador focusGroup al elemento componible superior de una colección de elementos componibles. El enfoque se mueve al grupo y, luego, a través de él antes de pasar al siguiente componente enfocado, por ejemplo:

Row {
    Column(Modifier.focusGroup()) {
        Button({}) { Text("Row1 Col1") }
        Button({}) { Text("Row2 Col1") }
        Button({}) { Text("Row3 Col1") }
    }
    Column(Modifier.focusGroup()) {
        Button({}) { Text("Row1 Col2") }
        Button({}) { Text("Row2 Col2") }
        Button({}) { Text("Row3 Col2") }
    }
}

Para obtener más información, consulta Cómo proporcionar una navegación coherente con grupos focales.

Prueba el acceso a todos los elementos de la IU de tu app solo con el teclado. Debe ser fácil acceder a los elementos que se usan con frecuencia sin usar el mouse o mediar una entrada táctil.

Recuerda que la compatibilidad con el teclado puede ser fundamental para los usuarios con necesidades de accesibilidad.

Pulsaciones de teclas

Para la entrada de texto que se controlaría con un teclado virtual en pantalla (IME), como para, un TextField,, las apps deberían comportarse como se espera en dispositivos de pantalla grande, sin que se requieran acciones adicionales por parte del desarrollador. En el caso de que el framework no pueda anticipar las combinaciones de teclas, las apps deberán controlar el comportamiento por sí mismas, en especial, las que tiene vistas personalizadas.

Algunos ejemplos son las apps de chat que usan la tecla Intro para enviar un mensaje, las apps de música que inician y detienen la reproducción con la barra espaciadora, y los juegos que controlan el movimiento con las teclas w, a, s y d.

Puedes controlar pulsaciones de teclas individuales con el modificador onKeyEvent, que acepta una expresión lambda a la que se llama cuando el componente modificado recibe un evento de tecla. La propiedad KeyEvent#type te permite determinar si el evento es una pulsación de tecla (KeyDown) o una liberación de tecla (KeyUp):

Box(
    modifier = Modifier.focusable().onKeyEvent {
        if(
            it.type == KeyEventType.KeyUp &&
            it.key == Key.S
        ) {
            doSomething()
            true
        } else {
            false
        }
    }
)  {
    Text("Press S key")
}

Como alternativa, puedes anular la devolución de llamada onKeyUp() y agregar el comportamiento esperado para cada código de clave recibido:

override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
    return when (keyCode) {
        KeyEvent.KEYCODE_ENTER -> {
            sendChatMessage()
            true
        }
        KeyEvent.KEYCODE_SPACE -> {
            playOrPauseMedia()
            true
        }
        else -> super.onKeyUp(keyCode, event)
    }
}

Un evento onKeyUp se produce cuando se suelta una tecla. El uso de la devolución de llamada evita que las apps necesiten procesar varios eventos onKeyDown si se mantiene presionada una tecla o si esta se suelta lentamente. Los juegos y las apps que necesitan detectar el momento en que se presiona una tecla o si el usuario mantiene presionada una tecla pueden detectar el evento onKeyDown y controlar los eventos onKeyDown repetidos.

Para obtener más información, consulta Cómo controlar las acciones del teclado.

Accesos directos

Cuando se utiliza un teclado de hardware, se esperan combinaciones de teclas comunes que incluyen las teclas Ctrl, Alt, Mayúsculas y Meta. Si una app no implementa atajos, la experiencia puede ser frustrante para los usuarios. Los usuarios avanzados también valoran los accesos directos para las tareas específicas de la app que se usan con frecuencia. Los accesos directos facilitan el uso de una app y la diferencian de aquellas que no tienen accesos directos.

Algunas combinaciones de teclas habituales incluyen Ctrl + S (guardar), Ctrl + Z (deshacer) y Ctrl + Mayúsculas + Z (rehacer). Para obtener una lista de los atajos predeterminados, consulta Cómo controlar las acciones del teclado.

Un objeto KeyEvent tiene los siguientes atributos que indican si se presionan las teclas modificadoras:

Por ejemplo:

Box(
    Modifier.onKeyEvent {
        if (it.isAltPressed && it.key == Key.A) {
            println("Alt + A is pressed")
            true
        } else {
            false
        }
    }
    .focusable()
)

Para obtener más información, consulta lo siguiente:

Pluma stylus

Muchos dispositivos con pantalla grande incluyen una pluma stylus. Las apps para Android controlan las plumas stylus como entrada de pantalla táctil. Algunos dispositivos también pueden tener una tabla de dibujo con USB o Bluetooth, como Wacom Intuos. Las apps para Android pueden recibir entradas Bluetooth, pero no entradas USB.

Para acceder a los objetos MotionEvent de la pluma stylus, agrega el modificador pointerInteropFilter a una superficie de dibujo. Implementa una clase ViewModel con un método que procese eventos de movimiento. Pasa el método como la lambda onTouchEvent del modificador pointerInteropFilter:

@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
   Canvas(modifier = modifier
       .clipToBounds()
       .pointerInteropFilter {
           viewModel.processMotionEvent(it)
       }

   ) {
       // Drawing code here.
   }
}

El objeto MotionEvent contiene información sobre el evento:

Puntos históricos

Android agrupa eventos de entrada y los entrega una vez por fotograma. Una pluma stylus puede informar eventos con frecuencias mucho más altas que la pantalla. Cuando creas apps de dibujo, verifica los eventos que pueden estar en el pasado reciente mediante las APIs de getHistorical:

Rechazo de la palma

Cuando los usuarios dibujan, escriben o interactúan con la app con una pluma stylus, a veces tocan la pantalla con la palma de las manos. El evento táctil (configurado en ACTION_DOWN o ACTION_POINTER_DOWN) se puede informar a tu app antes de que el sistema reconozca e ignore el toque inadvertido de la palma.

Para cancelar los eventos táctiles de la palma, Android envía un MotionEvent. Si tu app recibe ACTION_CANCEL, cancela el gesto. Si tu app recibe ACTION_POINTER_UP, verifica si FLAG_CANCELED está configurado. Si es así, cancela el gesto.

No verifiques solo FLAG_CANCELED. En Android 13 (nivel de API 33) y versiones posteriores, el sistema establece FLAG_CANCELED para eventos ACTION_CANCEL, pero no establece la marca en versiones anteriores de Android.

Android 12

En Android 12 (nivel de API 32) y versiones anteriores, solo se puede detectar el rechazo de la palma de la mano para eventos táctiles de un solo puntero. Si un toque de palma es el único puntero, el sistema configura ACTION_CANCEL en el objeto de evento de movimiento para cancelar el evento. Si otros punteros están inactivos, el sistema establece ACTION_POINTER_UP, que no es suficiente para detectar el rechazo de la palma.

Android 13

En Android 13 (nivel de API 33) y versiones posteriores, si un toque de palma es el único puntero, el sistema configura ACTION_CANCEL y FLAG_CANCELED en el objeto de evento de movimiento para cancelar el evento. Si otros punteros están inactivos, el sistema establece ACTION_POINTER_UP y FLAG_CANCELED.

Cuando tu app reciba un evento de movimiento con ACTION_POINTER_UP, busca FLAG_CANCELED para determinar si el evento indica el rechazo de la palma (o cualquier otra cancelación del evento).

Apps para tomar notas

ChromeOS tiene un intent especial que les muestra a los usuarios apps registradas para tomar notas. Para registrar una app como una para tomar notas, agrega lo siguiente al manifiesto de la app:

<intent-filter>
    <action android:name="org.chromium.arc.intent.action.CREATE_NOTE" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

Cuando se registra una app en el sistema, el usuario puede seleccionarla como la predeterminada para tomar notas. Cuando se solicita una nota nueva, la app debe crear una nota vacía y lista para la entrada de la pluma stylus. Cuando el usuario desea escribir en una imagen (como una captura de pantalla o una imagen descargada), la app se inicia con el objeto ClipData que contiene uno o más elementos con URI de content://. La app debe crear una nota que use la primera imagen adjunta como imagen de fondo y que ingrese un modo en el que el usuario pueda dibujarla en la pantalla con una pluma stylus.

Cómo probar intents para tomar notas sin una pluma stylus

[TBD quitar sección.]

Para probar si una app responde correctamente a los intents para tomar notas sin una pluma stylus activa, usa el siguiente método para mostrar las opciones para tomar notas en ChromeOS:

  1. Cambia al modo de desarrollo y haz que se pueda escribir en el dispositivo.
  2. Presiona Ctrl + Alt + F2 para abrir una terminal.
  3. Ejecuta el comando sudo vi /etc/chrome_dev.conf.
  4. Presiona i para editar y agregar el elemento --ash-enable-palette a una nueva línea al final del archivo.
  5. Para guardar, presiona Esc, escribe :, w, q y presiona Intro.
  6. Presiona Ctrl + Alt + F1 para volver a la IU normal de ChromeOS.
  7. Sal y vuelve a acceder.

Ahora debería haber un menú de la pluma stylus en la biblioteca:

  • Presiona el botón de la pluma stylus en la barra y elige Nueva nota. Se debería abrir una nota de dibujo en blanco.
  • Toma una captura de pantalla. En la barra, selecciona el botón de la pluma stylus > Captura de pantalla o descarga una imagen. En la notificación, debería aparecer la opción Escribir en la imagen. Se debería iniciar la app con la imagen lista para poder escribir sobre ella.

Compatibilidad con mouse y panel táctil

Por lo general, la mayoría de las apps solo necesitan controlar tres eventos centrados en pantallas grandes: hacer clic con el botón derecho, colocar el cursor sobre un elemento y arrastrar y soltar.

Hacer clic con el botón derecho

Cualquier acción que permita que una app muestre un menú contextual, como mantener presionado un elemento de la lista, también debe responder a eventos de clic con el botón derecho.

Para controlar eventos de clic con el botón derecho, las apps deben registrar un objeto View.OnContextClickListener:

Box(modifier = Modifier.fillMaxSize()) {
    AndroidView(
        modifier = Modifier.fillMaxSize(),
        factory = { context ->
            val rootView = FrameLayout(context)
            val onContextClickListener =
                View.OnContextClickListener { view ->
                    showContextMenu()
                    true
                }
            rootView.setOnContextClickListener(onContextClickListener)
            rootView
        },
    )
}

Para obtener detalles sobre cómo crear menús contextuales, consulta Cómo crear un menú contextual.

Colocar el cursor sobre un elemento

Puedes lograr que los diseños de tus apps se sientan optimizados y más fáciles de usar si controlas los eventos de desplazamiento. Esto es especialmente cierto para los componentespersonalizados:

Los dos ejemplos más comunes son los siguientes:

  • Cambiar el ícono del puntero del mouse para indicarles a los usuarios si un elemento tiene un comportamiento interactivo, como elementos que se pueden editar o en los que se puede hacer clic
  • Agregar comentarios visuales a los elementos en una lista o una cuadrícula grande cuando el puntero se coloca sobre ellos

Arrastrar y soltar

En un entorno multiventana, los usuarios esperan poder arrastrar y soltar elementos entre apps. Esto se aplica a dispositivos de escritorio, así como a tablets, teléfonos y dispositivos plegables en modo de pantalla dividida.

Considera si es probable que los usuarios arrastren elementos a tu app. Por ejemplo, los editores de fotos deben esperar recibir fotos, los reproductores de audio deben esperar recibir archivos de audio y los programas de dibujo deben esperar recibir fotos.

Para agregar compatibilidad con la función de arrastrar y soltar, consulta Arrastrar y soltar y la entrada de blog Android en ChromeOS: Implementación de arrastrar y soltar.

Consideraciones especiales para ChromeOS

Compatibilidad con puntero avanzado

Las apps que realizan un control avanzado de la entrada del mouse y del panel táctil deben implementar un modificador pointerInput para obtener un PointerEvent:

@Composable
private fun LogPointerEvents(filter: PointerEventType? = null) {
    var log by remember { mutableStateOf("") }
    Column {
        Text(log)
        Box(
            Modifier
                .size(100.dp)
                .background(Color.Red)
                .pointerInput(filter) {
                    awaitPointerEventScope {
                        while (true) {
                            val event = awaitPointerEvent()
                            // handle pointer event
                            if (filter == null || event.type == filter) {
                                log = "${event.type}, ${event.changes.first().position}"
                            }
                        }
                    }
                }
        )
    }
}

Examina el objeto PointerEvent para determinar lo siguiente:

Controles de juegos

Algunos dispositivos Android de pantalla grande admiten hasta cuatro controles de juegos. Usa las APIs estándar de control de juegos de Android para controlar los controles de juegos (consulta Cómo brindar compatibilidad con controles de juegos).

Los botones del controlador de juegos se asignan a valores comunes después de una asignación común. Sin embargo, no todos los fabricantes de controles de juegos siguen las mismas convenciones de asignación. Puedes proporcionar una experiencia mucho mejor si permites que los usuarios seleccionen diferentes asignaciones populares para controles. Para obtener más información, consulta Cómo procesar la presión de las teclas de los botones de los controles de juegos.

Modo de traducción de entrada

ChromeOS habilita un modo de traducción de entrada de forma predeterminada. En la mayoría de las apps para Android, ese modo ayuda a que funcionen como se espera en un entorno de escritorio. Algunos ejemplos incluyen la habilitación automática del desplazamiento con dos dedos en el panel táctil, el desplazamiento con la rueda del mouse y la asignación de coordenadas de pantalla sin procesar a las coordenadas de la ventana. Por lo general, los desarrolladores de apps no necesitan implementar ninguno de esos comportamientos.

Si una app implementa un comportamiento de entrada personalizado (por ejemplo, definir una acción personalizada de pellizcar con dos dedos en el panel táctil) o si esas traducciones de entrada no proporcionan los eventos de entrada que espera la app, puedes inhabilitar el modo de traducción de entrada agregando la siguiente etiqueta al manifiesto de Android:

<uses-feature
    android:name="android.hardware.type.pc"
    android:required="false" />

Recursos adicionales