Skip to content

Commit

Permalink
Android TalkBack fix (#2234)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
m-bert authored Aug 3, 2023
1 parent 0532309 commit 5c7118a
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,12 @@ open class GestureHandler<ConcreteGestureHandlerT : GestureHandler<ConcreteGestu
onReset()
}

fun withMarkedAsInBounds(closure: () -> Unit) {
isWithinBounds = true
closure()
isWithinBounds = false
}

fun setOnTouchEventListener(listener: OnTouchEventListener?): GestureHandler<*> {
onTouchEventListener = listener
return this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,21 @@ class GestureHandlerOrchestrator(
private fun isClipping(view: View) =
view !is ViewGroup || viewConfigHelper.isViewClippingChildren(view)

fun activateNativeHandlersForView(view: View) {
handlerRegistry.getHandlersForView(view)?.forEach {
if (it !is NativeViewGestureHandler) {
return@forEach
}
this.recordHandlerIfNotPresent(it, view)

it.withMarkedAsInBounds {
it.begin()
it.activate()
it.end()
}
}
}

companion object {
// The limit doesn't necessarily need to exists, it was just simpler to implement it that way
// it is also more allocation-wise efficient to have a fixed limit
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.swmansion.gesturehandler.react

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

val ReactContext.UIManager: UIManagerModule
get() = this.getNativeModule(UIManagerModule::class.java)!!

fun Context.isScreenReaderOn() =
(getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager).isEnabled
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import android.os.Build
import android.util.TypedValue
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.view.ViewParent
import androidx.core.view.children
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.PixelUtil
Expand Down Expand Up @@ -119,6 +121,7 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
private var needBackgroundUpdate = false
private var lastEventTime = -1L
private var lastAction = -1
private var receivedKeyEvent = false

var isTouched = false

Expand Down Expand Up @@ -333,13 +336,30 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
return false
}

override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
receivedKeyEvent = true
return super.onKeyUp(keyCode, event)
}

override fun performClick(): Boolean {
// don't preform click when a child button is pressed (mainly to prevent sound effect of
// a parent button from playing)
return if (!isChildTouched() && soundResponder == this) {
tryFreeingResponder()
soundResponder = null
super.performClick()
return if (!isChildTouched()) {

if (context.isScreenReaderOn()) {
findGestureHandlerRootView()?.activateNativeHandlers(this)
} else if (receivedKeyEvent) {
findGestureHandlerRootView()?.activateNativeHandlers(this)
receivedKeyEvent = false
}

if (soundResponder === this) {
tryFreeingResponder()
soundResponder = null
super.performClick()
} else {
false
}
} else {
false
}
Expand All @@ -356,7 +376,6 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
soundResponder = this
}
}

// button can be pressed alongside other button if both are non-exclusive and it doesn't have
// any pressed children (to prevent pressing the parent when children is pressed).
val canBePressedAlongsideOther = !exclusive && touchResponder?.exclusive != true && !isChildTouched()
Expand All @@ -378,6 +397,20 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
// by default Viewgroup would pass hotspot change events
}

private fun findGestureHandlerRootView(): RNGestureHandlerRootView? {
var parent: ViewParent? = this.parent
var gestureHandlerRootView: RNGestureHandlerRootView? = null

while (parent != null) {
if (parent is RNGestureHandlerRootView) {
gestureHandlerRootView = parent
}
parent = parent.parent
}

return gestureHandlerRootView
}

companion object {
var resolveOutValue = TypedValue()
var touchResponder: ButtonViewGroup? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.swmansion.gesturehandler.react
import android.os.SystemClock
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewParent
import com.facebook.react.bridge.ReactContext
Expand Down Expand Up @@ -118,6 +119,10 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
}
}

fun activateNativeHandlers(view: View) {
orchestrator?.activateNativeHandlersForView(view)
}

companion object {
private const val MIN_ALPHA_FOR_TOUCH = 0.1f
private fun findRootViewTag(viewGroup: ViewGroup): ViewGroup {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.swmansion.gesturehandler.react
import android.content.Context
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.UiThreadUtil
Expand Down Expand Up @@ -43,6 +44,10 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
super.requestDisallowInterceptTouchEvent(disallowIntercept)
}

fun activateNativeHandlers(view: View) {
rootHelper?.activateNativeHandlers(view)
}

companion object {
private fun hasGestureHandlerEnabledRootView(viewGroup: ViewGroup): Boolean {
UiThreadUtil.assertOnUiThread()
Expand Down

0 comments on commit 5c7118a

Please sign in to comment.