Skip to content

Commit 5c7118a

Browse files
authored
Android TalkBack fix (#2234)
This PR solves issue when buttons don't activate after enabling TalkBack. When TalkBack is enabled, buttons don't receive MotionEvents, hence they don't go through orchestrator. However, performClick method is still being called. That allows us to manually activate required handlers. These changes should also allow navigating with keyboard and fix activation of nested buttons.
1 parent 0532309 commit 5c7118a

File tree

6 files changed

+74
-5
lines changed

6 files changed

+74
-5
lines changed

android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,12 @@ open class GestureHandler<ConcreteGestureHandlerT : GestureHandler<ConcreteGestu
706706
onReset()
707707
}
708708

709+
fun withMarkedAsInBounds(closure: () -> Unit) {
710+
isWithinBounds = true
711+
closure()
712+
isWithinBounds = false
713+
}
714+
709715
fun setOnTouchEventListener(listener: OnTouchEventListener?): GestureHandler<*> {
710716
onTouchEventListener = listener
711717
return this

android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,21 @@ class GestureHandlerOrchestrator(
580580
private fun isClipping(view: View) =
581581
view !is ViewGroup || viewConfigHelper.isViewClippingChildren(view)
582582

583+
fun activateNativeHandlersForView(view: View) {
584+
handlerRegistry.getHandlersForView(view)?.forEach {
585+
if (it !is NativeViewGestureHandler) {
586+
return@forEach
587+
}
588+
this.recordHandlerIfNotPresent(it, view)
589+
590+
it.withMarkedAsInBounds {
591+
it.begin()
592+
it.activate()
593+
it.end()
594+
}
595+
}
596+
}
597+
583598
companion object {
584599
// The limit doesn't necessarily need to exists, it was just simpler to implement it that way
585600
// it is also more allocation-wise efficient to have a fixed limit

android/src/main/java/com/swmansion/gesturehandler/react/Extensions.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.swmansion.gesturehandler.react
22

3+
import android.content.Context
4+
import android.view.accessibility.AccessibilityManager
35
import com.facebook.react.bridge.ReactContext
46
import com.facebook.react.modules.core.DeviceEventManagerModule
57
import com.facebook.react.uimanager.UIManagerModule
@@ -9,3 +11,6 @@ val ReactContext.deviceEventEmitter: DeviceEventManagerModule.RCTDeviceEventEmit
911

1012
val ReactContext.UIManager: UIManagerModule
1113
get() = this.getNativeModule(UIManagerModule::class.java)!!
14+
15+
fun Context.isScreenReaderOn() =
16+
(getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager).isEnabled

android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import android.graphics.drawable.ShapeDrawable
1313
import android.graphics.drawable.shapes.RectShape
1414
import android.os.Build
1515
import android.util.TypedValue
16+
import android.view.KeyEvent
1617
import android.view.MotionEvent
1718
import android.view.View
1819
import android.view.View.OnClickListener
1920
import android.view.ViewGroup
21+
import android.view.ViewParent
2022
import androidx.core.view.children
2123
import com.facebook.react.module.annotations.ReactModule
2224
import com.facebook.react.uimanager.PixelUtil
@@ -119,6 +121,7 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
119121
private var needBackgroundUpdate = false
120122
private var lastEventTime = -1L
121123
private var lastAction = -1
124+
private var receivedKeyEvent = false
122125

123126
var isTouched = false
124127

@@ -333,13 +336,30 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
333336
return false
334337
}
335338

339+
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
340+
receivedKeyEvent = true
341+
return super.onKeyUp(keyCode, event)
342+
}
343+
336344
override fun performClick(): Boolean {
337345
// don't preform click when a child button is pressed (mainly to prevent sound effect of
338346
// a parent button from playing)
339-
return if (!isChildTouched() && soundResponder == this) {
340-
tryFreeingResponder()
341-
soundResponder = null
342-
super.performClick()
347+
return if (!isChildTouched()) {
348+
349+
if (context.isScreenReaderOn()) {
350+
findGestureHandlerRootView()?.activateNativeHandlers(this)
351+
} else if (receivedKeyEvent) {
352+
findGestureHandlerRootView()?.activateNativeHandlers(this)
353+
receivedKeyEvent = false
354+
}
355+
356+
if (soundResponder === this) {
357+
tryFreeingResponder()
358+
soundResponder = null
359+
super.performClick()
360+
} else {
361+
false
362+
}
343363
} else {
344364
false
345365
}
@@ -356,7 +376,6 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
356376
soundResponder = this
357377
}
358378
}
359-
360379
// button can be pressed alongside other button if both are non-exclusive and it doesn't have
361380
// any pressed children (to prevent pressing the parent when children is pressed).
362381
val canBePressedAlongsideOther = !exclusive && touchResponder?.exclusive != true && !isChildTouched()
@@ -378,6 +397,20 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
378397
// by default Viewgroup would pass hotspot change events
379398
}
380399

400+
private fun findGestureHandlerRootView(): RNGestureHandlerRootView? {
401+
var parent: ViewParent? = this.parent
402+
var gestureHandlerRootView: RNGestureHandlerRootView? = null
403+
404+
while (parent != null) {
405+
if (parent is RNGestureHandlerRootView) {
406+
gestureHandlerRootView = parent
407+
}
408+
parent = parent.parent
409+
}
410+
411+
return gestureHandlerRootView
412+
}
413+
381414
companion object {
382415
var resolveOutValue = TypedValue()
383416
var touchResponder: ButtonViewGroup? = null

android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.swmansion.gesturehandler.react
33
import android.os.SystemClock
44
import android.util.Log
55
import android.view.MotionEvent
6+
import android.view.View
67
import android.view.ViewGroup
78
import android.view.ViewParent
89
import com.facebook.react.bridge.ReactContext
@@ -118,6 +119,10 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
118119
}
119120
}
120121

122+
fun activateNativeHandlers(view: View) {
123+
orchestrator?.activateNativeHandlersForView(view)
124+
}
125+
121126
companion object {
122127
private const val MIN_ALPHA_FOR_TOUCH = 0.1f
123128
private fun findRootViewTag(viewGroup: ViewGroup): ViewGroup {

android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.swmansion.gesturehandler.react
33
import android.content.Context
44
import android.util.Log
55
import android.view.MotionEvent
6+
import android.view.View
67
import android.view.ViewGroup
78
import com.facebook.react.bridge.ReactContext
89
import com.facebook.react.bridge.UiThreadUtil
@@ -43,6 +44,10 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
4344
super.requestDisallowInterceptTouchEvent(disallowIntercept)
4445
}
4546

47+
fun activateNativeHandlers(view: View) {
48+
rootHelper?.activateNativeHandlers(view)
49+
}
50+
4651
companion object {
4752
private fun hasGestureHandlerEnabledRootView(viewGroup: ViewGroup): Boolean {
4853
UiThreadUtil.assertOnUiThread()

0 commit comments

Comments
 (0)