Slide 1

Slide 1 text

Accessibility and Android APIs Aung Kyaw Paing (Vincent) Senior Consultant @ thoughtworks | GDE Thailand aungkyawpaing.dev

Slide 2

Slide 2 text

Over 1 billion people are estimated to experience disability. This corresponds to about 15% of the world's population - World Health Organization (WHO)

Slide 3

Slide 3 text

Common Disabilities Deafness & Hard-of-hearing Motor Impairments Cognitive disabilities Blindness & Visual impairment

Slide 4

Slide 4 text

Deafness & Hard-of-hearing Motor Impairments Cognitive disabilities Blindness & Visual impairment Everyone could experiences one of these in their lives.

Slide 5

Slide 5 text

- Won “Innovation in Accessibility” award for the first time in Game awards - Even people without common disabilities start using these features! The Last of Us

Slide 6

Slide 6 text

Built in services already exists! Accessibility services in Android

Slide 7

Slide 7 text

- Screen reader - Describe content out aloud - Interact with gestures Talkback

Slide 8

Slide 8 text

- Integration with braille display - Used together with Talkback - Interact using keys on display BrailleBack

Slide 9

Slide 9 text

Switch Access - Switches instead of taps - Scan items and highlight - User can interact with switches

Slide 10

Slide 10 text

Voice Access - Control hand-free - Show labels and grids - Interact device with voice commands

Slide 11

Slide 11 text

Accessibility services Write one time, works everywhere

Slide 12

Slide 12 text

Let’s look into Android accessibility APIs What can I do?

Slide 13

Slide 13 text

Size your buttons - Recommended size 48dp - Use padding to expand touchable area

Slide 14

Slide 14 text

Think twice about colors - Do not use color as only means of communication

Slide 15

Slide 15 text

- Do not use color as only means of communication Think twice about colors

Slide 16

Slide 16 text

Favorite/Like/Love Search Open Navigation Drawer Label images - Provide a screen readable label of your image and actions - Translated for different languages

Slide 17

Slide 17 text

Slide 18

Slide 18 text

Slide 19

Slide 19 text

view.contentDescription = "Describe me!"

Slide 20

Slide 20 text

Icon( imageVector = Icons.Filled.KeyboardArrowLeft, contentDescription = "Next" ) Image( bitmap = ImageBitmap.imageResource(id = R.id.arrow_next), contentDescription = "" )

Slide 21

Slide 21 text

Icon( imageVector = Icons.Filled.KeyboardArrowLeft, contentDescription = "Next" ) Image( bitmap = ImageBitmap.imageResource(id = R.id.arrow_next), contentDescription = "" ) Text( text = "Hello There", modifier = Modifier.semantics { contentDescription = "General Kenobi" } )

Slide 22

Slide 22 text

Provide users a way to add the content description for user-generated contents

Slide 23

Slide 23 text

- Skip unnecessary elements for screen reader to navigate faster Skip elements ????????

Slide 24

Slide 24 text

Slide 25

Slide 25 text

Slide 26

Slide 26 text

Icon( imageVector = Icons.Filled.Time, contentDescription = null )

Slide 27

Slide 27 text

Add accessibility actions Let accessibility services knows an action can be performed

Slide 28

Slide 28 text

ViewCompat.addAccessibilityAction( itemView, getText(R.id.delete) ) { _, _ -> deleteMail() true } “Double tap to delete”

Slide 29

Slide 29 text

fun InboxRow() { Row( modifier = Modifier .semantics { onClick(label = "Delete") { deleteMail() return@onClick true } } ) { // Render Contents } }

Slide 30

Slide 30 text

fun InboxRow() { Row( modifier = Modifier .semantics { onClick(label = "Delete") { deleteMail() return@onClick true } } ) { // Render Contents } }

Slide 31

Slide 31 text

fun InboxRow() { Row( modifier = Modifier .semantics { onClick(label = "Delete") { deleteMail() return@onClick true } } ) { // Render Contents } }

Slide 32

Slide 32 text

Replace accessibility actions Sometimes a click is not just a click “Double Tap to Press”

Slide 33

Slide 33 text

ViewCompat.replaceAccessibilityAction( itemView, AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, "Open $text", null ) “Double tap to open $text”

Slide 34

Slide 34 text

ViewCompat.replaceAccessibilityAction( itemView, AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, "Open $text", null ) “Double tap to open $text”

Slide 35

Slide 35 text

@Composable fun ClickableRow( text: String, onClick: () -> Unit ) { Row( modifier = Modifier .clickable() { onClick() } ) { //Render Text and Arrow Right Icon } } “Double tap to click”

Slide 36

Slide 36 text

@Composable fun ClickableRow( text: String, onClick: () -> Unit ) { Row( modifier = Modifier .clickable(onClickLabel = "Open $text") { onClick() } ) { //Render Text and Arrow Right Icon } } “Double tap to open $text”

Slide 37

Slide 37 text

Merge components - Merge views to make screen reader navigate with fewer taps - Recommend to use for Lists + “Checkbox Checked” “With Soup” “Checkbox With Soup - Checked”

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

Slide 40

Slide 40 text

Slide 41

Slide 41 text

Slide 42

Slide 42 text

fun ProjectCard() { Column { Text("droidconsg") Text("3 Hours Ago") } }

Slide 43

Slide 43 text

fun ProjectCard() { Column(modifier = Modifier.Semantic( mergeDescendants = true ) { //Other accessibility stuffs }) { Text("droidconsg") Text("3 Hours Ago") } }

Slide 44

Slide 44 text

In addition….

Slide 45

Slide 45 text

fun ProjectCard() { Column(modifier = Modifier.Semantic( mergeDescendants = true ) { onClick(label = "Do Something") { //On Click actions } }) { BookmarkButton() } }

Slide 46

Slide 46 text

fun ProjectCard() { Column(modifier = Modifier.Semantic( mergeDescendants = true ) { onClick(label = "Do Something") { //On Click actions } }) { BookmarkButton(modifier = Modifier.clearAndSetSemantics { }) } }

Slide 47

Slide 47 text

Describe the state - Telling screen reader to alert user for state change on interaction Paused Playing Image ref: https://medium.com/google-dev eloper-experts/state-description s-on-android-b2029283871f

Slide 48

Slide 48 text

playPauseButton.setOnClickListener { if (isPlaying) { ViewCompat.setStateDescription(playPauseButton, getString(R.string.paused)) } else { ViewCompat.setStateDescription(playPauseButton, getString(R.string.playing)) } }

Slide 49

Slide 49 text

playPauseButton.setOnClickListener { if (isPlaying) { ViewCompat.setStateDescription(playPauseButton, getString(R.string.paused)) } else { ViewCompat.setStateDescription(playPauseButton, getString(R.string.playing)) } }

Slide 50

Slide 50 text

@Composable fun MusicPlayerControls { PlayPauseButton(modifier = Modifier.semantics { stateDescription = if (isPlaying) "Paused" else "Playing" }) }

Slide 51

Slide 51 text

@Composable fun MusicPlayerControls { PlayPauseButton(modifier = Modifier.semantics { stateDescription = if (isPlaying) "Paused" else "Playing" }) }

Slide 52

Slide 52 text

Timeout - Do not use constant timeouts - Instead, use recommended timeout from Accessibility manager

Slide 53

Slide 53 text

val accessibilityManager = ContextCompat.getSystemService(this, AccessibilityManager::class.java) accessibilityManager?.getRecommendedTimeoutMillis( DEFAULT_TIMEOUT, AccessibilityManager.FLAG_CONTENT_CONTROLS or AccessibilityManager.FLAG_CONTENT_ICONS )

Slide 54

Slide 54 text

val accessibilityManager = ContextCompat.getSystemService(this, AccessibilityManager::class.java) accessibilityManager?.getRecommendedTimeoutMillis( DEFAULT_TIMEOUT, AccessibilityManager.FLAG_CONTENT_CONTROLS or AccessibilityManager.FLAG_CONTENT_ICONS )

Slide 55

Slide 55 text

val accessibilityManager = ContextCompat.getSystemService(this, AccessibilityManager::class.java) accessibilityManager?.getRecommendedTimeoutMillis( DEFAULT_TIMEOUT, AccessibilityManager.FLAG_CONTENT_CONTROLS or AccessibilityManager.FLAG_CONTENT_ICONS )

Slide 56

Slide 56 text

val accessibilityManager = ContextCompat.getSystemService(this, AccessibilityManager::class.java) accessibilityManager?.getRecommendedTimeoutMillis( DEFAULT_TIMEOUT, AccessibilityManager.FLAG_CONTENT_CONTROLS or AccessibilityManager.FLAG_CONTENT_ICONS )

Slide 57

Slide 57 text

val accessibilityManager = ContextCompat.getSystemService(this, AccessibilityManager::class.java) accessibilityManager?.getRecommendedTimeoutMillis( DEFAULT_TIMEOUT, AccessibilityManager.FLAG_CONTENT_TEXT )

Slide 58

Slide 58 text

val accessibilityManager = LocalAccessibilityManager.current accessibilityManager?.calculateRecommendedTimeoutMillis( originalTimeoutMillis = DEFAULT_TIMEOUT, containsIcons = true, containsText = false, containsControls = false )

Slide 59

Slide 59 text

val accessibilityManager = LocalAccessibilityManager.current accessibilityManager?.calculateRecommendedTimeoutMillis( originalTimeoutMillis = DEFAULT_TIMEOUT, containsIcons = true, containsText = false, containsControls = false )

Slide 60

Slide 60 text

val accessibilityManager = LocalAccessibilityManager.current accessibilityManager?.calculateRecommendedTimeoutMillis( originalTimeoutMillis = DEFAULT_TIMEOUT, containsIcons = true, containsText = false, containsControls = false )

Slide 61

Slide 61 text

val accessibilityManager = LocalAccessibilityManager.current accessibilityManager?.calculateRecommendedTimeoutMillis( originalTimeoutMillis = DEFAULT_TIMEOUT, containsIcons = true, containsText = false, containsControls = false )

Slide 62

Slide 62 text

Anti-patterns - Do not reinvent the wheel - Use material or androidx components as much as possible - Do not abuse accessibility announcements val event = AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) event.text.add("You got a new notification!") accessibilityManager.sendAccessibilityEvent(event)

Slide 63

Slide 63 text

Anti-patterns - Do not reinvent the wheel - Use material or androidx components as much as possible - Do not abuse accessibility announcements val event = AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) event.text.add("You got a new notification!") accessibilityManager.sendAccessibilityEvent(event)

Slide 64

Slide 64 text

Check List - Adjust button sizes, text sizes and color contrast - Label images - Skip unnecessary elements - Provide accessibility actions - Replace with meaningful actions - Adjust timeout - Merge elements

Slide 65

Slide 65 text

How would you know if it works without tests? Testing

Slide 66

Slide 66 text

Automated Testing - Espresso provides a one-line code to test accessibility - Runs a list of test every time you interact with the view in a test class EspressoTest { init { AccessibilityChecks.enable() } }

Slide 67

Slide 67 text

Automated Testing There were 2 accessibility errors: AppCompatImageButton{id=2131165210, ...}: View is missing speakable text needed for a screen reader, AppCompatImageButton{id=2131165210,...}: View falls below the minimum recommended size ... ...

Slide 68

Slide 68 text

Compose UI Test composeTestRule .onNodeWithContentDescription("Content Description") .assertIsDisplayed() val matcher = hasStateDescription("state value") composeTestRule.onNode(matcher).assertIsDisplayed()

Slide 69

Slide 69 text

Accessibility Scanner - Use accessibility scanner to check for best practices https://bit.ly/3Sp7SKz

Slide 70

Slide 70 text

“Accessibility isn’t something that you check the list and say it’s done. It’s an user experience so you have to test it manually by yourself to know it’s a good experience”

Slide 71

Slide 71 text

dm me @vincentpaing Thanks you!