77
88package com.facebook.react.views.text
99
10- import android.graphics.Paint
1110import android.graphics.Rect
1211import android.os.Bundle
12+ import android.text.Layout
1313import android.text.Spanned
14- import android.text.style.AbsoluteSizeSpan
1514import android.text.style.ClickableSpan
1615import android.view.View
1716import android.widget.TextView
@@ -21,7 +20,6 @@ import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
2120import com.facebook.react.R
2221import com.facebook.react.uimanager.ReactAccessibilityDelegate
2322import com.facebook.react.views.text.internal.span.ReactClickableSpan
24- import kotlin.math.ceil
2523
2624internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate {
2725 public constructor (
@@ -76,16 +74,19 @@ internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate {
7674
7775 val link = accessibilityLinks?.getLinkById(virtualViewId) ? : return
7876
79- val span = getFirstSpan(link.start, link.end, ClickableSpan ::class .java)
80- if (span == null || span !is ReactClickableSpan || hostView !is ReactTextView ) {
81- return
77+ val span = getFirstSpan(link.start, link.end, ClickableSpan ::class .java) ? : return
78+
79+ if (span is ReactClickableSpan && hostView is TextView ) {
80+ span.isKeyboardFocused = hasFocus
81+ span.focusBgColor = (hostView as TextView ).highlightColor
82+ hostView.invalidate()
83+ } else if (hostView is PreparedLayoutTextView ) {
84+ if (hasFocus) {
85+ (hostView as PreparedLayoutTextView ).setSelection(link.start, link.end)
86+ } else {
87+ (hostView as PreparedLayoutTextView ).clearSelection()
88+ }
8289 }
83-
84- // TODO: When we refactor ReactTextView, implement this using
85- // https://developer.android.com/reference/android/text/Layout
86- span.isKeyboardFocused = hasFocus
87- span.focusBgColor = (hostView as TextView ).highlightColor
88- hostView.invalidate()
8990 }
9091
9192 override fun onPerformActionForVirtualView (
@@ -99,10 +100,7 @@ internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate {
99100
100101 val link = accessibilityLinks?.getLinkById(virtualViewId) ? : return false
101102
102- val span = getFirstSpan(link.start, link.end, ClickableSpan ::class .java)
103- if (span == null || span !is ReactClickableSpan ) {
104- return false
105- }
103+ val span = getFirstSpan(link.start, link.end, ClickableSpan ::class .java) ? : return false
106104
107105 if (action == AccessibilityNodeInfoCompat .ACTION_CLICK ) {
108106 span.onClick(hostView)
@@ -122,49 +120,60 @@ internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate {
122120
123121 override fun getVirtualViewAt (x : Float , y : Float ): Int {
124122 val accessibilityLinks = accessibilityLinks ? : return INVALID_ID
125- if (accessibilityLinks.size() == 0 || hostView !is TextView ) {
123+ if (accessibilityLinks.size() == 0 ||
124+ (hostView !is TextView && hostView !is PreparedLayoutTextView )) {
126125 return INVALID_ID
127126 }
128127
129- var x = x
130- var y = y
131-
132- val textView = hostView as TextView
133- if (textView.text !is Spanned ) {
134- return INVALID_ID
135- }
136-
137- val layout = textView.layout ? : return INVALID_ID
138-
139- x - = textView.totalPaddingLeft.toFloat()
140- y - = textView.totalPaddingTop.toFloat()
141- x + = textView.scrollX.toFloat()
142- y + = textView.scrollY.toFloat()
128+ var localX = x
129+ var localY = y
130+ localX - = hostView.paddingLeft.toFloat()
131+ localY - = hostView.paddingTop.toFloat()
132+ localX + = hostView.scrollX.toFloat()
133+ localY + = hostView.scrollY.toFloat()
143134
144- val line = layout.getLineForVertical(y.toInt())
145- val charOffset = layout.getOffsetForHorizontal(line, x)
135+ val layout = getLayoutFromHost() ? : return INVALID_ID
136+ val line = layout.getLineForVertical(localY.toInt())
137+ val charOffset = layout.getOffsetForHorizontal(line, localX)
146138
147139 val clickableSpan =
148140 getFirstSpan(charOffset, charOffset, ClickableSpan ::class .java) ? : return INVALID_ID
149141
150- val spanned = textView.text as Spanned
142+ val spanned = getSpannedFromHost() ? : return INVALID_ID
151143 val start = spanned.getSpanStart(clickableSpan)
152144 val end = spanned.getSpanEnd(clickableSpan)
153145
154146 val link: AccessibilityLinks .AccessibleLink ? = accessibilityLinks.getLinkBySpanPos(start, end)
155147 return link?.id ? : INVALID_ID
156148 }
157149
158- protected fun <T > getFirstSpan (start : Int , end : Int , classType : Class <T >? ): T ? {
159- if (hostView !is TextView || (hostView as TextView ).text !is Spanned ) {
160- return null
150+ private fun getLayoutFromHost (): Layout ? {
151+ return if (hostView is PreparedLayoutTextView ) {
152+ (hostView as PreparedLayoutTextView ).layout
153+ } else if (hostView is TextView ) {
154+ (hostView as TextView ).layout
155+ } else {
156+ null
161157 }
158+ }
162159
163- val spanned = (hostView as TextView ).text as Spanned
160+ protected fun <T > getFirstSpan (start : Int , end : Int , classType : Class <T >? ): T ? {
161+ val spanned = getSpannedFromHost() ? : return null
164162 val spans = spanned.getSpans(start, end, classType)
165163 return if (spans.isNotEmpty()) spans[0 ] else null
166164 }
167165
166+ private fun getSpannedFromHost (): Spanned ? {
167+ val host = hostView
168+ return if (host is PreparedLayoutTextView ) {
169+ host.layout?.text as ? Spanned
170+ } else if (host is TextView ) {
171+ host.text as ? Spanned
172+ } else {
173+ null
174+ }
175+ }
176+
168177 @Suppress(" DEPRECATION" )
169178 override fun onPopulateNodeForVirtualView (virtualViewId : Int , node : AccessibilityNodeInfoCompat ) {
170179 // If we get an invalid virtualViewId for some reason (which is known to happen in API 19 and
@@ -203,12 +212,11 @@ internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate {
203212
204213 private fun getBoundsInParent (accessibleLink : AccessibilityLinks .AccessibleLink ): Rect ? {
205214 // This view is not a text view, so return the entire views bounds.
206- if (hostView !is TextView ) {
215+ if (hostView !is TextView && hostView !is PreparedLayoutTextView ) {
207216 return Rect (0 , 0 , hostView.width, hostView.height)
208217 }
209218
210- val textView = hostView as TextView
211- val textViewLayout = textView.layout ? : return Rect (0 , 0 , textView.width, textView.height)
219+ val textViewLayout = getLayoutFromHost() ? : return Rect (0 , 0 , hostView.width, hostView.height)
212220
213221 val startOffset = accessibleLink.start
214222 val endOffset = accessibleLink.end
@@ -225,22 +233,15 @@ internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate {
225233
226234 val startXCoordinates = textViewLayout.getPrimaryHorizontal(startOffset).toDouble()
227235
228- val paint = Paint ()
229- val sizeSpan =
230- getFirstSpan(accessibleLink.start, accessibleLink.end, AbsoluteSizeSpan ::class .java)
231- val textSize = sizeSpan?.size?.toFloat() ? : textView.textSize
232- paint.textSize = textSize
233- val textWidth = ceil(paint.measureText(accessibleLink.description).toDouble()).toInt()
234-
235236 val endOffsetLineNumber = textViewLayout.getLineForOffset(endOffset)
236237 val isMultiline = startOffsetLineNumber != endOffsetLineNumber
237238 textViewLayout.getLineBounds(startOffsetLineNumber, rootRect)
238239
239- val verticalOffset = textView .scrollY + textView.totalPaddingTop
240+ val verticalOffset = hostView .scrollY + hostView.paddingTop
240241 rootRect.top + = verticalOffset
241242 rootRect.bottom + = verticalOffset
242243 rootRect.left =
243- (rootRect.left + (startXCoordinates + textView.totalPaddingLeft - textView .scrollX)).toInt()
244+ (rootRect.left + (startXCoordinates + hostView.paddingLeft - hostView .scrollX)).toInt()
244245
245246 // The bounds for multi-line strings should *only* include the first line. This is because for
246247 // API 25 and below, Talkback's click is triggered at the center point of these bounds, and if
@@ -250,8 +251,8 @@ internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate {
250251 if (isMultiline) {
251252 return Rect (rootRect.left, rootRect.top, rootRect.right, rootRect.bottom)
252253 }
253-
254- return Rect (rootRect.left, rootRect.top, rootRect.left + textWidth , rootRect.bottom)
254+ val endXCoordinates = textViewLayout.getPrimaryHorizontal(endOffset).toDouble()
255+ return Rect (rootRect.left, rootRect.top, endXCoordinates.toInt() , rootRect.bottom)
255256 }
256257
257258 override fun getAccessibilityNodeProvider (host : View ): AccessibilityNodeProviderCompat ? {
0 commit comments