as

Settings
Sign out
Notifications
Alexa
Amazon Appstore
AWS
Documentation
Support
Contact Us
My Cases
Devices
Build
Test
Publish
Connect
Documentation

Share:

Edit the Blog Post Header component above this, then place your content here, then fill out the Related Articles section below, if possible. You should have at least one related article and ideally, all three.

Feel free to add supplementary content to the sidebar at right, but please retain the Twitter component (it can live at the bottom of your added content). 

This text component can be edited or deleted as necessary.

Related Articles

Related article title here

Related articles only have an image (the squarer version of the banner) and a title. This text can be deleted. Place 1-3 related articles.

Subscribe

[Part 2] Designing for everyone: How to implement VoiceView on Fire devices

Amazon Developer Nov 18, 2025
Share:
Fire tablet Fire TV How to
Blog_Header_Post_Img

Last time we talked about you can design your app to make it accessible for as many people as possible. One of the key ways to do that is with VoiceView.

VoiceView is a way for visually impaired users to interact with visual elements in your app just by talking to their device and it’s built into Amazon Fire devices.

So let’s get into some code samples to help you implement VoiceView and make your app as widely accessible as possible.

Using VoiceView-specific extras in Fire OS

Fire OS extends the Android accessibility framework with specific enhancements for VoiceView. This way, you can provide richer contextual information to users of the screen reader. An important technique here is to use the extras bundle in AccessibilityNodeInfo to add keys such as:

  • com.amazon.accessibility.describedBy: Provide content descriptions for static content items that do not have on-screen text, such as a container.
  • com.amazon.accessibility.hint: Give usage hints to visually impaired users who may need assistance with how to navigate or interact with a screen.

 

For example:

Copied to clipboard
public void onInitializeAccessibilityNodeInfo(
  View host,
  AccessibilityNodeInfo info) {
  super.onInitializeAccessibilityNodeInfo(host, info);

  info
    .getExtras()
    .putString(
      "com.amazon.accessibility.describedBy",
      "This row contains trending movies."
    );

  info
    .getExtras()
    .putString(
      "com.amazon.accessibility.usageHint.touch", // for Fire tablets
      "Tap and hold for options. Double tap to select."
    );
      
  info
    .getExtras()
    .putString(
      "com.amazon.accessibility.usageHint.remote", // for Fire TV
      "Press left or right to find an it
    )
});

For developers that use React and Jetpack Compose, it's important to note that these frameworks abstract away the ability to manipulate AccessibilityNodeInfo directly. In this case, you would need to follow guidance from these frameworks on how to achieve similar results. For example, with Jetpack Compose, you would likely define semantic properties on components, similarly to how you would work with extras in AccessibilityNodeInfo.
 

Custom views and accessibility APIs

If you create custom UI components, then you'll need to manually implement accessibility. For example, imagine a custom component called CircularSeekBar. This is a circular progress selector that allows users to drag a thumb around a circular track to set a value from 0% to 100%.

UI component process selector

The visual design is intuitive for sighted users. But visually impaired users dependent on VoiceView need alternative ways to understand and interact with this component. So, you'll need to implement several key accessibility features.

First, make sure the view can be navigated to by keyboard and screen readers. This means setting isFocusable=true in the class definition.

Copied to clipboard
class CircularSeekBar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private var progress = 0
    private val maxProgress = 100

    init {
        isFocusable = true
    }

Next, describe the view to accessibility services. Override onInitializeAccessibilityNodeInfo to tell VoiceView that this is a seek bar with a range from 0-100, and that users can scroll forward/backward to adjust the value.

 

Copied to clipboard
override fun onInitializeAccessibilityNodeInfo(
  info: AccessibilityNodeInfo
) {
  super.onInitializeAccessibilityNodeInfo(info)

  // Use the compat wrapper for better backward compatibility.
  val infoCompat = AccessibilityNodeInfoCompat.wrap(info)

  // Identify the view as a SeekBar to the accessibility service.
  infoCompat.className = "android.widget.SeekBar"
  infoCompat.roleDescription = "Seek bar"
  infoCompat.contentDescription = "Circular progress selector"

  // Describe the view's range (min, max, and current value).
  infoCompat.rangeInfo = 
    AccessibilityNodeInfoCompat.RangeInfoCompat.obtain(
      AccessibilityNodeInfoCompat.RangeInfoCompat.RANGE_TYPE_INT,
      0f,
      maxProgress.toFloat(),
      progress.toFloat()
    )

  // Declare the actions that can be performed on this view.
  // This enables users to adjust the value with screen reader gestures.
  infoCompat.addAction(
    AccessibilityNodeInfoCompat
      .AccessibilityActionCompat
      .ACTION_SCROLL_FORWARD
  )
  infoCompat.addAction(
    AccessibilityNodeInfoCompat
      .AccessibilityActionCompat
      .ACTION_SCROLL_BACKWARD
  )
  infoCompat.isScrollable = true
}

Provide the actual functionality for VoiceView users by overriding performAccessibilityAction. For example, allow users to swipe up or down to change the value by 5, providing immediate audio feedback.

 

Copied to clipboard
override fun performAccessibilityAction(
  action: Int,
  arguments: Bundle?
): Boolean {
  val currentProgress = getProgress()

  // Determine the new progress based on the action.
  val newProgress = when (action) {
    AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD -> {
      (currentProgress + 5).coerceAtMost(getMaxProgress())
    }
    AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD -> {
      (currentProgress - 5).coerceAtLeast(0)
    }
    else -> return super.performAccessibilityAction(action, arguments)
  }

  // If progress changed, update it and announce the new value.
  if (newProgress != currentProgress
    setProgress(newProgress
    val accessibilityManager =
      context.getSystemService(Context.ACCESSIBILITY_SERVICE)
      as AccessibilityManager
    if (accessibilityManager.isEnabled) {
      val event = AccessibilityEvent
        .obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
      event.text.add("Progress: $newProgress%")
      accessibilityManager.sendAccessibilityEvent(event)
    }
    return true
  }

  // Return false if the action was not handled.
  return super.performAccessibilityAction(action, arguments)
}

If the above approach needed to be applied to a FireTV app that uses a remote for navigation with a D-pad, then you might include code that handles D-pad key presses by expressly triggering performAccessibilityAction. For example:

 

Copied to clipboard
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
  return when (keyCode) {
    KeyEvent.KEYCODE_DPAD_LEFT -> {
      performAccessibilityAction(
        AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD, null
      )
    }
    KeyEvent.KEYCODE_DPAD_RIGHT -> {
      performAccessibilityAction(
        AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD, null
      )
    }
    else -> super.onKeyDown(keyCode, event)
  }
}

You can also add custom actions that appear in the accessibility menu. In your MainActivity, attach an AccessibilityDelegateCompat to the container with your custom component.
 

Copied to clipboard
ViewCompat.setAccessibilityDelegate(
  seekBarContainer,
  object : AccessibilityDelegateCompat() {

    /**
     * This method is called when the accessibility service builds 
     * the node info for the view. Use it to add a custom action.
     */
    override fun onInitializeAccessibilityNodeInfo(
      host: View,
      info: AccessibilityNodeInfoCompat
    ) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val actionLabel = "Reset Progress"
      val customAction =
        AccessibilityNodeInfoCompat.AccessibilityActionCompat(
          R.id.action_reset_progress,  // ID defined in res/values/ids.xml
          actionLabel
        )
      info.addAction(customAction)
    }

    /**
     * This method is called when the user triggers the custom
     * accessibility action.
     */
    override fun performAccessibilityAction(
      host: View,
      action: Int,
      args: Bundle?
    ): Boolean {
      // Check if the action performed is our custom action.
      if (action == R.id.action_reset_progress) {
        // Perform the action: reset the seek bar's progress to 0.
        circularSeekBar.setProgress(0)
        val accessibilityManager =
          host.context.getSystemService(Context.ACCESSIBILITY_SERVICE)
            as AccessibilityManager
        if (accessibilityManager.isEnabled) {
            val event = AccessibilityEvent
              .obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
            event.text.add("Progress reset to 0%")
            accessibilityManager.sendAccessibilityEvent(event)
        }
        return true // Return true to indicate the action was handled.
      }
      // If it's not our action, let the default implementation handle it.
      return super.performAccessibilityAction(host, action, args)
    }
  }
)

When sighted users interact with touch, provide feedback by announcing the final value. This can be done within the onTouchEvent definition for the custom component:

 

Copied to clipboard
override fun onTouchEvent(event: MotionEvent): Boolean {
  when (event.action) {

    …

    MotionEvent.ACTION_UP -> {
      // When the user releases their touch,
      // send an announcement for accessibility.
      // This informs screen reader users of the final selected value.
      val accessibilityManager =
        context.getSystemService(Context.ACCESSIBILITY_SERVICE)
        as AccessibilityManager
      if (accessibilityManager.isEnabled) {
        val accessibilityEvent = AccessibilityEvent
          .obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
        accessibilityEvent.text.add("Progress set to $progress%")
        accessibilityManager.sendAccessibilityEvent(accessibilityEvent)
      }
      // Call performClick for compliance with accessibility standards.
      performClick()
      return true
    }
  }
  return super.onTouchEvent(event)
}

Accessibility navigation and focus traversal order

When you have users who rely on keyboard navigation or screen readers, an intuitive navigation experience is crucial. Think about a layout with buttons arranged in a non-linear pattern. Add into the interface decorative images that don't add functional value. You might have something like this:

 

navigation experience

If you don't provide proper focus management, your VoiceView users will encounter confusing navigation patterns or unnecessary announcements of decorative content. Instead, create a logical and predictable navigation experience by implementing focus control and content filtering.

To do this, you'll want to use android:importantForAccessibility="no" in your layout. This tells VoiceView to completely skip over a decorative image during navigation. This prevents users from hearing "Image" announcements that don't contribute to their understanding of the interface. In your layout XML, do this:

Copied to clipboard
<ImageView
  android:layout_width="100dp"
  android:layout_height="100dp"
  android:src="@android:drawable/ic_menu_gallery"
  android:importantForAccessibility="no"
  android:layout_gravity="center_horizontal"
  android:layout_marginTop="16dp"
  android:layout_marginBottom="16dp"
  android:contentDescription="@null" />

Here's another layout, where elements are arranged non-linearly:

non-linear arrangement

In this situation, override the default geometric navigation with a logical sequence. In this implementation, the nextFocus* attributes are key.

Copied to clipboard
<!-- Center Button -->
<Button
  android:id="@+id/button_center"
  android:text="Center"
  android:nextFocusForward="@id/button_right"
  android:nextFocusUp="@id/button_left"
  android:nextFocusDown="@id/button_right"
  android:nextFocusLeft="@id/button_left"
  android:nextFocusRight="@id/button_right" />

<!-- Right Button -->
<Button
  android:id="@+id/button_right"
  android:text="Right"
  android:nextFocusForward="@id/button_left"
  android:nextFocusUp="@id/button_center"
  android:nextFocusDown="@id/button_left"
  android:nextFocusLeft="@id/button_center"
  android:nextFocusRight="@id/button_left" />

<!-- Left Button -->
<Button
  android:id="@+id/button_left"
  android:text="Left"
  android:nextFocusForward="@id/button_center"
  android:nextFocusUp="@id/button_right"
  android:nextFocusDown="@id/button_center"
  android:nextFocusLeft="@id/button_right"
  android:nextFocusRight="@id/button_center" />

Applying this practice creates predictable navigation paths, especially on Fire TV where users interact via directional navigation rather than direct touch.

Labeling non-text elements

Visual design often relies on icons and images to convey meaning efficiently. But what's obvious to sighted users may not be so to screen reader users. When VoiceView encounters an unlabeled button or image, it might announce "Unlabeled button" or simply "Image." That's not helpful, and it will leave your users confused.

 

Or, imagine an interface with a "settings" button that uses only an icon: a gear symbol that's universally recognized by sighted users. Without proper labeling, VoiceView users might hear "Unlabeled button" when focusing on this element. It's impossible for them to understand the button's function. How can you bridge this gap? Provide text alternatives for all non-text elements that convey information or functionality.

 

Use the android:contentDescription attribute to provide clear, descriptive labels.

Copied to clipboard
<ImageButton
  android:id="@+id/settings_button"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:src="@android:drawable/ic_menu_preferences"
  android:contentDescription="Settings" />

Content descriptions should be added to any element that conveys information through visual design:

  • ImageButton elements: Icons that perform actions
  • ImageView elements: Images that convey information (not decorative)
  • Custom views: Components that rely on visual design for meaning
  • Interactive graphics: Charts, diagrams, or other visual elements that users can interact with

Keep in mind that writing effective descriptions means focusing on function rather than appearance. The description should answer "What does this do?" rather than "What does this look like?"

  • Avoid: "Gear icon" or "Red button"
  • Adopt: "Settings" or "Delete item"

Handling dynamic content updates

Modern apps frequently update content dynamically—a status message might appear, or a new item is added to a list, or some background operation completes. Sighted users can immediately see these changes, but screen reader users might miss these important updates… unless they're explicitly announced. Without those announcements, your app introduces an information gap, and it will leave some of your users confused about what's happening in the app.

Consider an app where content can change without a full page refresh, such as when a profile update completes in the background. The use of AccessibilityEvent.TYPE_ANNOUNCEMENT is critical here.

Copied to clipboard
updateButton.setOnClickListener {

  // Some background action (like saving data) would happen here.

  // Use AccessibilityEvent.TYPE_ANNOUNCEMENT to inform the user of the
  // result without needing to change the visible text on the screen.

  val accessibilityManager =
    context.getSystemService(Context.ACCESSIBILITY_SERVICE)
    as AccessibilityManager
  if (accessibilityManager.isEnabled) {
    val event = AccessibilityEvent
      .obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
    event.text.add("Your profile has been updated.")
    accessibilityManager.sendAccessibilityEvent(event)
  }

What about regions within your app display where new content may appear? In this scenario, designate containers as live regions to automatically announce new content. For example:

 

Copied to clipboard
<LinearLayout
  android:id="@+id/live_region_container"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_marginTop="16dp"
  android:orientation="vertical"
  android:accessibilityLiveRegion="polite" />

When new content is added to this container, VoiceView automatically announces it:

Copied to clipboard
addContentItemButton.setOnClickListener {
  itemCount++
  val newTextView = TextView(this)
  newTextView.text = "New Item #$itemCount has been added."
  // Because the container has android:accessibilityLiveRegion="polite",
  // the screen reader will automatically read the text of this new
  // TextView as soon as it's added.
  container.addView(newTextView)
}

The android:accessibilityLiveRegion attribute supports different announcement behaviors:

  • polite: Waits for VoiceView to finish what it is currently speaking before announcing. This is recommended for most cases.
  • assertive: Interrupts current speech to announce immediately. Use this sparingly for critical alerts.
  • off: Disables live region behavior.

Making list items more screen reader-friendly

Lists are fundamental to many app interfaces—song libraries, product catalogs, contact lists, and more. However, the default behavior of screen readers can make navigating these lists inefficient. If VoiceView treats each and every text element within a list item as a separate focusable element, then users will need to swipe multiple times to get through a single item. That will quickly get frustrating.

Consider a music app with a list of songs. Each list item contains a song title and artist name, displayed as separate text elements.

Screenshot for list item

Without accessible design, VoiceView would require users to swipe twice per song—once for the title and once for the artist. That kind of navigation experience is tedious. And your users will need to remember information across multiple focus points.

Create a more efficient and logical experience by grouping related information together. Set the root layout of each list item as focusable, thereby grouping child elements together.

Copied to clipboard
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:padding="16dp"
  android:focusable="true"
  android:background="?android:attr/selectableItemBackground">

  <!--
    By setting android:focusable="true" on the root layout,
    we tell VoiceView to treat this entire list item as a single
    interactive element. This groups the child TextViews together 
    for a more coherent screen reader experience. The contentDescription
    will be set programmatically in the adapter.
  -->

  <TextView
    android:id="@+id/song_title"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="Song Title"
    android:textSize="18sp"
    android:textStyle="bold"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

  <TextView
    android:id="@+id/song_artist"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="Artist Name"
    android:textSize="14sp"
    android:layout_marginTop="4dp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/song_title"
    app:layout_constraintEnd_toEndOf="parent"

</androidx.constraintlayout.widget.ConstraintLayout>

The key attribute android:focusable="true" tells VoiceView to treat the entire list item as a single, focusable unit rather than individual child elements.

 

Then, in the RecyclerView adapter for list objects, create a comprehensive content description that includes all relevant information:

Copied to clipboard
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
  val song = songs[position]
  holder.titleTextView.text = song.title
  holder.artistTextView.text = song.artist

  // By default, a screen reader would treat the title and artist as two
  // separate, focusable elements. By combining them into a single
  // contentDescription on the parent view (which is marked as focusable
  // in the XML), we let users hear all the item's info at once.
  holder.view.contentDescription = 
    "Song: ${song.title}, Artist: ${song.artist}"
}

This approach transforms two separate announcements ("Bohemian Rhapsody" followed by "Queen") into a single, comprehensive announcement ("Song: Bohemian Rhapsody, Artist: Queen").

Remember: Use clear, predictable patterns when crafting your contentDescription. For example:

  • Music: "Song: [title], Artist: [artist]"
  • Products: "Product: [name], Price: [price]"
  • Contacts: "Contact: [name], Phone: [number]"
  • News: "Article: [headline], Date: [date]"

When you approach your development this way, list navigation becomes much more efficient and user-friendly. Screen reader users can quickly browse through content frustration-free.

 

Why VoiceView?

Implementing VoiceView into your app is one of the best ways to make sure your app is available for as many people as possible. That’s important for you and it’s important for your users.

 

Additional Resources

Related articles

Sign up for our newsletter

Stay up to date with the latest Amazon Developer news, industry trends and blog posts.