|
| 1 | +package com.facebook.react.views.textinput; |
| 2 | + |
| 3 | +import android.os.Build; |
| 4 | +import android.view.MotionEvent; |
| 5 | +import android.view.View; |
| 6 | + |
| 7 | +import androidx.annotation.Nullable; |
| 8 | + |
| 9 | +class ReactEditTextClickDetector { |
| 10 | + |
| 11 | + private static final long MAX_CLICK_DURATION_MS = 250L; |
| 12 | + private static final int MAX_CLICK_DISTANCE_DP = 12; |
| 13 | + |
| 14 | + private final ReactEditText reactEditText; |
| 15 | + private final float screenDensity; |
| 16 | + |
| 17 | + @Nullable |
| 18 | + private TimestampedMotionEvent currentDownEvent; |
| 19 | + |
| 20 | + public ReactEditTextClickDetector(final ReactEditText reactEditText) { |
| 21 | + this.reactEditText = reactEditText; |
| 22 | + screenDensity = reactEditText.getResources().getDisplayMetrics().density; |
| 23 | + } |
| 24 | + |
| 25 | + void handleDown(final MotionEvent downEvent) { |
| 26 | + currentDownEvent = new TimestampedMotionEvent(System.currentTimeMillis(), downEvent); |
| 27 | + } |
| 28 | + |
| 29 | + void cancelPress() { |
| 30 | + currentDownEvent = null; |
| 31 | + } |
| 32 | + |
| 33 | + void handleUp(final MotionEvent upEvent) { |
| 34 | + if (currentDownEvent == null) { |
| 35 | + return; |
| 36 | + } |
| 37 | + |
| 38 | + final TimestampedMotionEvent downEvent = currentDownEvent; |
| 39 | + currentDownEvent = null; |
| 40 | + |
| 41 | + // for now, if we're not forcing showing the keyboard on clicks, we don't care if it was a |
| 42 | + // click. we also early return if the view is not enabled. |
| 43 | + if (!(forceShowKeyboardOnClicks() && reactEditText.isEnabled())) { |
| 44 | + return; |
| 45 | + } |
| 46 | + |
| 47 | + // make sure the press event was close enough in time |
| 48 | + final long now = System.currentTimeMillis(); |
| 49 | + final long timeDelta = now - downEvent.timestamp; |
| 50 | + if (timeDelta > MAX_CLICK_DURATION_MS) { |
| 51 | + return; |
| 52 | + } |
| 53 | + |
| 54 | + // make sure the press event was close enough in distance |
| 55 | + final float oldX = downEvent.motionEvent.getRawX(); |
| 56 | + final float oldY = downEvent.motionEvent.getRawY(); |
| 57 | + final float newX = upEvent.getRawX(); |
| 58 | + final float newY = upEvent.getRawY(); |
| 59 | + |
| 60 | + // distance = sqrt((x2 − x1)^2 + (y2 − y1)^2) |
| 61 | + final double distancePx = Math.sqrt( |
| 62 | + Math.pow((newX - oldX), 2) + Math.pow((newY - oldY), 2) |
| 63 | + ); |
| 64 | + |
| 65 | + double distanceDp = distancePx / screenDensity; |
| 66 | + if (distanceDp > MAX_CLICK_DISTANCE_DP) { |
| 67 | + return; |
| 68 | + } |
| 69 | + |
| 70 | + reactEditText.showSoftKeyboard(); |
| 71 | + } |
| 72 | + |
| 73 | + /** |
| 74 | + * There is a bug on Android 7/8/9 where clicking the view while it is already |
| 75 | + * focused does not show the keyboard. On those API levels, we force showing |
| 76 | + * the keyboard when we detect a click. |
| 77 | + */ |
| 78 | + private static boolean forceShowKeyboardOnClicks() { |
| 79 | + return Build.VERSION.SDK_INT <= Build.VERSION_CODES.P; |
| 80 | + } |
| 81 | + |
| 82 | + private static class TimestampedMotionEvent { |
| 83 | + |
| 84 | + final long timestamp; |
| 85 | + final MotionEvent motionEvent; |
| 86 | + |
| 87 | + TimestampedMotionEvent(final long timestamp, final MotionEvent motionEvent) { |
| 88 | + this.timestamp = timestamp; |
| 89 | + this.motionEvent = motionEvent; |
| 90 | + } |
| 91 | + } |
| 92 | +} |
0 commit comments