2. CAS D'USAGE (iOS ET ANDROID)


Table des matières

Cette section présente les différents cas d’utilisation de la plupart des contrôles offerts sur les plateformes Android  de Google et iOS  d’Apple.

La plupart du temps, le cas d’usage s’appliquera aux deux plateformes; sinon, l’utilisateur en sera avisé.

2.1 Navigation

Des cas d’usage portant sur la navigation courants dans les applications de Radio-Canada.

2.1.1 Barre de navigation

Cette section décrit les changements qui ont dus être apportés à la barre de navigation (TopBar) de ICI Tou Tv V4 afin de la conformer aux normes décrites à la section 1.1.2.

a)Bouton “Retour”

Le bouton qui permet de retourner à l’écran parent doit avoir un libellé précis. Voici comment nous y sommes pris pour l’implémenter sur ICI Tou Tv V4.

IOS 

À venir

ANDROID

Il a fallut d'abord définir un format de libellé pour le bouton Retour:

<string name="a11y_top_bar_back_button_label_format">Retour à l'ecran %s</string>


@Bindable val title = ObservableField<String>()
@Bindable val parentTitle = ObservableField<String>()
fun updateTitle(title: String?) {
    parentTitle.set(this.title.get() ?: resourceService.getString(R.string.menu_home))
    this.title.set(title)
}
<ImageButton
    android:id="@+id/back_button"
    android:layout_width="48dp"
    android:layout_height="?attr/actionBarSize"
    android:background="@color/transparent"
    android:contentDescription="@{@string/a11y_top_bar_back_button_label_format(vmToolbar.parentTitle)}"
    android:onClick="@{_ -> vmToolbar.onBackPressed()}"
    android:src="@drawable/rc_ui_arrow_w"
    android:tint="@color/white"
    android:visibility="@{Visibility.goneWhenNot(vmToolbar.isBackButtonVisible())}"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@+id/movable_guide_top"/>

b)Fonction de “Recherche” (bouton et menu)

La fonctionnalité de recherche est très courante et importante pour rendre une application accessible. Voici comment nous y sommes pris pour la rendre accessible  sur ICI Tou Tv V4.

IOS 

À venir

ANDROID

Il est indispensable de définir et d'attribuer un libellé au bouton qui permet de lancer la recherche

<string name="a11y_search_button_label">Recherche</string>
<ImageView
    android:id="@+id/toolbar_search_view_button"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_marginEnd="15dp"
    android:paddingLeft="@dimen/search_button_padding"
    android:paddingRight="@dimen/search_button_padding"
    android:contentDescription="@string/a11y_search_button_label"
    android:onClick="@{_ -> vmToolbar.onSearchButtonClicked()}"
    android:scaleType="fitCenter"
    android:src="@drawable/rc_icon_search"
    android:visibility="@{Visibility.goneWhenNot(vmToolbar.isSearchVisible())}"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>


Il faut également intituler le bouton qui permet d'effacer le terme de recherche ou de fermer le menu de recherche:

@SuppressLint("PrivateResource")
fun SearchQueryTextChangeListener.setupA11y(newText: String) {
    searchView.findViewById(R.id.search_close_btn)?.let {
        it.contentDescription = resources?.getString(
                if (newText.isEmpty())
                    R.string.a11y_search_close_button_label
                else
                    R.string.abc_searchview_description_clear
        )
    }
}


Une fois les résultats de la recherche disponibles, il faut soit annoncer qu'aucun résultat n'a été trouvé, soit indiquer le nombre de résultats:

onNewResultsAvailableCallback = object : Observable.OnPropertyChangedCallback() {
    override fun onPropertyChanged(observable: Observable?, p1: Int) {
        (observable as? ObservableField<*>)
                ?.takeIf { it.get() is SearchFilterResult }
                ?.let {
                    val filterResult = it.get() as? SearchFilterResult
                    filterResult
                            ?.takeIf { it.items.none() }
                            ?.let {
                                announcementService.announce(resourceService.getString(R.string.search_no_results))
                            }
 
                    filterResult
                            ?.takeIf { it.items.any() && it.previousItems?.none() == true }
                            ?.let {
                                announcementService.announce("${it.items.size} ${resourceService.getPlural(R.plurals.a11y_search_result_item_label, it.items.size)}")
                            }
                }
    }
}


2.1.2 Barre de tabulation

Cette section décrit les changements qui ont dus être apportés à la barre de tabulation (BottomBar) de ICI Tou Tv V4 afin de la conformer aux normes décrites à la section 1.1.3.

IOS 

À venir

ANDROID

  • Indiquer l'onglet sélectionné: afin d'indiquer la section sélectionnée aux utilisateurs des services d'accessibilité, l'état de sélection de l'item a été spécifié au AccessbilityNodeInfo

info.isSelected = this@setupA11y.selectedItemId == host.id


  • Indiquer qu'il s'agit d'un menu

info.text = this@setupA11y.context.getString(R.string.a11y_menu_label)


  • Également, pour indiquer le nombre d'éléments dans le menu , le CollectionInfo (le parent) et CollectionItemInfo (les menu items) on étés spécifiés:

info.collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(1, this@setupA11y.menu.size(), false)
info.collectionItemInfo = AccessibilityNodeInfo.CollectionItemInfo.obtain(0,
        1, menu.findItem(host.id).order, 1, false, info.isSelected)


Il a fallut également indiquer que le parent des éléments du menu est important pour l'accessibilité et configurer son delegate

//Attach the delegate on the parent only once
if (index == 0) {
    viewGroup.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
    viewGroup.setAccessibilityDelegate(menuItemParentDelegate)
}


  • Et pour finir, le libellé du dernier élément du menu contient la phrase : 'fin du menu'

//When the current menu item is the last menu item
menu.getItem(menu.size() - 1)
        ?.takeIf { it.itemId == host.id }
        ?.let { lastItem ->
            info.contentDescription = "%s (%s)".format(Locale.ROOT, lastItem.title,
                    this@setupA11y.context.getString(R.string.a11y_end_of_menu_message))
        }


Voici le code complet

fun BottomNavigationView.setupA11y() {
 
    A11yHelper.setupBottomBarA11y(this, object : View.AccessibilityDelegate() {
 
        override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo?) {
            super.onInitializeAccessibilityNodeInfo(host, info)
            info ?: return
            host ?: return
 
            val menu = this@setupA11y.menu
            info.isSelected = this@setupA11y.selectedItemId == host.id
            info.collectionItemInfo = AccessibilityNodeInfo.CollectionItemInfo.obtain(0,
                    1, menu.findItem(host.id).order, 1, false, info.isSelected)
            //When the current menu item is the last menu item
            menu.getItem(menu.size() - 1)
                    ?.takeIf { it.itemId == host.id }
                    ?.let { lastItem ->
                        info.contentDescription = "%s (%s)".format(Locale.ROOT, lastItem.title,
                                this@setupA11y.context.getString(R.string.a11y_end_of_menu_message))
                    }
        }
    }, object : View.AccessibilityDelegate() {
 
        override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo?) {
            super.onInitializeAccessibilityNodeInfo(host, info)
            info ?: return
 
            info.text = this@setupA11y.context.getString(R.string.a11y_menu_label)
            info.collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(1, this@setupA11y.menu.size(), false)
        }
    })
}
 
fun setupBottomBarA11y(viewGroup: ViewGroup?,
                       menuItemDelegate: View.AccessibilityDelegate?,
                       menuItemParentDelegate: View.AccessibilityDelegate?) {
    viewGroup?.let { vg ->
        (0 until vg.childCount)
                .map { vg.getChildAt(it) }
                .forEachIndexed { index: Int, child: View ->
                    if (child is MenuView.ItemView) {
                        child.setAccessibilityDelegate(menuItemDelegate)
                        //Attach the delegate on the parent only once
                        if (index == 0) {
                            viewGroup.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
                            viewGroup.setAccessibilityDelegate(menuItemParentDelegate)
                        }
                    } else if (child is ViewGroup) {
                        setupBottomBarA11y(child, menuItemDelegate, menuItemParentDelegate)
                    }
                }
    }
}


2.2  Commandes

Regroupe les cas d’usages les plus répandus des contrôles utilisés pour permettre à l’utilisateur d’interagir avec le contenu de la page d’une application mobile.

En plus de celles qui s’appliquent à tous les commandes, un contrôle interactif doit avoir les particularités suivantes :

  1. Il est parcouru par les services d’accessibilité (même s’il est désactivé).

  2. L’état du contrôle est annoncé par le service vocal d’accessibilité (activé, désactivé, etc.).

2.2.1 Boutons

Les boutons sont les contrôles interactifs par excellence. Leur utilité première est de permettre à l’utilisateur de déclencher une action.

En plus de celles qui s’appliquent à tous les contrôles interactifs, un bouton doit avoir les particularités suivantes :

  1. Il est parcouru par les services d’accessibilité (même s’il est désactivé).

  2. En plus d’être nommée, l’action liée au bouton est décrite.

Cette section présente quelques cas d’usage de boutons.

a)Un bouton qui démarre de l’audio

Un bouton qui sert à démarrer de l’audio devrait avoir les particularités suivantes en plus de celles que tous les boutons devraient avoir.

  • La description de l’action associée au bouton doit commencer ou terminer par : “Écouter l’audio

  • La description de l’action associée au bouton doit contenir le titre du média audio qui sera démarré

b)Un bouton qui démarre de la vidéo

Un bouton qui sert à démarrer de l’audio devrait avoir les particularités suivantes en plus de celles que tous les boutons devraient avoir.

  • La description de l’action associée au bouton doit commencer ou terminer par : “Écouter la vidéo

  • La description de l’action associée au bouton doit contenir le titre du média audio qui sera démarré

c)Un bouton dans une liste de boutons

Suivre les recommandations faites à la section Collections d’éléments.


2.3 Contrôles non interactifs

Regroupe les cas d’usages les plus répandus des contrôles présents pour des besoins indicatifs ou décoratifs.En plus de celles qui s’appliquent à tous les contrôles natifs, un contrôle non interactif doit avoir la particularité suivante :

  1. Il est ignoré par les services d’accessibilité.

ANDROID

Exemple de contrôle non interactif

Le logo de l'application ainsi que le titre de la page précédente qui apparaissent dans la barre de navigation sur Android et sur iOS

Définition

<ViewSwitcher
       android:id="@+id/titlesSwitcher"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="start"
       android:importantForAccessibility="noHideDescendants">
 
       <ca.rc_cbc.mob.androidfx.widgets.textviews.CustomTextView
              android:id="@+id/appNameTextView"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="start"
              android:layout_gravity="start"
              android:textColor="@color/color_FFFFFF"
              android:textSize="30sp"
              app:textFontNameId="@string/app_ico_moon"
              />
 
       <ca.rc_cbc.mob.androidfx.widgets.textviews.CustomTextView
              android:id="@+id/back_arrow_label"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:ellipsize="end"
              android:maxLines="1"
              android:textColor="@color/color_FFFFFF"
              android:textSize="@dimen/h1_text_size"
              android:gravity="start"
              app:textFontNameId="@string/app_font_name"
              />
</ViewSwitcher>


Explication

Grâce à la propriété importantForAccessibility, il est possible de demander aux services d'accessibilité d'ignorer le groupe de vue qui abrite le logo de l'application ICI PREMIÈRE ( appNameTextView) ainsi que le titre de la page précédente ( back_arrow_label) qui apparaît lorsque l'utilisateur effectue une navigation.

Résultat

Pour voir le résultat, activer la fonction TalkBack sur l'application ICI Première de Radio-Canada et observer le comportement suivant :

  • Le logo de l'application ne reçoit jamais le focus.

  • Le logo de l'application n'est jamais lu par la fonction TalkBack.