Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up
for free
Android Accessibility at GDG Devfest Brussels 2016
AppFoundry
October 23, 2016
Programming
0
87
Android Accessibility at GDG Devfest Brussels 2016
'Accessibility for everyone' session at the GDG Brussels 2016 Devfest.
AppFoundry
October 23, 2016
Tweet
Share
More Decks by AppFoundry
See All by AppFoundry
appfoundrybe
0
49
appfoundrybe
0
110
appfoundrybe
0
83
appfoundrybe
0
180
appfoundrybe
0
130
appfoundrybe
0
270
appfoundrybe
1
140
appfoundrybe
0
300
appfoundrybe
0
500
Other Decks in Programming
See All in Programming
sullis
0
120
osyo
1
360
line_developers_tw
0
400
yshrsmz
1
450
hr01
0
1.6k
hirotokirimaru
1
400
taoshotaro
1
360
ippey
0
170
decoc
1
320
dulltz
0
410
kubode
0
180
showwin
0
120
Featured
See All Featured
philhawksworth
190
17k
addyosmani
494
110k
bkeepers
52
4.1k
ammeep
656
54k
afnizarnur
176
14k
dougneiner
119
7.8k
chrislema
231
16k
ddemaree
274
31k
reverentgeek
27
1.9k
chriscoyier
499
130k
smashingmag
229
18k
brianwarren
83
4.7k
Transcript
Android accessibility
Accessibility /ˈapps for everybody/ Android apps that are usable by
everyone. Make no assumptions about the user.
AppFoundry appfoundry.be
None
None
None
Why?
Assumptions Sight See the screen Mobility Touch the screen Audio
Hear Speech Interact
Accessibility? 2 % visual impairment 8 % men color blind
0,1 % blind 8 out of 10 with visual impairment or blindness are older than 65
Accessibility? up to 20 % motor impairment Permanent Parkinson’s Tremor
… Situational Injury … Hands busy
Accessibility services Talkback Screen reader Brailleback Braille display Switch Access
Control by switch Voice Access Interact
Brailleback
Switch Access
Voice access Voice Assistant High-level voice commands Accessibility Low-level commands
Device control Scrolling, clicking, text editing
Android - iOS
Improve user experience for accessibility
Color blindness Normal vision Deuteranopia Protanopia Tritanopia
Contrast
Color correction Accessibility options Color simulation Developer options
Color contrast WCAG : background - foreground : 4.5 contrast
ratio
Android Accessibility features Large Text Magnification Gestures High Contrast Text
Color inversion Color correction Caption support App developer Text size = sp Layouts = responsive
None
Content description <TextView android:id="@+id/day" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/mon" android:contentDescription="@string/monday" />
Content description <TextView android:id="@+id/day" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/mon" android:contentDescription="@string/monday" />
Meaningful image <ImageView 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"/>
Meaningful image <ImageView 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"/>
Decorative image <ImageView android:contentDescription=“@null" app:srcCompat=“@drawable/decoration“ android:layout_width="wrap_content" android:layout_height="wrap_content"/>
Decorative image <ImageView android:contentDescription=“@null" app:srcCompat=“@drawable/decoration“ android:layout_width="wrap_content" android:layout_height="wrap_content"/>
Content description <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:contentDescription="@string/acc">
Content description <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:contentDescription="@string/acc">
States public void setIconAccessibilityText(boolean isFavorite, String teamName) { favoriteTeamMenuItem.setTitle( isFavorite
? ”defavoritise {teamName}” : ”mark {teamName} as favorite”; }
AccessibilityLiveRegion <TextView android:id="@+id/evaluation_result" android:layout_width="match_parent" android:layout_height="wrap_content" android:accessibilityLiveRegion="polite"/>
AccessibilityLiveRegion <TextView android:id="@+id/evaluation_result" android:layout_width="match_parent" android:layout_height="wrap_content" android:accessibilityLiveRegion="polite"/>
AnnounceForAccessibility private void makeViewVisible() { view.setVisibility(View.VISIBLE); view.announceForAccessibility(getString(R.string.acc)); }
AnnounceForAccessibility private void makeViewVisible() { view.setVisibility(View.VISIBLE); view.announceForAccessibility(getString(R.string.acc)); }
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); } }); }
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); } }); }
Control focus order <LinearLayout android:orientation="horizontal"> <EditText android:id="@+id/edit" android:nextFocusForward="@+id/text" />
<TextView android:id="@+id/text" android:focusable="true" android:text="Hello, I am a focusable TextView" /> </LinearLayout>
Control focus order <LinearLayout android:orientation="horizontal"> <EditText android:id="@+id/edit" android:nextFocusForward="@+id/text" />
<TextView android:id="@+id/text" android:focusable="true" android:text="Hello, I am a focusable TextView" /> </LinearLayout>
Filter / change if necessary
Ads ☹ No Ads Accessibility services detected
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; }
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<AccessibilityServiceInfo> enabledSpokenFeedbackServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN); return !enabledSpokenFeedbackServices.isEmpty(); }
Content description good / meaningful texts
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<String> 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); }
Help from your backend
None
None
Custom views
Custom actions
MatchView public interface MatchListener { void onMatchClicked(Match match);
void onMatchComment(Match match); void onMatchShare(Match match); void onMatchLiked(Match match); }
None
Don’t descend into view android:importantForAccessibility="noHideDescendants"
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); } } });
None
None
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;
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; …
Accessibility delegate public void showMatch(final Match match, final MatchListener listener)
{ … ViewCompat.setAccessibilityDelegate(this, new MatchAccessibilityDelegate(match, listener)); …
None
Testing
Manual testing
None
Talkback basics Touch to explore Advance through elements (swipe left
- right) Double tap selection Gestures for Back, Home, Context menus, …
Accessibility scanner
None
None
None
None
None
Code scanning (Lint)
None
None
Lint rules <?xml version="1.0" encoding="UTF-8"?> <lint> <issue id="ContentDescription" severity="error" />
</lint> lintOptions { abortOnError true }
Instrumentation tests
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"))); }
Espresso - Accessibility androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2.2' @Before public void setUp() {
AccessibilityChecks.enable(); }
None
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)
Android Testing Support Library https://developer.android.com/guide/topics/ui/accessibility
Sample github.com/filipmaelbrancke/AndroidAccessibility Accessibility sample Sample used in this presentation: Custom
Actions Espresso Test
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
+ accessibility + accessibility
None
Questions? Filip Maelbrancke Consultant @ AppFoundry filip.maelbrancke@appfoundry.be @fmaelbrancke