From 5c7118a75c63c1ae07c61d4baf760ffbfe7380d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= <63123542+m-bert@users.noreply.github.com> Date: Thu, 3 Aug 2023 17:04:41 +0200 Subject: [PATCH] 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. --- .../gesturehandler/core/GestureHandler.kt | 6 +++ .../core/GestureHandlerOrchestrator.kt | 15 +++++++ .../gesturehandler/react/Extensions.kt | 5 +++ .../RNGestureHandlerButtonViewManager.kt | 43 ++++++++++++++++--- .../react/RNGestureHandlerRootHelper.kt | 5 +++ .../react/RNGestureHandlerRootView.kt | 5 +++ 6 files changed, 74 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt b/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt index c9527fb958..082bfffc42 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt @@ -706,6 +706,12 @@ open class GestureHandler Unit) { + isWithinBounds = true + closure() + isWithinBounds = false + } + fun setOnTouchEventListener(listener: OnTouchEventListener?): GestureHandler<*> { onTouchEventListener = listener return this diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index 2407c8a271..6116617ed2 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -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 diff --git a/android/src/main/java/com/swmansion/gesturehandler/react/Extensions.kt b/android/src/main/java/com/swmansion/gesturehandler/react/Extensions.kt index 5f00002d18..c7791411a2 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/react/Extensions.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/react/Extensions.kt @@ -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 @@ -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 diff --git a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt b/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt index 1bec0762e8..9c9e9eeaf0 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt @@ -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 @@ -119,6 +121,7 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager(), R private var needBackgroundUpdate = false private var lastEventTime = -1L private var lastAction = -1 + private var receivedKeyEvent = false var isTouched = false @@ -333,13 +336,30 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager(), 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 } @@ -356,7 +376,6 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager(), 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() @@ -378,6 +397,20 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager(), 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 diff --git a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt index 5e8403d8a4..b20ba42229 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt @@ -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 @@ -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 { diff --git a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt b/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt index 953d3f3d74..c6c456e10c 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt @@ -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 @@ -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()