From 749997794f7b0297f0b5986d889fde8404a373fc Mon Sep 17 00:00:00 2001 From: AndyG Date: Wed, 11 May 2022 13:46:23 -0700 Subject: [PATCH 1/4] have clicks working to show keyboard on android 7 --- .../react/views/textinput/ReactEditText.java | 26 +++++++ .../textinput/ReactEditTextClickDetector.java | 77 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index c3fd32dd896038..e0ba4bce720f11 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -123,6 +123,8 @@ public class ReactEditText extends AppCompatEditText private static final KeyListener sKeyListener = QwertyKeyListener.getInstanceForFullKeyboard(); private @Nullable EventDispatcher mEventDispatcher; + private final ReactEditTextClickDetector clickDetector = new ReactEditTextClickDetector(this); + public ReactEditText(Context context) { super(context); setFocusableInTouchMode(false); @@ -207,6 +209,21 @@ public boolean onTouchEvent(MotionEvent ev) { // Disallow parent views to intercept touch events, until we can detect if we should be // capturing these touches or not. this.getParent().requestDisallowInterceptTouchEvent(true); + clickDetector.handleDown(ev); + break; + case MotionEvent.ACTION_UP: + final boolean wasClick = clickDetector.handleUp(ev); + if (wasClick && forceShowKeyboardOnClicks() && isEnabled()) { + /* + It is intentional that we do not return true here. + We want to force the keyboard to show, but we still want to allow the user + to interact with the view in other ways (like changing the selection). + */ + showSoftKeyboard(); + } + break; + case MotionEvent.ACTION_CANCEL: + clickDetector.cancelPress(); break; case MotionEvent.ACTION_MOVE: if (mDetectScrollMovement) { @@ -1064,6 +1081,15 @@ void setEventDispatcher(@Nullable EventDispatcher eventDispatcher) { mEventDispatcher = eventDispatcher; } + /** + * There is a bug on Android 7/8/9 where clicking the view while it is already + * focused does not show the keyboard. On those API levels, we force showing + * the keyboard when we detect a click. + */ + private boolean forceShowKeyboardOnClicks() { + return Build.VERSION.SDK_INT <= Build.VERSION_CODES.P; + } + /** * This class will redirect *TextChanged calls to the listeners only in the case where the text is * changed by the user, and not explicitly set by JS. diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java new file mode 100644 index 00000000000000..fbfda04710243d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java @@ -0,0 +1,77 @@ +package com.facebook.react.views.textinput; + +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.Nullable; + +class ReactEditTextClickDetector { + + private static final long MAX_CLICK_DURATION_MS = 250L; + private static final int MAX_CLICK_DISTANCE_DP = 12; + + private final float screenDensity; + + @Nullable + private TimestampedMotionEvent currentDownEvent; + + public ReactEditTextClickDetector(final View view) { + screenDensity = view.getResources().getDisplayMetrics().density; + } + + void handleDown(final MotionEvent downEvent) { + currentDownEvent = new TimestampedMotionEvent(downEvent); + } + + void cancelPress() { + currentDownEvent = null; + } + + /** + * @return true if the event was a click. + */ + boolean handleUp(final MotionEvent upEvent) { + if (currentDownEvent == null) { + return false; + } + + final TimestampedMotionEvent downEvent = currentDownEvent; + currentDownEvent = null; + + // make sure the press event was close enough in time + final long now = System.currentTimeMillis(); + final long timeDelta = now - downEvent.timestamp; + if (timeDelta > MAX_CLICK_DURATION_MS) { + return false; + } + + // make sure the press event was close enough in distance + final float oldX = downEvent.motionEvent.getRawX(); + final float oldY = downEvent.motionEvent.getRawY(); + final float newX = upEvent.getRawX(); + final float newY = upEvent.getRawY(); + + // distance = sqrt((x2 − x1)^2 + (y2 − y1)^2) + final double distancePx = Math.sqrt( + Math.pow((newX - oldX), 2) + Math.pow((newY - oldY), 2) + ); + + double distanceDp = distancePx / screenDensity; + return distanceDp <= MAX_CLICK_DISTANCE_DP; + } + + private static class TimestampedMotionEvent { + + final long timestamp; + final MotionEvent motionEvent; + + public TimestampedMotionEvent(final long timestamp, final MotionEvent motionEvent) { + this.timestamp = timestamp; + this.motionEvent = motionEvent; + } + + public TimestampedMotionEvent(final MotionEvent motionEvent) { + this(System.currentTimeMillis(), motionEvent); + } + } +} From 5222c76fe51d3ff32ebd0b84370e1db101ba5bac Mon Sep 17 00:00:00 2001 From: AndyG Date: Wed, 11 May 2022 13:49:01 -0700 Subject: [PATCH 2/4] cleanups --- .../react/views/textinput/ReactEditTextClickDetector.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java index fbfda04710243d..a1c44ca7ea64a9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java @@ -20,7 +20,7 @@ public ReactEditTextClickDetector(final View view) { } void handleDown(final MotionEvent downEvent) { - currentDownEvent = new TimestampedMotionEvent(downEvent); + currentDownEvent = new TimestampedMotionEvent(System.currentTimeMillis(), downEvent); } void cancelPress() { @@ -65,13 +65,9 @@ private static class TimestampedMotionEvent { final long timestamp; final MotionEvent motionEvent; - public TimestampedMotionEvent(final long timestamp, final MotionEvent motionEvent) { + TimestampedMotionEvent(final long timestamp, final MotionEvent motionEvent) { this.timestamp = timestamp; this.motionEvent = motionEvent; } - - public TimestampedMotionEvent(final MotionEvent motionEvent) { - this(System.currentTimeMillis(), motionEvent); - } } } From 3cb71b5c025e07245b1d664c3bbe4d71b118c3c5 Mon Sep 17 00:00:00 2001 From: AndyG Date: Fri, 13 May 2022 10:02:34 -0700 Subject: [PATCH 3/4] extract method --- .../react/views/textinput/ReactEditText.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index e0ba4bce720f11..053bd941ca027b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -212,15 +212,7 @@ public boolean onTouchEvent(MotionEvent ev) { clickDetector.handleDown(ev); break; case MotionEvent.ACTION_UP: - final boolean wasClick = clickDetector.handleUp(ev); - if (wasClick && forceShowKeyboardOnClicks() && isEnabled()) { - /* - It is intentional that we do not return true here. - We want to force the keyboard to show, but we still want to allow the user - to interact with the view in other ways (like changing the selection). - */ - showSoftKeyboard(); - } + handleTouchUp(); break; case MotionEvent.ACTION_CANCEL: clickDetector.cancelPress(); @@ -1090,6 +1082,18 @@ private boolean forceShowKeyboardOnClicks() { return Build.VERSION.SDK_INT <= Build.VERSION_CODES.P; } + private void handleTouchUp() { + final boolean wasClick = clickDetector.handleUp(ev); + if (wasClick && forceShowKeyboardOnClicks() && isEnabled()) { + /* + It is intentional that we do not return true here from onTouchEvent. + We want to force the keyboard to show, but we still want to allow the user + to interact with the view in other ways (like changing the selection). + */ + showSoftKeyboard(); + } + } + /** * This class will redirect *TextChanged calls to the listeners only in the case where the text is * changed by the user, and not explicitly set by JS. From 603275327037d9965b5f3ba312f75f0b01b753c5 Mon Sep 17 00:00:00 2001 From: AndyG Date: Fri, 13 May 2022 10:18:46 -0700 Subject: [PATCH 4/4] refactors lol --- .../react/views/textinput/ReactEditText.java | 23 +---------- .../textinput/ReactEditTextClickDetector.java | 39 ++++++++++++++----- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 053bd941ca027b..230be23a2013b1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -212,7 +212,7 @@ public boolean onTouchEvent(MotionEvent ev) { clickDetector.handleDown(ev); break; case MotionEvent.ACTION_UP: - handleTouchUp(); + clickDetector.handleUp(ev); break; case MotionEvent.ACTION_CANCEL: clickDetector.cancelPress(); @@ -1073,27 +1073,6 @@ void setEventDispatcher(@Nullable EventDispatcher eventDispatcher) { mEventDispatcher = eventDispatcher; } - /** - * There is a bug on Android 7/8/9 where clicking the view while it is already - * focused does not show the keyboard. On those API levels, we force showing - * the keyboard when we detect a click. - */ - private boolean forceShowKeyboardOnClicks() { - return Build.VERSION.SDK_INT <= Build.VERSION_CODES.P; - } - - private void handleTouchUp() { - final boolean wasClick = clickDetector.handleUp(ev); - if (wasClick && forceShowKeyboardOnClicks() && isEnabled()) { - /* - It is intentional that we do not return true here from onTouchEvent. - We want to force the keyboard to show, but we still want to allow the user - to interact with the view in other ways (like changing the selection). - */ - showSoftKeyboard(); - } - } - /** * This class will redirect *TextChanged calls to the listeners only in the case where the text is * changed by the user, and not explicitly set by JS. diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java index a1c44ca7ea64a9..74fdce25d21827 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextClickDetector.java @@ -1,5 +1,6 @@ package com.facebook.react.views.textinput; +import android.os.Build; import android.view.MotionEvent; import android.view.View; @@ -10,39 +11,44 @@ class ReactEditTextClickDetector { private static final long MAX_CLICK_DURATION_MS = 250L; private static final int MAX_CLICK_DISTANCE_DP = 12; + private final ReactEditText reactEditText; private final float screenDensity; @Nullable private TimestampedMotionEvent currentDownEvent; - public ReactEditTextClickDetector(final View view) { - screenDensity = view.getResources().getDisplayMetrics().density; + public ReactEditTextClickDetector(final ReactEditText reactEditText) { + this.reactEditText = reactEditText; + screenDensity = reactEditText.getResources().getDisplayMetrics().density; } void handleDown(final MotionEvent downEvent) { - currentDownEvent = new TimestampedMotionEvent(System.currentTimeMillis(), downEvent); + currentDownEvent = new TimestampedMotionEvent(downEvent); } void cancelPress() { currentDownEvent = null; } - /** - * @return true if the event was a click. - */ - boolean handleUp(final MotionEvent upEvent) { + void handleUp(final MotionEvent upEvent) { if (currentDownEvent == null) { - return false; + return; } final TimestampedMotionEvent downEvent = currentDownEvent; currentDownEvent = null; + // for now, if we're not forcing showing the keyboard on clicks, we don't care if it was a + // click. we also early return if the view is not enabled. + if (!(forceShowKeyboardOnClicks() && reactEditText.isEnabled())) { + return; + } + // make sure the press event was close enough in time final long now = System.currentTimeMillis(); final long timeDelta = now - downEvent.timestamp; if (timeDelta > MAX_CLICK_DURATION_MS) { - return false; + return; } // make sure the press event was close enough in distance @@ -57,7 +63,20 @@ boolean handleUp(final MotionEvent upEvent) { ); double distanceDp = distancePx / screenDensity; - return distanceDp <= MAX_CLICK_DISTANCE_DP; + if (distanceDp > MAX_CLICK_DISTANCE_DP) { + return; + } + + reactEditText.showSoftKeyboard(); + } + + /** + * There is a bug on Android 7/8/9 where clicking the view while it is already + * focused does not show the keyboard. On those API levels, we force showing + * the keyboard when we detect a click. + */ + private static boolean forceShowKeyboardOnClicks() { + return Build.VERSION.SDK_INT <= Build.VERSION_CODES.P; } private static class TimestampedMotionEvent {