@@ -57,6 +57,7 @@ import com.facebook.react.uimanager.ReactClippingViewGroupHelper.calculateClippi
5757import com.facebook.react.uimanager.ReactOverflowViewWithInset
5858import com.facebook.react.uimanager.ReactPointerEventsView
5959import com.facebook.react.uimanager.ReactZIndexedViewGroup
60+ import com.facebook.react.uimanager.TouchTargetHelper
6061import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper
6162import com.facebook.react.uimanager.common.UIManagerType
6263import com.facebook.react.uimanager.common.ViewUtil.getUIManagerType
@@ -153,6 +154,10 @@ public open class ReactViewGroup public constructor(context: Context?) :
153154 private var accessibilityStateChangeListener:
154155 AccessibilityManager .AccessibilityStateChangeListener ? =
155156 null
157+ private var touchExplorationStateChangeListener:
158+ AccessibilityManager .TouchExplorationStateChangeListener ? =
159+ null
160+ private var isTouchExplorationEnabled = false
156161
157162 init {
158163 initView()
@@ -181,6 +186,8 @@ public open class ReactViewGroup public constructor(context: Context?) :
181186 backfaceOpacity = 1f
182187 backfaceVisible = true
183188 childrenRemovedWhileTransitioning = null
189+ touchExplorationStateChangeListener = null
190+ isTouchExplorationEnabled = false
184191 }
185192
186193 internal open fun recycleView () {
@@ -308,8 +315,8 @@ public open class ReactViewGroup public constructor(context: Context?) :
308315 // For accessibility services (TalkBack), check if hover is within any child's hitSlop area.
309316 // Only apply this logic when accessibility services are enabled to avoid interfering with
310317 // other input methods (VR, mouse, stylus, etc.)
311- val accessibilityManager = context.getSystemService( Context . ACCESSIBILITY_SERVICE ) as ? AccessibilityManager
312- if (accessibilityManager?. isTouchExplorationEnabled == true &&
318+ // Use cached value to avoid expensive Binder call per frame
319+ if (isTouchExplorationEnabled &&
313320 ev.isFromSource(android.view.InputDevice .SOURCE_CLASS_POINTER ) &&
314321 (ev.action == MotionEvent .ACTION_HOVER_ENTER || ev.action == MotionEvent .ACTION_HOVER_MOVE )) {
315322 val x = ev.x
@@ -330,34 +337,23 @@ public open class ReactViewGroup public constructor(context: Context?) :
330337 val childX = x - child.left
331338 val childY = y - child.top
332339
333- // Check if within hitSlop-extended bounds
334- if (childX >= - hitSlopRect.left &&
335- childX < child.width + hitSlopRect.right &&
336- childY >= - hitSlopRect.top &&
337- childY < child.height + hitSlopRect.bottom) {
338-
339- // Only intercept if OUTSIDE normal bounds but WITHIN hitSlop
340- // This prevents interfering with normal child event handling
341- val inNormalBounds = childX >= 0 && childX < child.width &&
342- childY >= 0 && childY < child.height
343-
344- if (! inNormalBounds) {
345- // For TalkBack accessibility, request focus on the child
346- if (ev.action == MotionEvent .ACTION_HOVER_ENTER ) {
347- child.performAccessibilityAction(
348- android.view.accessibility.AccessibilityNodeInfo .ACTION_ACCESSIBILITY_FOCUS ,
349- null
350- )
351- return true
352- }
353- // Transform event coordinates to child's coordinate system
354- ev.offsetLocation(- child.left.toFloat(), - child.top.toFloat())
355- val handled = child.dispatchGenericMotionEvent(ev)
356- // Restore original coordinates
357- ev.offsetLocation(child.left.toFloat(), child.top.toFloat())
358- if (handled) {
359- return true
360- }
340+ // Use TouchTargetHelper to check if within hitSlop-extended bounds
341+ if (TouchTargetHelper .isTouchPointInView(childX, childY, child)) {
342+ // For TalkBack accessibility, request focus on the child
343+ if (ev.action == MotionEvent .ACTION_HOVER_ENTER ) {
344+ child.performAccessibilityAction(
345+ android.view.accessibility.AccessibilityNodeInfo .ACTION_ACCESSIBILITY_FOCUS ,
346+ null
347+ )
348+ return true
349+ }
350+ // Transform event coordinates to child's coordinate system
351+ ev.offsetLocation(- child.left.toFloat(), - child.top.toFloat())
352+ val handled = child.dispatchGenericMotionEvent(ev)
353+ // Restore original coordinates
354+ ev.offsetLocation(child.left.toFloat(), child.top.toFloat())
355+ if (handled) {
356+ return true
361357 }
362358 }
363359 }
@@ -632,6 +628,35 @@ public open class ReactViewGroup public constructor(context: Context?) :
632628 if (_removeClippedSubviews ) {
633629 updateClippingRect()
634630 }
631+
632+ // Initialize touch exploration state and register listener
633+ val accessibilityManager =
634+ context.getSystemService(Context .ACCESSIBILITY_SERVICE ) as ? AccessibilityManager
635+ if (accessibilityManager != null ) {
636+ // Query current state once and cache it
637+ isTouchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
638+
639+ // Register listener for future changes
640+ if (touchExplorationStateChangeListener == null ) {
641+ val listener =
642+ AccessibilityManager .TouchExplorationStateChangeListener { enabled ->
643+ isTouchExplorationEnabled = enabled
644+ }
645+ touchExplorationStateChangeListener = listener
646+ accessibilityManager.addTouchExplorationStateChangeListener(listener)
647+ }
648+ }
649+ }
650+
651+ override fun onDetachedFromWindow () {
652+ super .onDetachedFromWindow()
653+
654+ // Unregister touch exploration listener to avoid memory leaks
655+ val accessibilityManager =
656+ context.getSystemService(Context .ACCESSIBILITY_SERVICE ) as ? AccessibilityManager
657+ touchExplorationStateChangeListener?.let {
658+ accessibilityManager?.removeTouchExplorationStateChangeListener(it)
659+ }
635660 }
636661
637662 private fun customDrawOrderDisabled (): Boolean {
0 commit comments