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

Android Accessibility at BABBQ Amsterdam

Android Accessibility at BABBQ Amsterdam

'Accessibility for everyone' session at the Big Android BBQ Amsterdam 2016.
All slides containing video are removed though...

Filip Maelbrancke

August 16, 2016
Tweet

More Decks by Filip Maelbrancke

Other Decks in Programming

Transcript

  1. Android accessibility

    View Slide

  2. Accessibility = Lint nagging…??

    View Slide

  3. View Slide

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

    View Slide

  5. AppFoundry
    appfoundry.be

    View Slide

  6. View Slide

  7. View Slide

  8. View Slide

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

    View Slide

  10. 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 Slide

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

    Situational
    Injury

    Hands busy

    View Slide

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

    View Slide

  13. Brailleback

    View Slide

  14. Switch Access

    View Slide

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

    View Slide

  16. Improve user experience
    for accessibility

    View Slide

  17. Color blindness
    Normal vision Deuteranopia Protanopia Tritanopia

    View Slide

  18. Contrast

    View Slide

  19. Color correction
    Accessibility options
    Color simulation
    Developer options

    View Slide

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

    View Slide

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

    View Slide

  22. View Slide

  23. 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 Slide

  24. 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 Slide

  25. 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 Slide

  26. 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 Slide

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

    app:srcCompat=“@drawable/decoration“

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"/>

    View Slide

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

    app:srcCompat=“@drawable/decoration“

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"/>

    View Slide

  29. Content description
    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:contentDescription="@string/acc">

    View Slide

  30. Content description
    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:contentDescription="@string/acc">

    View Slide

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

    favoriteTeamMenuItem.setTitle(

    isFavorite ?

    ”defavoritise {teamName}” :

    ”mark {teamName} as favorite”;

    }

    View Slide

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

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:accessibilityLiveRegion="polite"/>

    View Slide

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

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:accessibilityLiveRegion="polite"/>

    View Slide

  34. AnnounceForAccessibility
    private void makeViewVisible() {

    view.setVisibility(View.VISIBLE);

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

    }

    View Slide

  35. AnnounceForAccessibility
    private void makeViewVisible() {

    view.setVisibility(View.VISIBLE);

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

    }

    View Slide

  36. 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 Slide

  37. 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 Slide

  38. 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 Slide

  39. 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 Slide

  40. Filter / change
    if necessary

    View Slide

  41. Ads

    No Ads
    Accessibility services detected

    View Slide

  42. 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 Slide

  43. 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 Slide

  44. Content description
    good / meaningful texts

    View Slide

  45. 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 Slide

  46. Help from your backend

    View Slide

  47. View Slide

  48. View Slide

  49. Custom views

    View Slide

  50. Custom actions

    View Slide

  51. View Slide

  52. View Slide

  53. View Slide

  54. View Slide

  55. Custom actions

    View Slide

  56. MatchView
    public interface MatchListener {


    void onMatchClicked(Match match);


    void onMatchComment(Match match);


    void onMatchShare(Match match);


    void onMatchLiked(Match match);

    }

    View Slide

  57. View Slide

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

    View Slide

  59. View Slide

  60. 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 Slide

  61. 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 Slide

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

    …


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



    View Slide

  63. View Slide

  64. Testing

    View Slide

  65. Manual testing

    View Slide

  66. View Slide

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

    View Slide

  68. Accessibility
    scanner

    View Slide

  69. View Slide

  70. View Slide

  71. View Slide

  72. View Slide

  73. Code scanning
    (Lint)

    View Slide

  74. View Slide

  75. View Slide

  76. Lint rules




    lintOptions {
    abortOnError true
    }

    View Slide

  77. Instrumentation
    tests

    View Slide

  78. 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 Slide

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

    public void setUp() {

    AccessibilityChecks.enable();

    }

    View Slide

  80. View Slide

  81. 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 Slide

  82. Android Accessibility Documentation
    https://developer.android.com/guide/topics/ui/accessibility

    View Slide

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

    View Slide

  84. 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 Slide

  85. + accessibility
    + accessibility

    View Slide

  86. View Slide

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

    View Slide