Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: take gravity into consideration when calculating selection coordinates on Android #749

Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import android.os.Build
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.view.Gravity
import android.view.View
import android.view.ViewTreeObserver.OnPreDrawListener
import android.widget.EditText
import com.facebook.react.views.scroll.ReactScrollView
import com.facebook.react.views.textinput.ReactEditText
import com.reactnativekeyboardcontroller.log.Logger
import com.reactnativekeyboardcontroller.ui.FrameScheduler
import java.lang.reflect.Field
import kotlin.math.max
import kotlin.math.min
Expand Down Expand Up @@ -151,69 +152,84 @@ class KeyboardControllerSelectionWatcher(
) {
private var lastSelectionStart: Int = -1
private var lastSelectionEnd: Int = -1
private var lastEditTextHeight: Int = -1

private val frameScheduler =
FrameScheduler {
val start = editText.selectionStart
val end = editText.selectionEnd

if (lastSelectionStart != start || lastSelectionEnd != end) {
lastSelectionStart = start
lastSelectionEnd = end
private val preDrawListener: OnPreDrawListener =
object : OnPreDrawListener {
override fun onPreDraw(): Boolean {
val start = editText.selectionStart
val end = editText.selectionEnd
val editTextHeight = editText.height

val view = editText
val layout = view.layout

if (layout === null) {
return@FrameScheduler
return true
}

if (lastSelectionStart != start || lastSelectionEnd != end || lastEditTextHeight != editTextHeight) {
lastSelectionStart = start
lastSelectionEnd = end
lastEditTextHeight = editTextHeight

val cursorPositionStartX: Float
val cursorPositionStartY: Float
val cursorPositionEndX: Float
val cursorPositionEndY: Float

val realStart = min(start, end)
val realEnd = max(start, end)

val lineStart = layout.getLineForOffset(realStart)
val baselineStart = layout.getLineTop(lineStart)

val textHeight = layout.height
val cursorWidth =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
view.textCursorDrawable?.intrinsicWidth ?: 0
} else {
0
}
val gravity = editText.gravity and Gravity.VERTICAL_GRAVITY_MASK
val verticalOffset =
when (gravity) {
Gravity.CENTER_VERTICAL -> (editTextHeight - textHeight) / 2 + editText.paddingTop
Gravity.BOTTOM -> editTextHeight - textHeight
// Default to Gravity.TOP or other cases
else -> editText.paddingTop * 2
}

cursorPositionStartX = layout.getPrimaryHorizontal(realStart)
cursorPositionStartY = (baselineStart + verticalOffset).toFloat()

val lineEnd = layout.getLineForOffset(realEnd)
val right = layout.getPrimaryHorizontal(realEnd)
val bottom = layout.getLineBottom(lineEnd)

cursorPositionEndX = right + cursorWidth
cursorPositionEndY = (bottom + verticalOffset).toFloat()

action(
start,
end,
cursorPositionStartX.dp,
cursorPositionStartY.dp,
cursorPositionEndX.dp,
cursorPositionEndY.dp,
)
}

val cursorPositionStartX: Float
val cursorPositionStartY: Float
val cursorPositionEndX: Float
val cursorPositionEndY: Float

val realStart = min(start, end)
val realEnd = max(start, end)

val lineStart = layout.getLineForOffset(realStart)
val baselineStart = layout.getLineBaseline(lineStart)
val ascentStart = layout.getLineAscent(lineStart)

cursorPositionStartX = layout.getPrimaryHorizontal(realStart)
cursorPositionStartY = (baselineStart + ascentStart).toFloat()

val lineEnd = layout.getLineForOffset(realEnd)

val right = layout.getPrimaryHorizontal(realEnd)
val bottom = layout.getLineBottom(lineEnd) + layout.getLineAscent(lineEnd)
val cursorWidth =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
view.textCursorDrawable?.intrinsicWidth ?: 0
} else {
0
}

cursorPositionEndX = right + cursorWidth
cursorPositionEndY = bottom.toFloat()

action(
start,
end,
cursorPositionStartX.dp,
cursorPositionStartY.dp,
cursorPositionEndX.dp,
cursorPositionEndY.dp,
)
return true
}
}

fun setup() {
frameScheduler.start()
editText.viewTreeObserver.addOnPreDrawListener(preDrawListener)
}

fun destroy() {
frameScheduler.stop()
editText.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
}
}

Expand Down

This file was deleted.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 17 additions & 20 deletions e2e/kit/helpers/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import colors from "colors/safe";
import { expect } from "detox";

import { waitForElementById } from "../awaitable";
import { getDevicePreference } from "../env/devicePreferences";
Expand Down Expand Up @@ -128,30 +129,26 @@ export const scrollDownUntilElementIsVisible = async (
.scroll(100, "down", NaN, 0.5);
};

type Frame = {
x: number;
y: number;
width: number;
height: number;
};

export const scrollUpUntilElementIsBarelyVisible = async (
scrollViewId: string,
elementId: string,
threshold = 10,
): Promise<void> => {
const preference = getDevicePreference();
const { frame } = (await element(by.id(elementId)).getAttributes()) as {
frame: Frame;
};
const distance =
preference.height -
preference.keyboard -
frame.y -
frame.height -
threshold;

await element(by.id(scrollViewId)).scroll(distance, "up", 0.01, 0.5);
for (;;) {
await element(by.id(scrollViewId)).scroll(50, "up", 0.01, 0.5);

try {
// verify that we can interact with element
if (device.getPlatform() === "ios") {
await expect(element(by.id(elementId))).toBeVisible();
} else {
// on Android visible is always true
await element(by.id(elementId)).tap({ x: 0, y: 25 });
}
} catch (e) {
await element(by.id(scrollViewId)).scroll(35, "down", 0.01, 0.5);
break;
}
}
};

export const closeKeyboard = async (textInputId: string) => {
Expand Down
7 changes: 0 additions & 7 deletions e2e/kit/helpers/env/devicePreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,37 @@ import parseDeviceName from "../../utils/parseDeviceName";

type Preference = {
emojiButtonCoordinates?: { x: number; y: number };
keyboard: number;
width: number;
height: number;
};

const DEVICE_PREFERENCES: Record<string, Preference> = {
"e2e_emulator_28": {
keyboard: 980,
emojiButtonCoordinates: undefined,
width: 1080,
height: 1920,
},
"e2e_emulator_31": {
keyboard: 900,
emojiButtonCoordinates: { x: 324, y: 1704 },
width: 1080,
height: 1920,
},
"iPhone 16 Pro": {
keyboard: 291,
emojiButtonCoordinates: { x: 40, y: 830 },
width: 393,
height: 852,
},
"iPhone 15 Pro": {
keyboard: 291,
emojiButtonCoordinates: { x: 40, y: 830 },
width: 393,
height: 852,
},
"iPhone 14 Pro": {
keyboard: 291,
emojiButtonCoordinates: { x: 40, y: 830 },
width: 393,
height: 852,
},
"iPhone 13 Pro": {
keyboard: 286,
emojiButtonCoordinates: { x: 40, y: 830 },
width: 390,
height: 844,
Expand Down
2 changes: 1 addition & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@types/pngjs": "^6.0.1",
"async-retry": "^1.3.3",
"colors": "^1.4.0",
"detox": "20.28.0",
"detox": "20.31.0",
"jest": "^29",
"patch-package": "^8.0.0",
"pixelmatch": "^5.3.0",
Expand Down
18 changes: 9 additions & 9 deletions e2e/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1114,23 +1114,23 @@ detect-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==

detox-copilot@^0.0.24:
version "0.0.24"
resolved "https://registry.yarnpkg.com/detox-copilot/-/detox-copilot-0.0.24.tgz#e505073866d1f4ff00641f2e48ef441a4dff6bbc"
integrity sha512-42g0QyJS31URl28YRxc4hGozSXhbbB1sKwzxEjZR9WtLoSx6WYDsQkQD8+yP5t1NExiSCZAfvNmBw8PYQwDKwg==
detox-copilot@^0.0.27:
version "0.0.27"
resolved "https://registry.yarnpkg.com/detox-copilot/-/detox-copilot-0.0.27.tgz#350ee91ae6ba77acac78513ccbda7aafcb3c6faf"
integrity sha512-H2febTNp0arVx2A8rvM1C2BwDiBEP/2Ya8Hd1mVyV66rR5u8om1gdIypaRGm+plpTLCHhlefe4+7qLtHgVzpng==

detox@20.28.0:
version "20.28.0"
resolved "https://registry.yarnpkg.com/detox/-/detox-20.28.0.tgz#7ae848e8df028c17d65cd0672040cd1c18338b25"
integrity sha512-JeUkWNnYE7lqby3S9AeYJP3ttCBKH+qZWACjWXwvSbe3tm6JeXvecVUYkzSoNfC4IzTX5p+rWvG0IPsfOsZSFw==
detox@20.31.0:
version "20.31.0"
resolved "https://registry.yarnpkg.com/detox/-/detox-20.31.0.tgz#55a286b7431b150d23d25702e13ad977fc005c6c"
integrity sha512-6Vsm/o8hElUYuZrBOYDNfkxEU+8q1CZ38wg/st8e1guOaDamIO/5u4hVUHVmId381/3bxfTvC6cO5WgW6BXmUQ==
dependencies:
ajv "^8.6.3"
bunyan "^1.8.12"
bunyan-debug-stream "^3.1.0"
caf "^15.0.1"
chalk "^4.0.0"
child-process-promise "^2.2.0"
detox-copilot "^0.0.24"
detox-copilot "^0.0.27"
execa "^5.1.1"
find-up "^5.0.0"
fs-extra "^11.0.0"
Expand Down
Loading