Saltar al contenido principal

Entrada de usuario y accesibilidad

Una aplicación verdaderamente adaptativa también maneja las diferencias en cómo funciona la entrada del usuario y también los programas para ayudar a las personas con problemas de accesibilidad.

No basta con adaptar cómo se ve tu aplicación, también tienes que admitir una variedad de entradas de usuario. El mouse y el teclado introducen tipos de entrada más allá de los que se encuentran en un dispositivo táctil, como la rueda de desplazamiento, el clic derecho, interacciones de desplazar el cursor (hover), recorrido por Tab y atajos de teclado.

Algunas de estas características funcionan de forma predeterminada en los Widgets de Material. Pero, si has creado un Widget personalizado, es posible que necesites implementarlas directamente.

Algunas características que abarcan una aplicación bien diseñada también ayudan a los usuarios que trabajan con tecnologías de asistencia. Por ejemplo, aparte de ser un buen diseño de aplicación, algunas características, como el recorrido por Tab y los atajos de teclado, son críticas para los usuarios que trabajan con dispositivos de asistencia. Además de los consejos estándar para crear aplicaciones accesibles, esta página cubre información para crear aplicaciones que sean tanto adaptativas como accesibles.

Rueda de desplazamiento para widgets personalizados

#

Los Widgets de desplazamiento como ScrollView o ListView admiten la rueda de desplazamiento de forma predeterminada, y dado que casi cualquier Widget personalizado desplazable está construido usando uno de estos, también funciona con ellos.

Si necesitas implementar un comportamiento de desplazamiento personalizado, puedes usar el Widget Listener, que te permite personalizar cómo reacciona tu UI a la rueda de desplazamiento.

dart
return Listener(
  onPointerSignal: (event) {
    if (event is PointerScrollEvent) print(event.scrollDelta.dy);
  },
  child: ListView(),
);

Recorrido por Tab e interacciones de foco

#

Los usuarios con teclados físicos esperan poder usar la tecla Tab para navegar rápidamente por una aplicación, y los usuarios con diferencias motoras o visuales a menudo dependen completamente de la navegación por teclado.

Hay dos consideraciones para las interacciones con Tab: cómo se mueve el foco de Widget a Widget, conocido como recorrido (traversal), y el resaltado visual que se muestra cuando un Widget está enfocado.

La mayoría de los componentes integrados, como botones y campos de texto, admiten recorrido y resaltados de forma predeterminada. Si tienes tu propio Widget que quieres incluir en el recorrido, puedes usar el Widget FocusableActionDetector para crear tus propios controles. El Widget FocusableActionDetector es útil para combinar foco, entrada de mouse y atajos en un solo Widget. Puedes crear un detector que defina acciones y vinculaciones de teclas, y proporcione callbacks para gestionar el foco y los resaltados al desplazar el cursor (hover).

dart
class _BasicActionDetectorState extends State<BasicActionDetector> {
  bool _hasFocus = false;
  @override
  Widget build(BuildContext context) {
    return FocusableActionDetector(
      onFocusChange: (value) => setState(() => _hasFocus = value),
      actions: <Type, Action<Intent>>{
        ActivateIntent: CallbackAction<Intent>(
          onInvoke: (intent) {
            print('Enter or Space was pressed!');
            return null;
          },
        ),
      },
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          const FlutterLogo(size: 100),
          // Position focus in the negative margin for a cool effect
          if (_hasFocus)
            Positioned(
              left: -4,
              top: -4,
              bottom: -4,
              right: -4,
              child: _roundedBorder(),
            ),
        ],
      ),
    );
  }
}

Controlar el orden de recorrido

#

Para obtener más control sobre el orden en que se enfocan los Widgets cuando el usuario navega con Tab, puedes usar FocusTraversalGroup para definir secciones del árbol que deben tratarse como un grupo al presionar Tab.

Por ejemplo, podrías querer navegar con Tab a través de todos los campos en un formulario antes de pasar con Tab al botón de enviar:

dart
return Column(
  children: [
    FocusTraversalGroup(child: MyFormWithMultipleColumnsAndRows()),
    SubmitButton(),
  ],
);

Flutter tiene varias formas integradas de recorrer Widgets y grupos, utilizando por defecto la clase ReadingOrderTraversalPolicy. Esta clase suele funcionar bien, pero es posible modificar esto usando otra clase TraversalPolicy predefinida o creando una política personalizada.

Aceleradores de teclado

#

Además del recorrido por Tab, los usuarios de escritorio y web están acostumbrados a tener varios atajos de teclado vinculados a acciones específicas. Ya sea la tecla Delete para eliminaciones rápidas o Control+N para un nuevo documento, asegúrate de considerar los diferentes aceleradores que esperan tus usuarios. El teclado es una herramienta de entrada potente, así que intenta exprimirle toda la eficiencia que puedas. ¡Tus usuarios lo agradecerán!

Los aceleradores de teclado se pueden lograr de varias formas en Flutter, dependiendo de tus objetivos.

Si tienes un solo Widget como un TextField o un Button que ya tiene un nodo de foco, puedes envolverlo en un Widget KeyboardListener o un Widget Focus y escuchar eventos de teclado:

dart
  @override
  Widget build(BuildContext context) {
    return Focus(
      onKeyEvent: (node, event) {
        if (event is KeyDownEvent) {
          print(event.logicalKey);
        }
        return KeyEventResult.ignored;
      },
      child: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 400),
        child: const TextField(
          decoration: InputDecoration(border: OutlineInputBorder()),
        ),
      ),
    );
  }
}

Para aplicar un conjunto de atajos de teclado a una sección grande del árbol, usa el Widget Shortcuts:

dart
// Define a class for each type of shortcut action you want
class CreateNewItemIntent extends Intent {
  const CreateNewItemIntent();
}

Widget build(BuildContext context) {
  return Shortcuts(
    // Bind intents to key combinations
    shortcuts: const <ShortcutActivator, Intent>{
      SingleActivator(LogicalKeyboardKey.keyN, control: true):
          CreateNewItemIntent(),
    },
    child: Actions(
      // Bind intents to an actual method in your code
      actions: <Type, Action<Intent>>{
        CreateNewItemIntent: CallbackAction<CreateNewItemIntent>(
          onInvoke: (intent) => _createNewItem(),
        ),
      },
      // Your sub-tree must be wrapped in a focusNode, so it can take focus.
      child: Focus(autofocus: true, child: Container()),
    ),
  );
}

El Widget Shortcuts es útil porque solo permite que se activen atajos cuando este árbol de widgets o uno de sus hijos tiene el foco y es visible.

La última opción es un listener global. Este listener se puede usar para atajos globales siempre activos en toda la aplicación o para paneles que pueden aceptar atajos siempre que estén visibles (independientemente de su estado de foco). Agregar listeners globales es fácil con HardwareKeyboard:

dart
@override
void initState() {
  super.initState();
  HardwareKeyboard.instance.addHandler(_handleKey);
}

@override
void dispose() {
  HardwareKeyboard.instance.removeHandler(_handleKey);
  super.dispose();
}

Para verificar combinaciones de teclas con el listener global, puedes usar el conjunto HardwareKeyboard.instance.logicalKeysPressed. Por ejemplo, un método como el siguiente puede verificar si cualquiera de las teclas proporcionadas se mantiene presionada:

dart
static bool isKeyDown(Set<LogicalKeyboardKey> keys) {
  return keys
      .intersection(HardwareKeyboard.instance.logicalKeysPressed)
      .isNotEmpty;
}

Combinando estas dos cosas, puedes activar una acción cuando se presiona Shift+N:

dart
bool _handleKey(KeyEvent event) {
  bool isShiftDown = isKeyDown({
    LogicalKeyboardKey.shiftLeft,
    LogicalKeyboardKey.shiftRight,
  });

  if (isShiftDown && event.logicalKey == LogicalKeyboardKey.keyN) {
    _createNewItem();
    return true;
  }

  return false;
}

Una nota de precaución al usar el listener estático es que a menudo necesitas desactivarlo cuando el usuario está escribiendo en un campo o cuando el Widget con el que está asociado está oculto a la vista. A diferencia de Shortcuts o KeyboardListener, esta es tu responsabilidad de gestionar. Esto puede ser especialmente importante cuando estás vinculando un acelerador Delete/Backspace para Delete, pero luego tienes TextFields hijos en los que el usuario podría estar escribiendo.

Entrada, salida y desplazar el cursor (hover) del mouse para widgets personalizados

#

En escritorio, es común cambiar el cursor del mouse para indicar la funcionalidad sobre el contenido sobre el que el mouse se encuentra desplazando (hover). Por ejemplo, típicamente ves un cursor de mano cuando pasas el cursor sobre un botón, o un cursor I cuando pasas el cursor sobre texto.

Los botones de Material de Flutter manejan estados de foco básicos para cursores de botón y texto estándar. (Una excepción notable es si cambias el estilo predeterminado de los botones de Material para establecer el overlayColor a transparente).

Implementa un estado de foco para cualquier botón personalizado o detector de gestos en tu aplicación. Si cambias los estilos predeterminados de los botones de Material, prueba los estados de foco del teclado e implementa los tuyos si es necesario.

Para cambiar el cursor desde dentro de tus Widgets personalizados, usa MouseRegion:

dart
// Show hand cursor
return MouseRegion(
  cursor: SystemMouseCursors.click,
  // Request focus when clicked
  child: GestureDetector(
    onTap: () {
      Focus.of(context).requestFocus();
      _submit();
    },
    child: Logo(showBorder: hasFocus),
  ),
);

MouseRegion también es útil para crear efectos personalizados al pasar el cursor (rollover y hover):

dart
return MouseRegion(
  onEnter: (_) => setState(() => _isMouseOver = true),
  onExit: (_) => setState(() => _isMouseOver = false),
  onHover: (e) => print(e.localPosition),
  child: Container(
    height: 500,
    color: _isMouseOver ? Colors.blue : Colors.black,
  ),
);

Para un ejemplo que cambia el estilo del botón para delinear el botón cuando tiene el foco, revisa el código de botones para la aplicación Wonderous. La aplicación modifica la propiedad FocusNode.hasFocus para verificar si el botón tiene el foco y, de ser así, agrega un contorno.

Densidad visual

#

Podrías considerar ampliar el "área de impacto" (hit area) de un Widget para dar cabida a una pantalla táctil, por ejemplo.

Diferentes dispositivos de entrada ofrecen varios niveles de precisión, lo que requiere áreas de impacto de diferentes tamaños. La clase VisualDensity de Flutter facilita ajustar la densidad de tus vistas en toda la aplicación, por ejemplo, haciendo que un botón sea más grande (y por lo tanto más fácil de tocar) en un dispositivo táctil.

Cuando cambias la VisualDensity para tu MaterialApp, los MaterialComponents que lo admiten animan sus densidades para coincidir. De forma predeterminada, tanto la densidad horizontal como la vertical están configuradas en 0.0, pero puedes configurar las densidades en cualquier valor negativo o positivo que desees. Al alternar entre diferentes densidades, puedes ajustar fácilmente tu UI.

Scaffold adaptativo

Para configurar una densidad visual personalizada, inyecta la densidad en el tema de tu MaterialApp:

dart
double densityAmt = touchMode ? 0.0 : -1.0;
VisualDensity density = VisualDensity(
  horizontal: densityAmt,
  vertical: densityAmt,
);
return MaterialApp(
  theme: ThemeData(visualDensity: density),
  home: MainAppScaffold(),
  debugShowCheckedModeBanner: false,
);

Para usar VisualDensity dentro de tus propias vistas, puedes buscarlo:

dart
VisualDensity density = Theme.of(context).visualDensity;

El contenedor no solo reacciona automáticamente a los cambios en la densidad, sino que también se anima cuando cambia. Esto une tus componentes personalizados, junto con los componentes integrados, para un efecto de transición fluido en toda la aplicación.

Como se muestra, VisualDensity no tiene unidades, por lo que puede significar cosas diferentes para diferentes vistas. En el siguiente ejemplo, 1 unidad de densidad equivale a 6 píxeles, pero esto depende totalmente de ti decidirlo. El hecho de que no tenga unidades lo hace bastante versátil, y debería funcionar en la mayoría de los contextos.

Vale la pena señalar que Material generalmente usa un valor de alrededor de 4 píxeles lógicos para cada unidad de densidad visual. Para obtener más información sobre los componentes compatibles, consulta la API VisualDensity. Para obtener más información sobre los principios de densidad en general, consulta la guía de Material Design.