From 5e0371885db750844989981db53c0929e98d9baf Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Wed, 8 Jul 2020 10:50:48 +0300 Subject: [PATCH] Fix overlay touch detection (#6372) When the Overlay was displayed with `statusBar.drawBehind: false`, the StatusBar height wasn't accounted for when calculating if touch events were inside the view's bounds. To begin with, StatusBar height had to be accounted for because we used `MotionEvent.rawY` coordinates instead of `MotionEvent.y` coordinate which already take into account the view's position within the hierarchy, which compensates for StatusBar height. fixes #5976 --- lib/android/app/build.gradle | 7 ++- .../utils/MotionEvent.kt | 13 +++++ .../views/touch/OverlayTouchDelegate.java | 49 ------------------- .../views/touch/OverlayTouchDelegate.kt | 26 ++++++++++ .../utils/MotionEventTest.kt | 49 +++++++++++++++++++ ...est.java => OverlayTouchDelegateTest.java} | 12 ++--- playground/android/build.gradle | 2 +- playground/src/screens/Toast.tsx | 10 +++- 8 files changed, 108 insertions(+), 60 deletions(-) create mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt delete mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.java create mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.kt create mode 100644 lib/android/app/src/test/java/com/reactnativenavigation/utils/MotionEventTest.kt rename lib/android/app/src/test/java/com/reactnativenavigation/views/{TouchDelegateTest.java => OverlayTouchDelegateTest.java} (85%) diff --git a/lib/android/app/build.gradle b/lib/android/app/build.gradle index 0ec25726328..be8a3781f4e 100644 --- a/lib/android/app/build.gradle +++ b/lib/android/app/build.gradle @@ -59,6 +59,9 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } flavorDimensions "RNN.reactNativeVersion" productFlavors { @@ -154,7 +157,7 @@ allprojects { p -> } dependencies { - implementation "androidx.core:core-ktx:1.1.0" + implementation "androidx.core:core-ktx:1.3.0" implementation "org.jetbrains.kotlin:$kotlinStdlib:$kotlinVersion" implementation 'androidx.appcompat:appcompat:1.1.0' @@ -174,5 +177,7 @@ dependencies { testImplementation 'org.assertj:assertj-core:3.8.0' testImplementation 'com.squareup.assertj:assertj-android:1.1.1' testImplementation 'org.mockito:mockito-core:2.28.2' + testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" + testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlinVersion" } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt b/lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt new file mode 100644 index 00000000000..a79e487712f --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt @@ -0,0 +1,13 @@ +package com.reactnativenavigation.utils + +import android.graphics.Rect +import android.view.MotionEvent +import android.view.View + +val hitRect = Rect() + +fun MotionEvent.coordinatesInsideView(view: View?): Boolean { + view ?: return false + view.getHitRect(hitRect) + return hitRect.contains(x.toInt(), y.toInt()) +} \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.java b/lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.java deleted file mode 100644 index e298f7a866e..00000000000 --- a/lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.reactnativenavigation.views.touch; - -import android.graphics.Rect; -import android.view.MotionEvent; -import android.view.View; - -import com.reactnativenavigation.options.params.Bool; -import com.reactnativenavigation.options.params.NullBool; -import com.reactnativenavigation.viewcontrollers.viewcontroller.IReactView; -import com.reactnativenavigation.views.component.ComponentLayout; - -import androidx.annotation.VisibleForTesting; - -public class OverlayTouchDelegate { - private final Rect hitRect = new Rect(); - private Bool interceptTouchOutside = new NullBool(); - private ComponentLayout component; - private IReactView reactView; - - public void setInterceptTouchOutside(Bool interceptTouchOutside) { - this.interceptTouchOutside = interceptTouchOutside; - } - - public OverlayTouchDelegate(ComponentLayout component, IReactView reactView) { - this.component = component; - this.reactView = reactView; - } - - public boolean onInterceptTouchEvent(MotionEvent event) { - return interceptTouchOutside.hasValue() && event.getActionMasked() == MotionEvent.ACTION_DOWN ? - handleDown(event) : - component.superOnInterceptTouchEvent(event); - } - - @VisibleForTesting - public boolean handleDown(MotionEvent event) { - if (isTouchInsideOverlay(event)) return component.superOnInterceptTouchEvent(event); - return interceptTouchOutside.isFalse(); - } - - private boolean isTouchInsideOverlay(MotionEvent ev) { - getOverlayView().getHitRect(hitRect); - return hitRect.contains((int) ev.getRawX(), (int) ev.getRawY()); - } - - private View getOverlayView() { - return reactView.asView().getChildCount() > 0 ? reactView.asView().getChildAt(0) : reactView.asView(); - } -} diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.kt b/lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.kt new file mode 100644 index 00000000000..2c789aee30c --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.kt @@ -0,0 +1,26 @@ +package com.reactnativenavigation.views.touch + +import android.view.MotionEvent +import androidx.annotation.VisibleForTesting +import com.reactnativenavigation.options.params.Bool +import com.reactnativenavigation.options.params.NullBool +import com.reactnativenavigation.react.ReactView +import com.reactnativenavigation.utils.coordinatesInsideView +import com.reactnativenavigation.views.component.ComponentLayout + +open class OverlayTouchDelegate(private val component: ComponentLayout, private val reactView: ReactView) { + var interceptTouchOutside: Bool = NullBool() + + fun onInterceptTouchEvent(event: MotionEvent): Boolean { + return when (interceptTouchOutside.hasValue() && event.actionMasked == MotionEvent.ACTION_DOWN) { + true -> handleDown(event) + false -> component.superOnInterceptTouchEvent(event) + } + } + + @VisibleForTesting + open fun handleDown(event: MotionEvent) = when (event.coordinatesInsideView(reactView.getChildAt(0))) { + true -> component.superOnInterceptTouchEvent(event) + false -> interceptTouchOutside.isFalse + } +} \ No newline at end of file diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/utils/MotionEventTest.kt b/lib/android/app/src/test/java/com/reactnativenavigation/utils/MotionEventTest.kt new file mode 100644 index 00000000000..48d8b6e200c --- /dev/null +++ b/lib/android/app/src/test/java/com/reactnativenavigation/utils/MotionEventTest.kt @@ -0,0 +1,49 @@ +package com.reactnativenavigation.utils + +import android.app.Activity +import android.view.MotionEvent +import android.view.View +import android.view.View.MeasureSpec +import android.widget.FrameLayout +import androidx.core.view.marginTop +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import com.reactnativenavigation.BaseTest +import org.assertj.core.api.Java6Assertions.assertThat +import org.junit.Test + +class MotionEventTest : BaseTest() { + private lateinit var uut: MotionEvent + private lateinit var activity: Activity + private lateinit var parent: FrameLayout + private val x = 173f + private val y = 249f + + override fun beforeEach() { + uut = MotionEvent.obtain(0L, 0, 0, x, y, 0) + activity = newActivity() + parent = FrameLayout(activity) + activity.setContentView(parent) + } + + @Test + fun coordinatesInsideView() { + val v: View = mock() + assertThat(uut.coordinatesInsideView(v)).isFalse() + } + + @Test + fun coordinatesInsideView_inside() { + val view = View(activity) + parent.addView(view, 200, 300) + assertThat(uut.coordinatesInsideView(view)).isTrue() + } + + @Test + fun coordinatesInsideView_outside() { + val view = View(activity) + parent.addView(view, 200, 300) + view.top = (y + 1).toInt() + assertThat(uut.coordinatesInsideView(view)).isFalse() + } +} \ No newline at end of file diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/views/TouchDelegateTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/views/OverlayTouchDelegateTest.java similarity index 85% rename from lib/android/app/src/test/java/com/reactnativenavigation/views/TouchDelegateTest.java rename to lib/android/app/src/test/java/com/reactnativenavigation/views/OverlayTouchDelegateTest.java index 90943f9f592..8858f0a4561 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/views/TouchDelegateTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/views/OverlayTouchDelegateTest.java @@ -3,33 +3,31 @@ import android.view.MotionEvent; import com.reactnativenavigation.BaseTest; -import com.reactnativenavigation.mocks.SimpleOverlay; import com.reactnativenavigation.options.params.Bool; +import com.reactnativenavigation.react.ReactView; import com.reactnativenavigation.views.component.ComponentLayout; import com.reactnativenavigation.views.touch.OverlayTouchDelegate; import org.junit.Test; -import org.mockito.Mockito; import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -public class TouchDelegateTest extends BaseTest { +public class OverlayTouchDelegateTest extends BaseTest { private OverlayTouchDelegate uut; private final int x = 10; private final int y = 10; private final MotionEvent downEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x, y, 0); private final MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, x, y, 0); - private SimpleOverlay reactView; private ComponentLayout component; @Override public void beforeEach() { - super.beforeEach(); - reactView = spy(new SimpleOverlay(newActivity())); - component = Mockito.mock(ComponentLayout.class); + ReactView reactView = mock(ReactView.class); + component = mock(ComponentLayout.class); uut = spy(new OverlayTouchDelegate(component, reactView)); } diff --git a/playground/android/build.gradle b/playground/android/build.gradle index 6a97033c0e9..fb6ec9a9980 100644 --- a/playground/android/build.gradle +++ b/playground/android/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - kotlinVersion = "1.3.61" + kotlinVersion = "1.3.72" RNNKotlinVersion = kotlinVersion detoxKotlinVersion = kotlinVersion } diff --git a/playground/src/screens/Toast.tsx b/playground/src/screens/Toast.tsx index 3bd259ece76..4894db4b67d 100644 --- a/playground/src/screens/Toast.tsx +++ b/playground/src/screens/Toast.tsx @@ -15,7 +15,11 @@ export default function Toast({ componentId }: NavigationComponentProps) { return ( - dismiss('Outer button clicked')}> + dismiss('Outer button clicked')} + > This a very important message!