Hoy vamos con un post en el que aprenderemos a crear una app para todos los públicos, teniendo en cuenta ciertos aspectos para conseguir una aplicación accesible.

Imagen de una persona con discapacidad visual escribiendo en un teclado adaptado

Para enseñar los distintos aspectos que debe cumplir una app accesible, hemos construido un ejemplo en la que se implementan los puntos a tener en cuenta, entre ellos, tamaño de fuente, colores, uso de imágenes, indicadores de actividad, etc…

Tamaño de fuentes

En Xamarin.Forms existen distintos tamaños de fuente preestablecidos. Es recomendable utilizarlos ya que, de esta forma, las fuentes ajustarán su tamaño al definido por el usuario en su configuración de accesibilidad. Alguno de ellos son Title, Subtitle, Large, Header…

En la siguiente imagen se muestran dos dispositivos con distinta configuración de accesibilidad. El de la derecha tiene deshabilitada la opción Texto más grande. Por el contrario, a la izquierda se muestra un dispositivo en el que el usuario tiene configurado un tamaño de texto muy grande. Se aprecia que todos los label amplían su tamaño, menos el último, cuyo texto es Fixed text, debido a que en éste no se han utilizado los tamaños de fuente preestablecidos, sino que se ha fijado a 20.

Imagen de dos dispositivos. En ambos se muestran varias etiquetas, pero tienen tamaños distintos porque, el de la izquierda, utiliza la opción de accesibilidad para ampliar el tamaño de los textos

A la hora de utilizar los tamaños de fuente prefijados, hay que tener en cuenta que estos no tienen los mismos valores en las distintas plataformas, por lo que, es posible, que tengamos que hacer ciertas personalizaciones en nuestra app para cada una de las plataformas.

A continuación, se muestra el tamaño de todas estas fuentes:

Tabla en la que se muestra el nombre y los tamaños de fuente de Xamarin en las distintas plataformas

Alto contraste

A la hora de diseñar las pantallas de nuestra aplicación, es importante elegir colores con un alto contraste para facilitar la lectura.

Imagen con dos frames negros. Sobre uno se muestra un texto de color verde oscuro, muy difícil de leer. En otro el texto es blanco, lo que facilita la lectura

En la imagen anterior, se muestran dos casos muy distintos. Aunque la información y los controles utilizados son los mismos, hay un caso en el que la lectura se complica debido al bajo contraste existente entre el color de fondo y el de la fuente. Una persona con una discapacidad visual, sin tener porque se invidente, no será capaz de leer el texto.

Cuidado con los colores

En el punto anterior, ya hemos hablado sobre la necesidad de usar colores con un alto contraste para tener una interfaz más amigable. Pero a la hora de elegir colores, no solo tenemos que tener en cuenta el contraste, hay que tener cuidado al elegir un color ya que no todos vemos los colores de la misma manera. Un caso extremos serían los daltónicos, que pueden confundir dos colores que para otra persona está muy bien diferenciados.

En los casos en los que un color esté aportando cierta información, es una buena práctica usar, no solo el color, sino una imagen que también sirva para aportar esa información añadida, tal y como se muestra en la siguiente imagen.

Imagen con dos frames que muestran nombres de personas. En el primero, el nombre de los hombres está en rojo y las mujeres en verde. En la segunda, además, se incluye un icono de un hombre y una mujer, respectivamente

Tamaño de botones

Nuestras aplicaciones suelen requerir que el usuario interactúe con la interfaz. Es deseable que pueda hacerlo de una forma sencilla, por tanto, el tamaño de los botones tiene que ser lo suficientemente grande para ser pulsado con facilidad. Además, los botones tienen que tener una separación mínima entre ellos, de lo contrario, es posible que el usuario realice acciones queriendo hacer otras diferentes.

Imagen con 3 botones. Uno pequeño. Otro grande. Y grande, a pesar de tener una imagen pequeña.

En la imagen anterior, se aprecia que el primer botón es muy pequeño, por lo que, para muchos usuarios será muy complicado pulsarlo. En este punto se suele ironizar diciendo que hay que hacer botones para gente con dedos gordos, pero tenemos que tener presente que, además de gente con manos muy grandes, hay personas con problemas de movilidad en las manos a las que se les hace muy complicado pulsar un botón si éste es demasiado pequeño.

El último botón del ejemplo muestra un truco que se puede usar cuando tenemos que mostrar un botón con una imagen un tanto pequeña (cuestión de diseño). Para aumentar la zona de acción de ese botón, se ha añadido un padding. De está manera, aunque la imagen es pequeña, el botón es lo suficientemente grande como para ser pulsado.

<StackLayout Spacing="48" 
             HorizontalOptions="Center" VerticalOptions="Center">
    <ImageButton x:Name="IbSmall" Source="ok.png"
                    Aspect="AspectFit"
                    BorderColor="Green"
                    BorderWidth="1"
                    BackgroundColor="Transparent"
                    HeightRequest="24" WidthRequest="24" 
                    HorizontalOptions="Center"/>
    <ImageButton x:Name="IbNormal" Source="ok.png"
                    Aspect="AspectFit"
                    BorderColor="Green"
                    BorderWidth="1"
                    BackgroundColor="Transparent"
                    HeightRequest="48" WidthRequest="48" />
    <ImageButton x:Name="IbWithPadding" Source="ok.png"
                    Aspect="AspectFit"
                    BorderColor="Green"
                    BorderWidth="1"
                    BackgroundColor="Transparent"
                    Padding="12"
                    HeightRequest="48" WidthRequest="48" />
</StackLayout>

Texto alternativo en imágenes

Las aplicaciones que creamos, suelen tener imágenes. Muchas veces, esas imágenes no aportan valor y están ahí solamente por una cuestión de diseño, para que nuestra app sea más atractiva. Pero otras veces, las imágenes sí que aportan un valor que las personas invidentes se están perdiendo. Por esa razón, hay que asociar un texto alternativo a esas imágenes de manera que los lectores de interfaz puedan interpretarlo y ofrecer el mismo valor tanto si el usuario puede, o no puede, ver la imagen.

<Image Aspect="AspectFit"
       AutomationProperties.HelpText="Imagen del logo de DevsDNA"
       Source="https://www.devsdna.com/wp-content/uploads/2020/05/logo_03.png"
       HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"/>

Conseguir este propósito, es tan sencillo como informar la propiedad AutomationProperties.HelpText a la imagen, tal y como se muestra en el siguiente fragmento de código.

Descripción de interfaz

Llegados a este punto podemos pensar: Está bien tener un texto alternativo para las imágenes, pero ¿qué pasa con el resto de controles? ¿Cómo va a interactuar alguien ciego, o con muy poca visión, con nuestra aplicación? La solución es sencilla y, no es otra que, utilizar las funcionalidades que Xamarin Forms pone a nuestro alcance para conseguir este objetivo. Estás son las siguientes propiedades adjuntas, que podemos utilizar en todos los controles:

  • AutomationProperties.IsInAccessibletree: Booleano que sirve para indicar si el control está en el árbol de accesibilidad. Todos los controles que, no sean exclusivos de diseño, deben estar en el árbol. Por ejemplo, un entry, un button, un label, etc… Por el contrario, un shape que utilizamos para cumplir con los requisitos de diseño, una imagen de fondo, etc… no debe estar en este árbol, para no aportar información innecesaria al usuario.
  • AutomationProperties.Name: Texto que, el lector de pantalla, utilizará para dar una descripción corta del control.
  • AutomationProperties.HelpText: Texto con la descripción larga, utilizada por el lector de pantalla.
Imagen con 2 interfaces idénticas en las que, visualmente, no se aprecia diferencia

En la imagen anterior, aunque ambos frames muestran lo mismo, el primero no utiliza las propiedades de accesibilidad, por lo que, para las personas invidentes, será más complicado interactuar con éste que con el segundo.

A continuación, se muestra el código XAML utilizado para crear el segundo de los frames, que sí que cumple con los requisitos de accesibilidad.

<StackLayout>
    <Label Text="Interfaz correcta" 
            FontSize="Subtitle" 
            HorizontalTextAlignment="Center"/>

    <Label Text="Nombre" AutomationProperties.IsInAccessibleTree="False"/>
    <Entry AutomationProperties.Name="Nombre" AutomationProperties.HelpText="Introduzca su nombre"/>

    <Label Text="Género" AutomationProperties.IsInAccessibleTree="False"/>
    <Picker AutomationProperties.Name="Género" AutomationProperties.HelpText="Seleccione su genero">
        <Picker.Items>
            <x:String>Hombre</x:String>
            <x:String>Mujer</x:String>
        </Picker.Items>
    </Picker>

    <Grid ColumnDefinitions="*, *">
        <Button Grid.Column="0" Text="Cancelar" AutomationProperties.HelpText="Cancelar el formulario de datos"/>
        <Image Grid.Column="1" Source="ok.png" HeightRequest="50" AutomationProperties.Name="Aceptar" AutomationProperties.HelpText="Aceptar datos introducidos"/>
    </Grid>
</StackLayout>

En éste código vemos lo sencillo que es usar las propiedades adjuntas que nos ofrece Xamarin Forms.

Navegación por pantalla

Como desarrolladores, cuando estamos creando una interfaz, nuestra intención es guiar al usuario a través de ella, que la recorra con cierto orden. Por ejemplo, si creamos un formulario de registro, queremos que rellene los campos en un orden concreto. Para conseguir este objetivo, nos apoyamos en un diseño visual que guíe al usuario de la manera que deseamos, pero ¿qué pasa si el usuario no puede ver nuestra interfaz? ¿de qué manera podemos guiarle por ella? Los lectores de pantalla navegan por los controles de la pantalla, pero, en muchas ocasiones, ese orden de navegación no es el deseado. Para conseguirlo, en Xamarin Forms, existe la propiedad TabIndex, con la que podemos customizar el orden de navegación a través de los controles. De esta manera, los lectores de pantalla navegarán por la interfaz de la manera que nosotros, como desarrolladores, deseemos.

En el siguiente ejemplo, se muestra cómo se ha modificado la navegación por defecto para conseguir que el paso a través de los controles sea diferente, satisfaciendo nuestras necesidades.

Imagen que muestra una interfaz con 6 entries, colocados en forma de tabla, con 2 filas y 3 columnas.
<Grid RowDefinitions="Auto, Auto, Auto"
      ColumnDefinitions="*, *, *">

    <Label Grid.ColumnSpan="3" 
            Text="Interfaz correcta" 
            FontSize="Subtitle" 
            HorizontalTextAlignment="Center"/>

    <Entry Grid.Row="1" Grid.Column="0" Placeholder="1" TabIndex="1" />
    <Entry Grid.Row="1" Grid.Column="1" Placeholder="3" TabIndex="3"/>
    <Entry Grid.Row="1" Grid.Column="2" Placeholder="5" TabIndex="5"/>
    <Entry Grid.Row="2" Grid.Column="0" Placeholder="2" TabIndex="2"/>
    <Entry Grid.Row="2" Grid.Column="1" Placeholder="4" TabIndex="4"/>
    <Entry Grid.Row="2" Grid.Column="2" Placeholder="6" TabIndex="6"/>
</Grid>

Si no se hubiera modificado el TabIndex de los distintos controles, los lectores de pantalla navegarían por los entries 1, 3, 5, 2, 4 y 6, en ese orden.

Describiendo estado del proceso

En este punto vamos a plantear un nuevo problema a tener en cuenta. Nuestra aplicaciones, muchas veces, ejecutan procesos que les ocupan un tiempo, determinado o indeterminado. Para indicar al usuario que un proceso se está realizando, se utilizan controles, como ActivityIndicator o ProgressBar. Pero, si el usuario no ve la pantalla, ¿cómo le informamos de que un proceso se ha iniciado? ¿o de que ya ha terminado? Para conseguir este objetivo, no tenemos ninguna propiedad, en los controles antes mencionados, que reconozcan los lectores de pantalla y sirvan para indicar al usuario final el estado del proceso.

Por tanto, es necesario realizar una implementación nativa en las distintas plataformas que implementen una interfaz, con los método que vamos a necesitar desde el proyecto común.

La interfaz tendrá solamente 2 métodos. Uno que indica si el lector de pantalla está activo y otro que reproduce un mensaje.

public interface IAccessibilityService
{
    bool IsVoiceAssistanceActive();
    void PlayAudio(string message);
}

Las implementaciones son las siguientes, para iOS y Android, respectivamente.

[assembly: Xamarin.Forms.Dependency(typeof(AccessibilityService))]
namespace XamarinAccessibility.iOS.Services
{
    public class AccessibilityService : IAccessibilityService
    {
        [DllImport(Constants.UIKitLibrary, EntryPoint = "UIAccessibilityPostNotification")]
        public extern static void PostNotification(uint notification, IntPtr id);

        public bool IsVoiceAssistanceActive()
        {
            return UIAccessibility.IsVoiceOverRunning;
        }

        public void PlayAudio(string message)
        {
            if (IsVoiceAssistanceActive())
                PostNotification(1008, new NSString(message).Handle);
        }
    }
}
[assembly: Xamarin.Forms.Dependency(typeof(XamarinAccessibility.Droid.Services.AccessibilityService))]
namespace XamarinAccessibility.Droid.Services
{
    public class AccessibilityService : IAccessibilityService
    {
        public bool IsVoiceAssistanceActive()
        {
            AccessibilityManager am = (AccessibilityManager)Android.App.Application.Context.GetSystemService(Context.AccessibilityService);
            if (am != null & am.IsEnabled)
                return am.IsTouchExplorationEnabled;
             
            return false;
        }

        public void PlayAudio(string message)
        {
            if (IsVoiceAssistanceActive())
                Toast.MakeText(Android.App.Application.Context, message, ToastLength.Short).Show();
        }
    }
}

Con está funcionalidad desarrollada, estamos en condiciones de implementar una interfaz como la que se muestra a continuación. E informar a usuarios, con lector de pantalla activo, de lo que está sucediendo en pantalla.

Gif en el que se muestran in indicador de actividad y una barra de progreso después de pulsar un botón

A continuación se muestra el diseño de la interfaz y el code behind, desde el que se realiza la reproducción de mensajes, llamando a los métodos de la interfaz IAccessibilityService, que hemos visto anteriormente.

<Grid RowDefinitions="50, 50"
      Margin="20"
      HorizontalOptions="FillAndExpand" VerticalOptions="Center">
    <Button x:Name="BtnUndefinedAction" Text="Proceso indefinido" Clicked="BtnUndefinedAction_Clicked" />
    <ActivityIndicator x:Name="AiUndefinedAction" Color="Red" IsRunning="False" IsVisible="False" />

    <Button x:Name="BtnDefinedAction" Grid.Row="1" 
            Text="Proceso definido" Clicked="BtnDefinedAction_Clicked" />
    <ProgressBar x:Name="PbDefinedAction" Grid.Row="1" 
                    BackgroundColor="Gray" ProgressColor="Red" IsVisible="False" Progress="0"/>
</Grid>
public partial class AccessibleActionsView : ContentPage
{
    private readonly IAccessibilityService accessibilityService;

    public AccessibleActionsView()
    {
        accessibilityService = DependencyService.Get<IAccessibilityService>();
        InitializeComponent();
    }

    private async void BtnUndefinedAction_Clicked(object sender, EventArgs e)
    {
        accessibilityService.PlayAudio("Iniciando proceso");
        BtnUndefinedAction.IsVisible = false;
        AiUndefinedAction.IsRunning = true;
        AiUndefinedAction.IsVisible = true;
        
        await Task.Delay(3000);
        accessibilityService.PlayAudio("Proceso finalizado");

        BtnUndefinedAction.IsVisible = true;
        AiUndefinedAction.IsRunning = false;
        AiUndefinedAction.IsVisible = false;
    }

    private async void BtnDefinedAction_Clicked(object sender, EventArgs e)
    {
        accessibilityService.PlayAudio("Iniciando proceso");
        BtnDefinedAction.IsVisible = false;
        PbDefinedAction.IsVisible = true;
        PbDefinedAction.Progress = 0;
        await Task.Delay(3000);

        for (int i = 0; i < 5; i++)
        {
            double progress = 0.25 * i;
            PbDefinedAction.Progress = progress;
            accessibilityService.PlayAudio($"{progress * 100} porciento completado");
            await Task.Delay(3000);
        }
        
        accessibilityService.PlayAudio("Proceso finalizado");
        
        BtnDefinedAction.IsVisible = true;
        PbDefinedAction.IsVisible = false;
        PbDefinedAction.Progress = 0;
    }
}

Como has podido comprobar, no es complicado implementar aplicaciones accesibles, simplemente hay que tener en cuenta a todos los públicos desde el inicio de un desarrollo. El tiempo de implementación apenas se verá incrementado y, con ello, conseguimos llegar al 100% del público objetivo.

Por último, aquí tienes el enlace al repositorio de código en el que se implementan todos los puntos que se han mostrado en este post: https://github.com/DevsDNA/Samples/tree/master/XamarinAccessibility

En DevsDNA, nos preocupa la accesibilidad. Por eso, intentamos que todo el mundo pueda disfrutar de nuestras apps. El mejor ejemplo de ello es nuestra app corporativa. La podéis descargar para iOS y Android en los siguientes enlaces:

Además, todo el código de nuestra app está disponible en https://github.com/DevsDNA/DevsDNA-Application

Leave a Reply