Gestos con una mano con Compose


En Wear OS 7 (nivel de API 37) y versiones posteriores, un framework de gestos con una mano, junto con una API que forma parte de Compose para Wear OS, permite a los usuarios interactuar con tu app sin tocarla.

Si bien inicialmente se admitía en dispositivos Pixel Watch (Pixel Watch 3 y modelos posteriores), el framework está disponible para todos los OEM. Si adoptas esta API, la compatibilidad con gestos de tu app se ajustará automáticamente en todo el ecosistema a medida que se expanda la compatibilidad con hardware.

Para ayudar a los usuarios a descubrir los gestos disponibles sin sobrecargar la IU, el framework de Wear OS proporciona indicadores de gestos animados. Estas sugerencias visuales destacan dónde se puede realizar un gesto, mientras que el sistema administra automáticamente su cadencia de visualización y frecuencia de silenciamiento según las preferencias del usuario.

Gestos y acciones compatibles

El framework de gestos de Wear OS admite dos tipos de gestos:

  • Acción principal (doble pellizco): Se asigna a la acción principal en una pantalla, como responder una llamada o activar o desactivar la reproducción de contenido multimedia.
  • Acción de descartar (giro de muñeca): Se asigna a la navegación hacia atrás, a descartar un diálogo o a cancelar un mensaje.

Cómo configurar gestos en Compose

Si bien la API de gestos con una mano puede mejorar tu IU, es importante tener en cuenta que algunos OEMs y hardware no admiten estos gestos. Si la API detecta que tu app se ejecuta en uno de estos dispositivos no admitidos, la biblioteca no realiza ninguna operación automáticamente sin afectar las interacciones táctiles estándar.

Al igual que con los comportamientos estándar de Compose, puedes habilitar los gestos con una mano en los elementos de la IU con modificadores. Configuras los gestos de tu app según la acción que se realizará (primaria o de descarte) y un gestureId para coordinar las preferencias del usuario a nivel del sistema, como la cadencia de visualización de sugerencias y el silenciamiento de frecuencia. Para expresar esta configuración, crea un objeto OneHandedGestureConfiguration. Te recomendamos que uses la función rememberOneHandedGestureConfiguration para crearlo. En OneHandedGestureConfiguration, también puedes proporcionar la prioridad del gesto.

La función rememberOneHandedGestureConfiguration hace un seguimiento del historial de interacción del usuario en las recomposiciones sin exponer el estado de la aplicación. Una vez que tu app haya creado la configuración, debe pasarla a Modifier.oneHandedGesture en tu elemento componible interactivo.

Para ayudar a los usuarios a descubrir los gestos disponibles, la biblioteca proporciona el método OneHandedGestureClickIndicator. Este método actúa como un wrapper que reemplaza su contenido subyacente para indicarle al usuario que hay disponible una acción de gesto.

Componentes interactivos

Para habilitar gestos en un control interactivo, como un botón, crea una configuración que especifique OneHandedGestureAction.Primary y aplica el modificador oneHandedGesture. Pasa el mismo MutableInteractionSource al control y al modificador para que los eventos de gestos emitan comentarios visuales de presión en el control.

Para habilitar el indicador de gestos, crea y recuerda una instancia de OneHandedGestureClickIndicatorState. Luego, para activar la respuesta visual, llama a showIndicator dentro de la devolución de llamada onGestureAvailable proporcionada por el modificador oneHandedGesture, que indica al sistema que se produjo un evento de indicación. Una vez que se llama, el componente reemplaza brevemente su contenido normal por una animación de gesto.

var isPlaying by remember { mutableStateOf(false) }
val onClick = { isPlaying = !isPlaying }

val gestureConfig = rememberOneHandedGestureConfiguration(
    action = OneHandedGestureAction.Primary
)
val indicatorState = remember { OneHandedGestureClickIndicatorState() }
val coroutineScope = rememberCoroutineScope()
val interactionSource = remember { MutableInteractionSource() }

Button(
    onClick = onClick,
    interactionSource = interactionSource,
    modifier = Modifier
        .fillMaxWidth()
        .oneHandedGesture(
            gestureConfiguration = gestureConfig,
            interactionSource = interactionSource,
            onGestureLabel = if (isPlaying) "pause" else "play",
            onGestureAvailable = { coroutineScope.launch { indicatorState.showIndicator() } },
            onGesture = onClick
        )
) {
    OneHandedGestureClickIndicator(
        gestureConfiguration = gestureConfig,
        state = indicatorState
    ) {
        Text(if (isPlaying) "Pause" else "Play", modifier = Modifier.fillMaxWidth())
    }
}

Contenedores desplazables

Para las pantallas o listas desplazables, crea una configuración que especifique OneHandedGestureAction.Primary y aplica el modificador oneHandedGesture a tu contenedor, llamando a un asistente de desplazamiento, como scrollDown.

Para proporcionar comentarios visuales sobre las acciones de desplazamiento, puedes usar OneHandedGestureScrollIndicator. Este componente funciona como un indicador de desplazamiento estándar que muestra la posición de desplazamiento, pero también puede indicar que hay un gesto de desplazamiento disponible para el usuario. Por lo general, este indicador se pasa a la ranura scrollIndicator de un ScreenScaffold y se vincula con el estado de un contenedor desplazable, como un TransformingLazyColumn. También observa un OneHandedGestureScrollIndicatorState para administrar sus transiciones visuales.

Para activar la respuesta visual, llama a showIndicator en este estado, por lo general, dentro de la devolución de llamada onGestureAvailable del modificador oneHandedGesture. Una vez que se activa, el indicador reemplaza temporalmente su estado visual estándar por una secuencia de animación de gestos para alertar al usuario.

val scrollState = rememberTransformingLazyColumnState()
val gestureConfig = rememberOneHandedGestureConfiguration(
    action = OneHandedGestureAction.Primary,
    priority = OneHandedGesturePriority.Scrollable
)
val indicatorState = remember(gestureConfig) { OneHandedGestureScrollIndicatorState() }
val coroutineScope = rememberCoroutineScope()

ScreenScaffold(
    scrollState = scrollState,
    scrollIndicator = {
        OneHandedGestureScrollIndicator(
            gestureConfiguration = gestureConfig,
            indicatorState = indicatorState,
            scrollState = scrollState,
            modifier = Modifier.align(Alignment.CenterEnd)
        )
    }
) { contentPadding ->
    TransformingLazyColumn(
        state = scrollState,
        contentPadding = contentPadding,
        modifier = Modifier
            .fillMaxSize()
            .oneHandedGesture(
                gestureConfiguration = gestureConfig,
                onGestureLabel = "scroll",
                onGestureAvailable = {
                    coroutineScope.launch { indicatorState.showIndicator() }
                },
                onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) }
            )
    ) {
        items(10) { index ->
            Text("Item $index", modifier = Modifier.padding(8.dp))
        }
    }
}

Cómo combinar varios gestos

Puedes configurar un gesto de desplazamiento y un gesto de clic con la misma acción principal agregando gesturePriority a tu objeto OneHandedGestureConfiguration:

  • OneHandedGesturePriority.Clickable (más alta): Se asigna a los controles interactivos, como los que tienen el tipo Button o Card, para que capturen gestos cuando estén visibles en la pantalla.
  • OneHandedGesturePriority.Scrollable (medio): Asigna a contenedores desplazables o paginables para que cedan a elementos secundarios en los que se puede hacer clic, pero se desplacen cuando no se vea ningún control en el que se pueda hacer clic.
  • OneHandedGesturePriority.Unspecified (más baja): Es una prioridad sin asignar. Este es el valor predeterminado para un gesto que no tiene un priority establecido.

Si se configura de forma explícita priority = OneHandedGesturePriority.Clickable en un botón interno y priority = OneHandedGesturePriority.Scrollable en su lista principal, el sistema puede mostrar este comportamiento de prioridad de gestos. Cuando el usuario activa la acción principal con el gesto de una mano, primero se desplaza la lista hacia abajo hasta que se ve el botón y, luego, se captura la acción de clic del botón.

Cómo probar y depurar gestos con ADB

Puedes probar los gestos con una mano en un dispositivo físico o en un emulador sin realizar movimientos físicos de la muñeca con Android Debug Bridge (adb) y el servicio del sistema IWearGestureService.

Habilita la simulación de gestos

Antes de simular gestos con ADB, configura la configuración del dispositivo y las anulaciones de restricciones:

  1. Verifica que tu dispositivo Wear OS ejecute Wear OS 7 (nivel de API 37) o una versión posterior:

    adb shell getprop ro.build.version.sdk
    
  2. Si realizas pruebas en un dispositivo físico que no está en tu muñeca o que está en un cargador, anula la restricción de fuera del cuerpo para que el framework de gestos permanezca activo:

    adb shell cmd IWearGestureService override-constraints offbody-state
    

Cómo activar eventos de gestos con ADB

Para simular el gesto de pellizcar dos veces (que es la acción Primary en los relojes Pixel), ejecuta el siguiente comando de shell de ADB:

adb shell cmd IWearGestureService gesture DoublePinch

Para simular el gesto de giro de muñeca (que es la acción Dismiss en los relojes Pixel), ejecuta el siguiente comando de shell de ADB:

adb shell cmd IWearGestureService gesture WristTurn

Cómo restablecer el seguimiento de sugerencias de gestos

El sistema hace un seguimiento del historial de interacción del usuario y muestra sugerencias de gestos flotantes según el parámetro de configuración de cadencia global (como Siempre o Diariamente). Cuando depures los indicadores de gestos de tu app, restablece este historial de seguimiento para que vuelvan a aparecer las sugerencias de tu paquete:

  • En compilaciones o emuladores de userdebug:

    adb shell cmd IWearGestureService hint clear <your_package_name>
    
  • En compilaciones de venta minorista (user):

    En los dispositivos comerciales sin acceso de administrador, hint clear está bloqueado por los permisos del sistema. Borra los datos locales de la app para restablecer el descubrimiento de sugerencias:

    adb shell pm clear <your_package_name>
    

Restablece las restricciones predeterminadas

Para restablecer todas las anulaciones de restricciones de depuración cuando termines de realizar las pruebas, haz lo siguiente:

adb shell cmd IWearGestureService override-constraints reset

Soluciona problemas relacionados con la inyección de gestos

Si tu app no recibe gestos simulados, haz lo siguiente:

  1. Verifica que la pantalla del reloj esté activa y ENCENDIDA. El framework de gestos no envía gestos a las aplicaciones mientras la pantalla está apagada o en modo ambiente. Para activar la pantalla con ADB, ejecuta lo siguiente:

    adb shell input keyevent KEYCODE_WAKEUP
    
  2. Comprueba si tu app está registrada como suscriptora de gestos activa y si actualmente tiene el enfoque de la ventana:

    adb shell cmd IWearGestureService get-active-gestures -readable
    

    Cuando la pantalla de gestos de tu app está en primer plano y la pantalla está activa, este comando devuelve [DoublePinch] o [DoublePinch, WristTurn]. Si se devuelve una lista vacía ([]), verifica si la ventana tiene el enfoque o si las restricciones fuera del cuerpo bloquean la activación.

  3. Inspecciona el estado del servicio de gestos interno y los tokens de suscriptor activos:

    adb shell dumpsys IWearGestureService
    

Recursos adicionales

Para obtener orientación sobre el diseño de cuándo y dónde usar gestos con una mano, consulta Gestos con una mano.