Accessibility in Android
Introduction
Accessibility in Android ensures that users with visual, motor, or cognitive impairments can interact with your app effectively. Google mandates minimum accessibility standards for apps published on the Play Store, and more importantly, building accessible apps is simply the right thing to do. This tutorial walks through the core pillars of Android accessibility, from TalkBack integration and touch target sizing to dynamic type scaling and automated scanning. You will learn practical, production-ready patterns that you can apply immediately.
TalkBack Support
TalkBack is Android's screen reader, analogous to VoiceOver on iOS. When enabled, it announces the content of UI elements as users navigate via touch or keyboard. The most common mistake developers make is assuming TalkBack will read text that is visually displayed — it does not. TalkBack relies on semantic properties exposed through the Android accessibility framework.
Consider a custom badge view that displays a numeric count inside a colored circle. If you render the number via Canvas.drawText(), TalkBack will hear nothing because the canvas drawing is invisible to the accessibility tree.
class BadgeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatView(context, attrs, defStyleAttr) {
private var count: Int = 0
private val paint = Paint().apply {
isAntiAlias = true
color = ContextCompat.getColor(context, android.R.color.holo_red_dark)
textSize = 24f
textAlign = Paint.Align.CENTER
}
init {
isImportantForAccessibility = ImportantForAccessibility.AUTO
}
fun setCount(value: Int) {
count = value
contentDescription = if (value > 0) "$value notifications" else null
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (count > 0) {
canvas.drawCircle(width / 2f, height / 2f, height / 2f, paint)
paint.color = Color.WHITE
canvas.drawText(count.toString(), width / 2f, height / 2f + paint.textSize / 3, paint)
}
}
}
Setting isImportantForAccessibility to AUTO tells Android that this view may have meaningful content, prompting the system to include it in the accessibility traversal. The key line is contentDescription assignment inside setCount(). Rather than relying on the drawn text, we explicitly describe the semantic meaning — in this case, that the number represents notifications. When TalkBack is active, a user focusing on this view will hear "notifications, 3" or similar, depending on locale. Note that we set contentDescription to null when the count is zero, which removes the view from TalkBack's traversal entirely and avoids announcing empty or misleading information.
Content Descriptions
Content descriptions provide text alternatives for images, icons, and custom views. The rule of thumb is simple: if a user can understand the purpose of a view by looking at it, a blind user should be able to understand it by hearing its content description. This applies to vector drawables, icon buttons, and image-only views.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<ImageButton
android:id="@+id/btn_share"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_share"
android:contentDescription="@string/desc_share"
android:background="?attr/selectableItemBackgroundBorderless" />
<ImageButton
android:id="@+id/btn_delete"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_delete"
android:contentDescription="@string/desc_delete"
android:background="?attr/selectableItemBackgroundBorderless" />
</LinearLayout>
The XML above demonstrates two icon buttons paired with string resource content descriptions rather than hardcoded strings. Always reference @string/ resources for content descriptions so they can be localized and managed centrally. The android:background attribute uses the Material ripple borderless style, which gives visual feedback while keeping the touch target large. Without explicit contentDescription attributes, these buttons would be silent to TalkBack users because vector drawables carry no semantic text. The string resources referenced here — desc_share and desc_delete — should contain concise, purpose-oriented labels such as "Share" and "Delete" respectively. Avoid generic labels like "icon" or "image"; describe the action, not the visual appearance.
Touch Target Sizes
Google's Material Design guidelines specify a minimum touch target size of 48 by 48 density-independent pixels (dp). This requirement serves two groups: users with motor impairments who may have difficulty with precise taps, and users on small-screen devices where targets appear physically small. Many developers set a view's layout dimensions to 48dp but fail to realize that padding alone does not always expand the actual touch rectangle in every scenario.
class AccessibleButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatButton(context, attrs, defStyleAttr) {
init {
val minTouchTarget = (48 * resources.displayMetrics.density).toInt()
setMinimumWidth(minTouchTarget)
setMinimumHeight(minTouchTarget)
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
setPaddingRelative(
minTouchTarget / 2,
minTouchTarget / 2,
minTouchTarget / 2,
minTouchTarget / 2
)
}
}
This custom button class enforces the 48dp minimum touch target programmatically by setting both minimum width and height. The density conversion ensures the target is truly 48dp across all screen densities, not just 48 pixels on a dense screen. The padding calculation centers the text within the expanded target area. Using setMinimumWidth and setMinimumHeight is preferable to fixed layout_width and layout_height because it allows the button to grow larger when the content requires it, while never shrinking below the accessibility threshold. At runtime, this means a short label like "OK" will still present a generous 48dp touch surface, reducing misclicks and satisfying automated accessibility tests.
Focus Order
For users navigating with switch access or hardware keyboards, the focus order determines the sequence in which interactive elements receive attention. Android defaults to the order elements appear in the XML layout, but this is not always the logical reading order — especially with complex or RTL-aware layouts. A mismatched focus order forces keyboard and switch users to traverse the interface in a confusing sequence.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:descendantFocusability="beforeChildren">
<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_username"
android:nextFocusDown="@+id/et_password"
android:nextFocusRight="@+id/et_password" />
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_password"
android:inputType="textPassword"
android:nextFocusDown="@+id/btn_login"
android:nextFocusRight="@+id/btn_login" />
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login" />
</LinearLayout>
The XML above uses nextFocusDown and nextFocusRight to explicitly define the traversal sequence, ensuring that focus moves logically from username to password to the login button regardless of layout direction or view hierarchy quirks. The descendantFocusability attribute on the root layout is set to beforeChildren, which is the default but is stated explicitly for clarity. In practice, nextFocusDown should be paired with nextFocusRight because switch access users may navigate horizontally on RTL layouts. Without these attributes, Android falls back to layout order, which can be unpredictable when views are added dynamically or when using constraint layouts with chains. Test your focus order by connecting a USB keyboard or enabling Switch Access and confirming the sequence matches the visual flow.
Dynamic Type Scaling
Dynamic type scaling allows users to set a preferred font scale in their device's accessibility settings, which affects all text in your app. If your layout hardcodes pixel values or uses dp for text sizing, scaling will be ignored and text may overflow or become unreadable. The correct approach is to use sp for all text dimensions and avoid fixed widths on text-containing views.
class ScalableTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
init {
// Text size is already in sp by default when set via setTextSize(COMPLEX_UNIT_SP),
// so no additional scaling logic is needed. We verify the scaling factor is applied.
val configuration = resources.configuration
val scale = configuration.fontScale
if (scale != 1f) {
// The framework automatically scales sp values based on fontScale.
// Log for verification during development.
android.util.Log.d(
"ScalableTextView",
"Current fontScale: $scale, text size in sp: ${textSize / resources.displayMetrics.scaledDensity}"
)
}
}
}
Android's resource system automatically multiplies sp values by the user's font scale factor, so a 16sp text will render larger when the user has selected a bigger display size. This custom view subclass exists primarily as a verification hook during development, logging the effective scale factor so engineers can confirm scaling is active. The critical point is that textSize in sp units is the only reliable way to respect dynamic type; using dp or raw pixels bypasses the scaling pipeline entirely. In production, you rarely need a custom view for this — simply declare android:textSize="16sp" in XML and ensure no code overrides it with TypedValue.COMPLEX_UNIT_DIP or hardcoded pixel values.
Testing with Accessibility Scanner
Google provides the Accessibility Scanner app, available on the Play Store, which analyzes a screen and reports violations against established guidelines. It checks for insufficient color contrast, small touch targets, missing content descriptions, and invalid focus order. The scanner integrates directly into Android Studio's layout Inspector, making it easy to audit any XML layout without installing a physical device.
To use it, install Accessibility Scanner on a test device, open your app, and tap the scan button. The tool overlays color-coded indicators on violations — red for critical issues, yellow for warnings. In Android Studio, open any layout file, switch to the Design or Split view, and click the Accessibility tab in the Layout Inspector panel. You will see the same violation data rendered directly over the layout tree. A typical report will flag a 36dp icon button as a critical touch target violation and an unlabeled ImageView as a missing content description. Treat red findings as blocking bugs and yellow findings as high-priority improvements.
Common Pitfalls
The most frequent accessibility mistakes are also the easiest to fix once you know what to look for. Hardcoding text sizes in dp instead of sp breaks dynamic type support and is the single most common bug found in accessibility reviews. Another widespread issue is relying on color alone to convey meaning — for example, using only a red background to indicate an error state without adding an icon or text label, which is invisible to color-blind users. Views that are drawn on a Canvas but never given a contentDescription are completely silent to TalkBack, even if the information is visually clear. Auto-generated content descriptions from image filenames, such as "ic_launcher_2x.png," should always be overridden with human-readable text. Finally, many developers forget that visibility="gone" removes a view from the accessibility tree entirely, while visibility="invisible" keeps it present but non-interactive — choosing the wrong one can cause stale or confusing announcements.
Conclusion
Accessibility is not a feature you bolt on after launch; it is a design discipline that shapes every layout, interaction, and text string in your app. By providing meaningful content descriptions, enforcing minimum touch targets, respecting dynamic type scaling, and explicitly defining focus order, you build interfaces that work for everyone. The Accessibility Scanner gives you an automated safety net, but the deepest improvements come from understanding how real users with assistive technologies experience your product. Start by auditing your most-used screens, fix the critical violations, and iterate. The effort pays off in broader reach, better user satisfaction, and compliance with platform requirements.