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 only have an image (the squarer version of the banner) and a title. This text can be deleted. Place 1-3 related articles.
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.
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:
For example:
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.
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%.
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.
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.
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.
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:
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.
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:
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)
}
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:
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:
<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:
In this situation, override the default geometric navigation with a logical sequence. In this implementation, the nextFocus* attributes are key.
<!-- 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.
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.
<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:
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?"
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.
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:
<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:
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:
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.
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.
<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:
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:
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.
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.