Accessibility Guide: Custom Views in Kotlin XML for Android

In Android development, creating custom views allows developers to encapsulate specific functionality and UI elements into reusable components. However, ensuring that these custom views are accessible is critical for providing a usable experience for all users, including those with disabilities. This blog post will guide you through making custom views accessible in Kotlin-based XML development for Android, following accessibility best practices and guidelines.

What is Accessibility in Android Development?

Accessibility refers to the design of products, devices, services, or environments for people with disabilities. In the context of Android, accessibility ensures that users with visual, auditory, motor, or cognitive impairments can effectively use your application. By making your app accessible, you’re not only complying with legal requirements but also broadening your user base and providing a better experience for everyone.

Why is Accessibility Important for Custom Views?

Custom views often involve unique functionalities and interactions that the standard Android widgets do not provide. If these views are not properly designed with accessibility in mind, they can become significant barriers for users with disabilities. Ensuring accessibility in custom views requires careful consideration of attributes, focus handling, and semantic information.

Accessibility Best Practices for Custom Views

1. Semantic Information with contentDescription

The contentDescription attribute is used to provide a textual description of a view’s purpose or function. Screen readers use this description to convey information to visually impaired users.

<com.example.customview.CustomView
    android:id="@+id/customView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:contentDescription="@string/custom_view_description"/>

In your strings.xml:

<string name="custom_view_description">Displays a custom graphic with interactive elements.</string>

Kotlin Usage:

val customView = findViewById<CustomView>(R.id.customView)
customView.contentDescription = getString(R.string.custom_view_description)

2. Focus Handling with focusable and onClick

Ensure your custom view is focusable and responds to click events for users who navigate using a keyboard or switch access.

<com.example.customview.CustomView
    android:id="@+id/customView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:contentDescription="@string/custom_view_description"
    android:focusable="true"
    android:clickable="true"/>

In your custom view class in Kotlin:

class CustomView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {

    init {
        isFocusable = true
        isClickable = true
        setOnClickListener {
            // Handle click action
        }
    }

    override fun performClick(): Boolean {
        super.performClick()
        // Custom click handling logic
        return true
    }
}

3. Touch Target Size

Ensure touch targets are large enough (at least 48×48 dp) to be easily selectable by users with motor impairments.

<com.example.customview.CustomView
    android:id="@+id/customView"
    android:layout_width="48dp"
    android:layout_height="48dp"
    android:contentDescription="@string/custom_view_description"
    android:focusable="true"
    android:clickable="true"/>

Adjust sizes in your layout file or programmatically in Kotlin:

customView.layoutParams.width = 48.dpToPx(context)
customView.layoutParams.height = 48.dpToPx(context)

fun Int.dpToPx(context: Context): Int =
    (this * context.resources.displayMetrics.density).toInt()

4. State Description

Use stateDescription to provide additional information about the current state of a custom view, especially if the state changes dynamically.

import android.view.View
import android.content.Context
import android.util.AttributeSet
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.ViewCompat

class ToggleButtonView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    private var isChecked = false

    init {
        isClickable = true
        updateAccessibilityStateDescription()
    }

    override fun performClick(): Boolean {
        isChecked = !isChecked
        updateAccessibilityStateDescription()
        invalidate() // Redraw the view
        return super.performClick()
    }

    private fun updateAccessibilityStateDescription() {
        stateDescription = if (isChecked) "Checked" else "Unchecked"
    }

    // Optional: Override onInitializeAccessibilityNodeInfo for more control
    override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
        super.onInitializeAccessibilityNodeInfo(info)
        info.className = ToggleButtonView::class.java.name
        info.isCheckable = true
        info.isChecked = isChecked
    }
}

5. Live Regions

If your custom view updates frequently, use live regions to notify screen readers about the changes.

<com.example.customview.CustomView
    android:id="@+id/customView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:contentDescription="@string/custom_view_description"
    android:focusable="true"
    android:clickable="true"
    android:accessibilityLiveRegion="polite"/>

Options for accessibilityLiveRegion are none, polite, and assertive.

Kotlin Usage (programmatically):

ViewCompat.setAccessibilityLiveRegion(customView, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE)

6. Using AccessibilityDelegate for Custom Logic

For complex custom views, you may need to override the accessibility behavior. You can use AccessibilityDelegate to customize how accessibility events are handled.

import android.view.View
import android.content.Context
import android.util.AttributeSet
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat

class CustomAccessibleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {

    init {
        ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
            override fun onInitializeAccessibilityNodeInfo(v: View, info: AccessibilityNodeInfoCompat) {
                super.onInitializeAccessibilityNodeInfo(v, info)
                // Customize the accessibility node info
                info.contentDescription = "Custom accessibility description"
                info.isFocusable = true
                // Add custom actions if needed
            }
        })
        isFocusable = true
        isClickable = true
    }
}

7. Keyboard Navigation

Ensure users can navigate through your custom views using a keyboard or directional pad. Use android:nextFocusForward, android:nextFocusLeft, android:nextFocusRight, android:nextFocusUp, and android:nextFocusDown attributes to control the focus order.

<com.example.customview.CustomView
    android:id="@+id/customView1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:contentDescription="@string/custom_view1_description"
    android:focusable="true"
    android:clickable="true"
    android:nextFocusRight="@id/customView2"/>

<com.example.customview.CustomView
    android:id="@+id/customView2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:contentDescription="@string/custom_view2_description"
    android:focusable="true"
    android:clickable="true"
    android:nextFocusLeft="@id/customView1"/>

8. Testing Accessibility

Regularly test the accessibility of your custom views using Android accessibility tools:

  • Accessibility Scanner: Identifies accessibility issues.
  • TalkBack: Android’s built-in screen reader.
  • Switch Access: For users with motor impairments.

These tools help ensure that your custom views provide a seamless experience for all users.

Example: Accessible Rating Bar Custom View

Let’s create an accessible rating bar custom view that adheres to accessibility best practices.

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import android.widget.ImageView
import androidx.core.content.ContextCompat
import com.example.app.R // Replace with your app's R class
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat

class AccessibleRatingBar(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {

    private var rating = 0
    private val maxRating = 5
    private val starViews = mutableListOf<ImageView>()

    init {
        orientation = HORIZONTAL
        isFocusable = true
        isClickable = true

        for (i in 1..maxRating) {
            val star = ImageView(context)
            val params = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT
            )
            star.layoutParams = params
            star.setImageResource(R.drawable.ic_star_border) // Default star image
            star.setOnClickListener {
                rating = i
                updateStars()
                contentDescription = "Rating: $rating out of $maxRating"
            }
            starViews.add(star)
            addView(star)
        }
        updateStars()

        ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
            override fun onInitializeAccessibilityNodeInfo(v: View, info: AccessibilityNodeInfoCompat) {
                super.onInitializeAccessibilityNodeInfo(v, info)
                info.contentDescription = "Rating: $rating out of $maxRating. Use double tap to set the rating."
                info.isFocusable = true
            }
        })
    }

    private fun updateStars() {
        for (i in 0 until maxRating) {
            val star = starViews[i]
            if (i < rating) {
                star.setImageResource(R.drawable.ic_star) // Filled star
            } else {
                star.setImageResource(R.drawable.ic_star_border) // Empty star
            }
        }
    }

    fun setRating(newRating: Int) {
        rating = newRating
        updateStars()
        contentDescription = "Rating: $rating out of $maxRating"
    }
}

In your layout XML:

<com.example.customview.AccessibleRatingBar
    android:id="@+id/ratingBar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:contentDescription="Rating: Not rated. Use double tap to set the rating."
    android:focusable="true"
    android:clickable="true"/>

Conclusion

Making custom views accessible is an essential part of Android development. By implementing the best practices outlined in this guide—such as providing semantic information, ensuring proper focus handling, adjusting touch target sizes, and using live regions—you can create inclusive and usable experiences for all users. Regular testing with accessibility tools is crucial to identify and address potential issues. Embracing accessibility not only broadens your user base but also aligns with the ethical responsibility to create technology that is accessible to everyone.