Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android Accessibility at GDG Devfest Brussels 2016

AppFoundry
October 23, 2016

Android Accessibility at GDG Devfest Brussels 2016

'Accessibility for everyone' session at the GDG Brussels 2016 Devfest.

AppFoundry

October 23, 2016
Tweet

More Decks by AppFoundry

Other Decks in Programming

Transcript

  1. Android accessibility

    View full-size slide

  2. Accessibility
    /ˈapps for everybody/
    Android apps that are usable by everyone. Make no assumptions
    about the user.

    View full-size slide

  3. AppFoundry
    appfoundry.be

    View full-size slide

  4. Assumptions
    Sight
    See the screen
    Mobility
    Touch the screen
    Audio
    Hear
    Speech
    Interact

    View full-size slide

  5. Accessibility?
    2 %
    visual impairment
    8 %
    men color blind
    0,1 %
    blind
    8 out of 10 with visual impairment or blindness are older than 65

    View full-size slide

  6. Accessibility?
    up to 20 %
    motor impairment
    Permanent
    Parkinson’s
    Tremor

    Situational
    Injury

    Hands busy

    View full-size slide

  7. Accessibility services
    Talkback
    Screen reader
    Brailleback
    Braille display
    Switch Access
    Control by switch
    Voice Access
    Interact

    View full-size slide

  8. Switch Access

    View full-size slide

  9. Voice access
    Voice Assistant
    High-level voice commands
    Accessibility
    Low-level commands
    Device control
    Scrolling, clicking, text editing

    View full-size slide

  10. Android - iOS

    View full-size slide

  11. Improve user experience
    for accessibility

    View full-size slide

  12. Color blindness
    Normal vision Deuteranopia Protanopia Tritanopia

    View full-size slide

  13. Color correction
    Accessibility options
    Color simulation
    Developer options

    View full-size slide

  14. Color contrast
    WCAG : background - foreground : 4.5 contrast ratio

    View full-size slide

  15. Android Accessibility features
    Large Text
    Magnification Gestures
    High Contrast Text
    Color inversion
    Color correction
    Caption support
    App developer
    Text size = sp
    Layouts = responsive

    View full-size slide

  16. Content description
    android:id="@+id/day"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="@string/mon"

    android:contentDescription="@string/monday" />

    View full-size slide

  17. Content description
    android:id="@+id/day"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="@string/mon"

    android:contentDescription="@string/monday" />

    View full-size slide

  18. Meaningful image
    android:id="@+id/action_mode_close_button"

    android:contentDescription="Done"

    android:focusable="true"

    android:clickable="true"

    app:srcCompat="?attr/actionModeCloseDrawable"

    style="?attr/actionModeCloseButtonStyle"

    android:layout_width="wrap_content"

    android:layout_height="match_parent"/>

    View full-size slide

  19. Meaningful image
    android:id="@+id/action_mode_close_button"

    android:contentDescription="Done"

    android:focusable="true"

    android:clickable="true"

    app:srcCompat="?attr/actionModeCloseDrawable"

    style="?attr/actionModeCloseButtonStyle"

    android:layout_width="wrap_content"

    android:layout_height="match_parent"/>

    View full-size slide

  20. Decorative image
    android:contentDescription=“@null"

    app:srcCompat=“@drawable/decoration“

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"/>

    View full-size slide

  21. Decorative image
    android:contentDescription=“@null"

    app:srcCompat=“@drawable/decoration“

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"/>

    View full-size slide

  22. Content description
    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:contentDescription="@string/acc">

    View full-size slide

  23. Content description
    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:contentDescription="@string/acc">

    View full-size slide

  24. States
    public void setIconAccessibilityText(boolean isFavorite, String
    teamName) {

    favoriteTeamMenuItem.setTitle(

    isFavorite ?

    ”defavoritise {teamName}” :

    ”mark {teamName} as favorite”;

    }

    View full-size slide

  25. AccessibilityLiveRegion
    android:id="@+id/evaluation_result"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:accessibilityLiveRegion="polite"/>

    View full-size slide

  26. AccessibilityLiveRegion
    android:id="@+id/evaluation_result"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:accessibilityLiveRegion="polite"/>

    View full-size slide

  27. AnnounceForAccessibility
    private void makeViewVisible() {

    view.setVisibility(View.VISIBLE);

    view.announceForAccessibility(getString(R.string.acc));

    }

    View full-size slide

  28. AnnounceForAccessibility
    private void makeViewVisible() {

    view.setVisibility(View.VISIBLE);

    view.announceForAccessibility(getString(R.string.acc));

    }

    View full-size slide

  29. AnnounceForAccessibility
    private void setFavoriteListener(final Team team, final boolean isFavorite)
    {

    view.setOnClickListener(new View.OnClickListener() {


    @Override

    public void onClick(View v) {

    teamListener.onTeamClicked(team);

    String announcement = isFavorite

    ? "Removed " + team.displayTitle() + " from favourites"

    : "Added " + team.displayTitle() + " to favourites";

    view.announceForAccessibility(announcement);

    }


    });

    }

    View full-size slide

  30. AnnounceForAccessibility
    private void setFavoriteListener(final Team team, final boolean isFavorite)
    {

    view.setOnClickListener(new View.OnClickListener() {


    @Override

    public void onClick(View v) {

    teamListener.onTeamClicked(team);

    String announcement = isFavorite

    ? "Removed " + team.displayTitle() + " from favourites"

    : "Added " + team.displayTitle() + " to favourites";

    view.announceForAccessibility(announcement);

    }


    });

    }

    View full-size slide

  31. Control focus order
    android:orientation="horizontal">


    android:id="@+id/edit"

    android:nextFocusForward="@+id/text" />


    android:id="@+id/text"

    android:focusable="true"

    android:text="Hello, I am a focusable TextView" />


    View full-size slide

  32. Control focus order
    android:orientation="horizontal">


    android:id="@+id/edit"

    android:nextFocusForward="@+id/text" />


    android:id="@+id/text"

    android:focusable="true"

    android:text="Hello, I am a focusable TextView" />


    View full-size slide

  33. Filter / change
    if necessary

    View full-size slide

  34. Ads

    No Ads
    Accessibility services detected

    View full-size slide

  35. Talkback detection
    public boolean isAccessibilityEnabled() {

    final AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);

    boolean isAccessibilityEnabled = am.isEnabled();

    boolean isExploreByTouchEnabled = am.isTouchExplorationEnabled();

    return isAccessibilityEnabled && isExploreByTouchEnabled;

    }

    View full-size slide

  36. Talkback detection
    public boolean isAccessibilityEnabled() {

    final AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);

    boolean isAccessibilityEnabled = am.isEnabled();

    boolean isExploreByTouchEnabled = am.isTouchExplorationEnabled();

    boolean isSpokenFeedbackEnabled = isSpokenFeedbackEnabled(am);

    return isAccessibilityEnabled && isExploreByTouchEnabled && isSpokenFeedbackEnabled;

    }


    /**

    * Check whether 'spoken feedback' services are enabled.

    */

    private boolean isSpokenFeedbackEnabled(final AccessibilityManager am) {

    List enabledSpokenFeedbackServices =
    am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN);

    return !enabledSpokenFeedbackServices.isEmpty();

    }

    View full-size slide

  37. Content description
    good / meaningful texts

    View full-size slide

  38. Meaningful description
    private void setAccessibilityText(final TeamInfo teamInfo) {

    String accessibilityText;

    if (teamInfo.coaches().isEmpty()) {

    accessibilityText = getString(R.string.no_trainer_acc, teamInfo.teamName());

    } else {

    List trainerNames = getTrainerNames(teamInfo.coaches());


    Joiner joiner = Joiner.on(andStr).skipNulls();

    String concatTrainers = joiner.join(trainerNames);

    accessibilityText = getQuantityString(R.plurals.trainer_acc, teamInfo.coaches().size(),
    teamInfo.teamName(), concatTrainers);

    }


    getItemView().setContentDescription(accessibilityText);

    }

    View full-size slide

  39. Help from your backend

    View full-size slide

  40. Custom views

    View full-size slide

  41. Custom actions

    View full-size slide

  42. MatchView
    public interface MatchListener {


    void onMatchClicked(Match match);


    void onMatchComment(Match match);


    void onMatchShare(Match match);


    void onMatchLiked(Match match);

    }

    View full-size slide

  43. Don’t descend into view
    android:importantForAccessibility="noHideDescendants"

    View full-size slide

  44. DIY
    public void showMatch(final Match match, final MatchListener listener) {

    …


    setOnClickListener(new View.OnClickListener() {

    @Override

    public void onClick(View view) {

    if (isAccessibilityEnabled) {

    showCustomActions(match);

    } else {

    listener.onMatchClicked(match);

    }

    }

    });

    View full-size slide

  45. Accessibility delegate
    public static class MatchAccessibilityDelegate extends AccessibilityDelegateCompat {


    private final Match match;

    private final MatchListener listener;


    public MatchAccessibilityDelegate(final Match match, final MatchListener listener) {

    this.match = match;

    this.listener = listener;

    }


    @Override

    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {

    super.onInitializeAccessibilityNodeInfo(host, info);

    info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.comment, "Comment"));

    info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.share, "Share"));

    …

    }


    @Override

    public boolean performAccessibilityAction(View host, int action, Bundle args) {

    switch (action) {

    case R.id.comment:

    listener.onMatchComment(match);

    return true;

    case R.id.share:

    listener.onMatchShare(match);

    return true;

    …

    return true;


    View full-size slide

  46. Accessibility delegate
    public static class MatchAccessibilityDelegate extends AccessibilityDelegateCompat {


    private final Match match;

    private final MatchListener listener;


    public MatchAccessibilityDelegate(final Match match, final MatchListener listener) {

    this.match = match;

    this.listener = listener;

    }


    @Override

    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {

    super.onInitializeAccessibilityNodeInfo(host, info);

    info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.comment, "Comment"));

    info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.share, "Share"));

    …

    }


    @Override

    public boolean performAccessibilityAction(View host, int action, Bundle args) {

    switch (action) {

    case R.id.comment:

    listener.onMatchComment(match);

    return true;

    case R.id.share:

    listener.onMatchShare(match);

    return true;

    …


    View full-size slide

  47. Accessibility delegate
    public void showMatch(final Match match, final MatchListener listener) {

    …


    ViewCompat.setAccessibilityDelegate(this, new MatchAccessibilityDelegate(match, listener));



    View full-size slide

  48. Manual testing

    View full-size slide

  49. Talkback basics
    Touch to explore
    Advance through elements (swipe left - right)
    Double tap selection
    Gestures for Back, Home, Context menus, …

    View full-size slide

  50. Accessibility
    scanner

    View full-size slide

  51. Code scanning
    (Lint)

    View full-size slide

  52. Lint rules




    lintOptions {
    abortOnError true
    }

    View full-size slide

  53. Instrumentation
    tests

    View full-size slide

  54. Espresso - Accessibility
    @Test

    public void testPlayStopButtonContentDescription() {

    final ViewInteraction playButtonInteraction = onView(withId(R.id.play_button));

    playButtonInteraction.check(matches(withContentDescription("Play")));


    playButtonInteraction.perform(click());

    playButtonInteraction.check(matches(withContentDescription("Stop")));

    }

    View full-size slide

  55. Espresso - Accessibility
    androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2.2'
    @Before

    public void setUp() {

    AccessibilityChecks.enable();

    }

    View full-size slide

  56. Accessibility checks
    com..…AccessibilityViewCheckException: There were 2 accessibility errors:
    AppCompatImageButton{id=2131427416, res-name=imageButton1, visibility=VISIBLE, width=144, height=132, has-
    focus=true, has-focusable=true, has-window-focus=false, is-clickable=true, is-enabled=true, is-
    focused=true, is-focusable=true, is-layout-requested=false, is-selected=false, root-is-layout-
    requested=false, has-input-connection=false, x=48.0, y=48.0}: View falls below the minimum recommended
    size for touch targets. Minimum touch target size is 48x48dp. Actual size is 48.0x44.0dp (screen
    density is 3.0).,
    AppCompatImageButton{id=2131427416, res-name=imageButton1, visibility=VISIBLE, width=144, height=132, has-
    focus=true, has-focusable=true, has-window-focus=false, is-clickable=true, is-enabled=true, is-
    focused=true, is-focusable=true, is-layout-requested=false, is-selected=false, root-is-layout-
    requested=false, has-input-connection=false, x=48.0, y=48.0}: View is missing speakable text needed
    for a screen reader
    at …
    at android.support.test.espresso.contrib.AccessibilityChecks$2.check(AccessibilityChecks.java:58)
    at android.support.test.espresso.action.ViewActions$1.perform(ViewActions.java:131)

    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

    View full-size slide

  57. Android Testing Support Library
    https://developer.android.com/guide/topics/ui/accessibility

    View full-size slide

  58. Sample
    github.com/filipmaelbrancke/AndroidAccessibility
    Accessibility sample
    Sample used in this presentation:
    Custom Actions
    Espresso Test

    View full-size slide

  59. Resources
    Accessibility Testing Checklist
    https://developer.android.com/training/accessibility/testing.html
    Google’s Accessibility Test Framework
    github.com/google/Accessibility-Test-Framework-for-Android
    Espresso Accessibility Checking
    google.github.io/android-testing-support-library/docs/accesibility-checking/
    Talkback Gestures
    support.google.com/accessibility/android/answer/6151827?hl=en

    View full-size slide

  60. + accessibility
    + accessibility

    View full-size slide

  61. Questions?
    Filip Maelbrancke
    Consultant @ AppFoundry
    [email protected]
    @fmaelbrancke

    View full-size slide