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

Accessbility and Android APIs

Accessbility and Android APIs

Aung Kyaw Paing

November 19, 2022
Tweet

More Decks by Aung Kyaw Paing

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  5. - 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

    View Slide

  6. Built in services already exists!
    Accessibility
    services in Android

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  11. Accessibility services
    Write one time, works everywhere

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  17. android:id="@+id/event_banner"
    android:contentDescription="20% discount sale event"
    />

    View Slide

  18. android:id="@+id/event_banner"
    android:contentDescription="20% discount sale event"
    />

    android:id="@+id/action_settings"
    android:title="@string/action_settings"
    android:contentDescription="Settings" />

    View Slide

  19. android:id="@+id/event_banner"
    android:contentDescription="20% discount sale event"
    />

    android:id="@+id/action_settings"
    android:title="@string/action_settings"
    android:contentDescription="Settings" />

    view.contentDescription = "Describe me!"

    View Slide

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

    View Slide

  21. 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"
    }
    )

    View Slide

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

    View Slide

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

    View Slide

  24. android:id="@+id/event_banner"
    android:contentDescription="@null"
    />

    View Slide

  25. android:id="@+id/event_banner"
    android:contentDescription="@null"
    />

    android:id="@+id/event_banner"
    android:importantForAccessibility="no"
    />

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  29. fun InboxRow() {
    Row(
    modifier = Modifier
    .semantics {
    onClick(label = "Delete") {
    deleteMail()
    [email protected] true
    }
    }
    ) {
    // Render Contents
    }
    }

    View Slide

  30. fun InboxRow() {
    Row(
    modifier = Modifier
    .semantics {
    onClick(label = "Delete") {
    deleteMail()
    [email protected] true
    }
    }
    ) {
    // Render Contents
    }
    }

    View Slide

  31. fun InboxRow() {
    Row(
    modifier = Modifier
    .semantics {
    onClick(label = "Delete") {
    deleteMail()
    [email protected] true
    }
    }
    ) {
    // Render Contents
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  36. @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”

    View Slide

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

    View Slide

  38. View Slide

  39. android:id="@+id/project_container" ...
    android:screenReaderFocusable="true">
    android:id="@+id/project_title" ...
    android:focusable="false"
    android:text="droidconsg" />
    android:id="@+id/build_time"
    android:focusable="false"
    android:text="3 Hours Ago" />

    View Slide

  40. android:id="@+id/project_container" ...
    android:screenReaderFocusable="true">
    android:id="@+id/project_title" ...
    android:focusable="false"
    android:text="droidconsg" />
    android:id="@+id/build_time"
    android:focusable="false"
    android:text="3 Hours Ago" />

    View Slide

  41. android:id="@+id/project_container" ...
    android:screenReaderFocusable="true"
    android:contentDescription="droiconsg built 3 hours ago">
    android:id="@+id/project_title" ...
    android:focusable="false"
    android:text="droidconsg" />
    android:id="@+id/build_time"
    android:focusable="false"
    android:text="3 Hours Ago" />

    View Slide

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

    View Slide

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

    View Slide

  44. In addition….

    View Slide

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

    View Slide

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

    View Slide

  47. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  62. 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)

    View Slide

  63. 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)

    View Slide

  64. 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

    View Slide

  65. How would you know if it works without tests?
    Testing

    View Slide

  66. 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()
    }
    }

    View Slide

  67. 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 ...
    ...

    View Slide

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

    View Slide

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

    View Slide

  70. “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”

    View Slide

  71. dm me @vincentpaing
    Thanks you!

    View Slide