I have a String
with multiple Links. I am using SpannableText
and It is working perfect except accessibility talk back.
Is there any way to give accessibility to Links?
Create a class like below
package com.go.disney.disneycruise.util
import android.content.Context
import android.graphics.Rect
import android.os.Bundle
import android.text.Layout
import android.text.Spanned
import android.text.style.ClickableSpan
import android.view.accessibility.AccessibilityEvent
import android.widget.TextView
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.customview.widget.ExploreByTouchHelper
import com.disney.wdpro.dlog.DLog
import com.disney.wdpro.support.accessibility.ContentDescriptionBuilder
import kotlin.math.max
import kotlin.math.min
/**
* An accessibility delegate that allows [ClickableSpan] to be focused and
* clicked by accessibility services.
*
* @author arun.muraleedharan.
*/
class LinkAccessibilityHelper(
private val linkTextView: TextView,
private val context: Context,
private val linkAccessibilityId: String? = null
) : ExploreByTouchHelper(linkTextView) {
private val viewRect = Rect()
override fun getVirtualViewAt(x: Float, y: Float): Int {
val text = linkTextView.text
if (text is Spanned) {
val offset = getOffsetForPosition(linkTextView, x, y)
val linkSpans = text.getSpans(offset, offset, ClickableSpan::class.java)
if (linkSpans.size == 1) {
val linkSpan = linkSpans[0]
return text.getSpanStart(linkSpan)
}
}
return INVALID_ID
}
override fun getVisibleVirtualViews(virtualViewIds: MutableList<Int>) {
val text = linkTextView.text
if (text is Spanned) {
val linkSpans = text.getSpans(0, text.length, ClickableSpan::class.java)
for (span in linkSpans) {
virtualViewIds.add(text.getSpanStart(span))
}
}
}
override fun onPopulateEventForVirtualView(virtualViewId: Int, event: AccessibilityEvent) {
val span = getSpanForOffset(virtualViewId)
if (span != null) {
val spannedTextAccessibility = getTextForSpan(span).toString() + LINK
event.contentDescription = spannedTextAccessibility
} else {
DLog.e(TAG, "LinkSpan is null for offset: $virtualViewId")
event.contentDescription = linkTextView.text
}
}
override fun onPopulateNodeForVirtualView(
virtualViewId: Int,
info: AccessibilityNodeInfoCompat
) {
linkAccessibilityId?.let { info.viewIdResourceName = it }
val span = getSpanForOffset(virtualViewId)
if (span != null) {
val contentDescriptionBuilder = ContentDescriptionBuilder(
context
)
contentDescriptionBuilder.append(getTextForSpan(span).toString())
contentDescriptionBuilder.appendWithSeparator(LINK)
info.contentDescription = contentDescriptionBuilder.toString()
} else {
DLog.e(TAG, "LinkSpan is null for offset: $virtualViewId")
info.contentDescription = linkTextView.text
}
info.isFocusable = true
info.isClickable = true
getBoundsForSpan(span, viewRect)
if (viewRect.isEmpty) {
DLog.e(TAG, "LinkSpan bounds is empty for: $virtualViewId")
viewRect[0, 0, 1] = 1
}
info.setBoundsInParent(viewRect)
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK)
}
override fun onPerformActionForVirtualView(
virtualViewId: Int,
action: Int,
arguments: Bundle?
): Boolean {
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
val span = getSpanForOffset(virtualViewId)
if (span != null) {
span.onClick(linkTextView)
return true
} else {
DLog.e(TAG, "LinkSpan is null for offset: $virtualViewId")
}
}
return false
}
private fun getSpanForOffset(offset: Int): ClickableSpan? {
val text = linkTextView.text
if (text is Spanned) {
val spans = text.getSpans(offset, offset, ClickableSpan::class.java)
if (spans.size == 1) {
return spans[0]
}
}
return null
}
private fun getTextForSpan(span: ClickableSpan): CharSequence {
val text = linkTextView.text
if (text is Spanned) {
return text.subSequence(
text.getSpanStart(span),
text.getSpanEnd(span)
)
}
return text
}
// Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the
// section on the first line.
private fun getBoundsForSpan(span: ClickableSpan?, outRect: Rect): Rect {
val text = linkTextView.text
outRect.setEmpty()
if (text is Spanned) {
val layout = linkTextView.layout
if (layout != null) {
val spanStart = text.getSpanStart(span)
val spanEnd = text.getSpanEnd(span)
val xStart = layout.getPrimaryHorizontal(spanStart)
val xEnd = layout.getPrimaryHorizontal(spanEnd)
val lineStart = layout.getLineForOffset(spanStart)
val lineEnd = layout.getLineForOffset(spanEnd)
layout.getLineBounds(lineStart, outRect)
if (lineEnd == lineStart) {
// If the span is on a single line, adjust both the left and right bounds
// so outrect is exactly bounding the span.
outRect.left = min(xStart, xEnd).toInt()
outRect.right = max(xStart, xEnd).toInt()
} else {
// If the span wraps across multiple lines, only use the first line (as returned
// by layout.getLineBounds above), and adjust the "start" of outrect to where
// the span starts, leaving the "end" of outrect at the end of the line.
// ("start" being left for LTR, and right for RTL)
if (layout.getParagraphDirection(lineStart) == Layout.DIR_RIGHT_TO_LEFT) {
outRect.right = xStart.toInt()
} else {
outRect.left = xStart.toInt()
}
}
// Offset for padding
outRect.offset(linkTextView.totalPaddingLeft, linkTextView.totalPaddingTop)
}
}
return outRect
}
companion object {
private const val TAG = "LinkAccessibilityHelper"
const val LINK = "link."
// Compat implementation of TextView#getOffsetForPosition().
private fun getOffsetForPosition(view: TextView, x: Float, y: Float): Int {
if (view.layout == null) {
return -1
}
val line = getLineAtCoordinate(view, y)
return getOffsetAtCoordinate(view, line, x)
}
private fun convertToLocalHorizontalCoordinate(view: TextView, x: Float): Float {
var coordinateX = x
coordinateX -= view.totalPaddingLeft.toFloat()
// Clamp the position to inside of the view.
coordinateX = max(0.0f, coordinateX)
coordinateX = min((view.width - view.totalPaddingRight - 1).toFloat(), coordinateX)
coordinateX += view.scrollX.toFloat()
return coordinateX
}
private fun getLineAtCoordinate(view: TextView, y: Float): Int {
var coordinateY = y
coordinateY -= view.totalPaddingTop.toFloat()
// Clamp the position to inside of the view.
coordinateY = max(0.0f, coordinateY)
coordinateY = min((view.height - view.totalPaddingBottom - 1).toFloat(), coordinateY)
coordinateY += view.scrollY.toFloat()
return view.layout.getLineForVertical(coordinateY.toInt())
}
private fun getOffsetAtCoordinate(view: TextView, line: Int, x: Float): Int {
var coordinateX = x
coordinateX = convertToLocalHorizontalCoordinate(view, coordinateX)
return view.layout.getOffsetForHorizontal(line, coordinateX)
}
}
}
After that implement it like below
ViewCompat.setAccessibilityDelegate(
textView, LinkAccessibilityHelper(
textView, context, "linkId"
)
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With