mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 09:42:53 +00:00
Keep Android chat still when selecting text (#7131)
Android's scroll guard now reads the view's transform matrix, so React Native's inverted chat list (scale: -1, decomposed into a 180-degree rotation with positive scaleY) no longer scrolls on text focus or selection. Closes #7125 Co-authored-by: luke <luopeihuan@gmail.com> Co-authored-by: RoundRedDot <roundreddot@gmail.com>
This commit is contained in:
co-authored by
luke
RoundRedDot
parent
5acfbb3777
commit
48b310c8a2
+3
-1
@@ -124,7 +124,9 @@ This is the Android counterpart of the iOS local-simulator flow in [development.
|
||||
|
||||
Android focus and selection visibility requests must not reposition inverted timelines. The
|
||||
`modules/paseo-scroll` package keeps React Native's scroll manager interface and returns zero for
|
||||
child-reveal scroll calculations when the vertical scale is inverted. Dragging and explicit scroll
|
||||
child-reveal scroll calculations when the view's transform matrix inverts the vertical axis. React
|
||||
Native's Android list uses `scale: -1`, which decomposes into a 180-degree rotation with positive
|
||||
`scaleY`; checking the scale property alone misses it. Dragging and explicit scroll
|
||||
commands still work; non-inverted scroll views keep Android's default behavior.
|
||||
|
||||
Register this package before React Native's core package through its Expo config plugin. Normal
|
||||
|
||||
+26
-2
@@ -9,7 +9,11 @@ import android.widget.TextView
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.facebook.react.bridge.BridgeReactContext
|
||||
import com.facebook.react.bridge.JavaOnlyArray
|
||||
import com.facebook.react.bridge.JavaOnlyMap
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.react.uimanager.DisplayMetricsHolder
|
||||
import com.facebook.react.uimanager.ReactStylesDiffMap
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
import com.facebook.react.views.scroll.ReactScrollView
|
||||
import com.facebook.react.views.scroll.ReactScrollViewManager
|
||||
@@ -47,16 +51,29 @@ class ScrollSelectionTest {
|
||||
assertNotEquals(before, scroll.scrollY)
|
||||
}
|
||||
|
||||
private fun withScroll(inverted: Boolean, check: (ReactScrollView, TextView) -> Unit) {
|
||||
@Test fun verticalScaleStillKeepsFocusAndSelectionInPlace() = withScroll(inverted = true, transform = "scaleY") { scroll, text ->
|
||||
val before = scroll.scrollY
|
||||
assertTrue(text.requestFocus())
|
||||
text.requestRectangleOnScreen(Rect(0, 0, 80, 40), true)
|
||||
assertEquals(before, scroll.scrollY)
|
||||
}
|
||||
|
||||
@Test fun horizontalScaleStillRevealsSelectedText() = withScroll(inverted = true, transform = "scaleX") { scroll, text ->
|
||||
val before = scroll.scrollY
|
||||
text.requestRectangleOnScreen(Rect(0, 0, 80, 40), true)
|
||||
assertNotEquals(before, scroll.scrollY)
|
||||
}
|
||||
|
||||
private fun withScroll(inverted: Boolean, transform: String = "scale", check: (ReactScrollView, TextView) -> Unit) {
|
||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
instrumentation.runOnMainSync {
|
||||
val appContext = instrumentation.targetContext
|
||||
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(appContext)
|
||||
SoLoader.init(appContext, OpenSourceMergedSoMapping)
|
||||
val reactContext = BridgeReactContext(appContext)
|
||||
val context = ThemedReactContext(reactContext, appContext)
|
||||
val manager = PaseoScrollPackage().createViewManager(reactContext, ReactScrollViewManager.REACT_CLASS) as ReactScrollViewManager
|
||||
val scroll = manager.createViewInstance(context)
|
||||
scroll.scaleY = if (inverted) -1f else 1f
|
||||
val content = FrameLayout(context)
|
||||
val text = TextView(context).apply {
|
||||
setText("Selectable timeline text", TextView.BufferType.SPANNABLE)
|
||||
@@ -68,6 +85,13 @@ class ScrollSelectionTest {
|
||||
content.layout(0, 0, 300, 2000)
|
||||
scroll.measure(exact(300), exact(300))
|
||||
scroll.layout(0, 0, 300, 300)
|
||||
// Apply RN's actual Android inversion through the manager's transform decomposition.
|
||||
val transforms = JavaOnlyArray.of(JavaOnlyMap.of(transform, if (inverted) -1.0 else 1.0))
|
||||
manager.updateProperties(scroll, ReactStylesDiffMap(JavaOnlyMap.of("transform", transforms)))
|
||||
if (inverted && transform == "scale") {
|
||||
assertEquals(1f, scroll.scaleY, 0f)
|
||||
assertEquals(180f, kotlin.math.abs(scroll.rotation), 0f)
|
||||
}
|
||||
scroll.scrollTo(0, 400)
|
||||
check(scroll, text)
|
||||
}
|
||||
|
||||
+9
-2
@@ -1,5 +1,6 @@
|
||||
package sh.paseo.scroll
|
||||
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.ViewManagerOnDemandReactPackage
|
||||
@@ -27,9 +28,15 @@ private class PaseoScrollViewManager : ReactScrollViewManager() {
|
||||
}
|
||||
|
||||
private class PaseoScrollView(context: ThemedReactContext) : ReactScrollView(context) {
|
||||
private val transformValues = FloatArray(9)
|
||||
|
||||
// Both RN focus handling and Android selection visibility requests use this calculation.
|
||||
// Its coordinates ignore inversion. Keep focus/selection bookkeeping, but leave timeline
|
||||
// movement to user gestures and explicit scroll commands.
|
||||
override fun computeScrollDeltaToGetChildRectOnScreen(rect: Rect): Int =
|
||||
if (scaleY < 0) 0 else super.computeScrollDeltaToGetChildRectOnScreen(rect)
|
||||
override fun computeScrollDeltaToGetChildRectOnScreen(rect: Rect): Int {
|
||||
// RN's Android scale: -1 becomes a 180-degree rotation with positive scaleY.
|
||||
matrix.getValues(transformValues)
|
||||
if (transformValues[Matrix.MSCALE_Y] < 0) return 0
|
||||
return super.computeScrollDeltaToGetChildRectOnScreen(rect)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user