mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 09:42:53 +00:00
fix(android): keep inverted chat still during text selection
React Native decomposes Android list inversion into a 180-degree rotation with positive scaleY, so the existing visibility-scroll guard never recognized the inverted viewport. Inspect the composed matrix and reproduce the actual list transform through the native view manager in regression tests.
This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
|
||||
Keep assistant prose in one native text view so selection crosses paragraphs, headings and lists. The shared [Markdown renderer](markdown-renderer.md) owns that view.
|
||||
|
||||
## Android inverted chat
|
||||
|
||||
Keep selection visibility handling in `modules/paseo-scroll`. Android's visibility
|
||||
rectangles ignore list inversion. React Native decomposes Android's `scale: -1`
|
||||
into rotation with positive `scaleY`, so test the composed matrix when deciding
|
||||
whether those requests should move the viewport. Native tests must apply the
|
||||
list's transform through React Native's view manager; assigning `scaleY` directly
|
||||
misses that decomposition.
|
||||
|
||||
## Diagrams in lists and quotes
|
||||
|
||||
Selection stops at a Mermaid diagram. Native extracts Mermaid fences into separate views because the library exposes no native block renderer override. A diagram inside a list or quote therefore appears outside that container, and its following prose starts a separate Markdown fragment.
|
||||
|
||||
+22
-1
@@ -8,6 +8,9 @@ import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.facebook.react.bridge.JavaOnlyArray
|
||||
import com.facebook.react.bridge.JavaOnlyMap
|
||||
import com.facebook.react.uimanager.ReactStylesDiffMap
|
||||
import com.facebook.react.bridge.BridgeReactContext
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
@@ -47,16 +50,34 @@ class ScrollSelectionTest {
|
||||
assertNotEquals(before, scroll.scrollY)
|
||||
}
|
||||
|
||||
@Test fun directVerticalMirroringKeepsSelectionStill() = withScroll(inverted = false) { scroll, text ->
|
||||
scroll.scaleY = -1f
|
||||
val before = scroll.scrollY
|
||||
text.requestRectangleOnScreen(Rect(0, 0, 80, 40), true)
|
||||
assertEquals(before, scroll.scrollY)
|
||||
}
|
||||
|
||||
@Test fun horizontalMirroringStillRevealsSelectedText() = withScroll(inverted = false) { scroll, text ->
|
||||
scroll.scaleX = -1f
|
||||
val before = scroll.scrollY
|
||||
text.requestRectangleOnScreen(Rect(0, 0, 80, 40), true)
|
||||
assertNotEquals(before, scroll.scrollY)
|
||||
}
|
||||
|
||||
private fun withScroll(inverted: Boolean, check: (ReactScrollView, TextView) -> Unit) {
|
||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
instrumentation.runOnMainSync {
|
||||
val appContext = instrumentation.targetContext
|
||||
SoLoader.init(appContext, OpenSourceMergedSoMapping)
|
||||
com.facebook.react.uimanager.DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(appContext)
|
||||
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
|
||||
// Exercise VirtualizedList's Android transform through RN's real decomposition.
|
||||
if (inverted) manager.updateProperties(scroll, ReactStylesDiffMap(JavaOnlyMap.of(
|
||||
"transform", JavaOnlyArray.of(JavaOnlyMap.of("scale", -1.0))
|
||||
)))
|
||||
val content = FrameLayout(context)
|
||||
val text = TextView(context).apply {
|
||||
setText("Selectable timeline text", TextView.BufferType.SPANNABLE)
|
||||
|
||||
+12
-5
@@ -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) {
|
||||
// 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)
|
||||
private val transformValues = FloatArray(9)
|
||||
|
||||
// Android VirtualizedList uses scale: -1, which RN decomposes into a 180-degree
|
||||
// rotation with positive scaleY. Inspect the composed transform so both that
|
||||
// inversion and a direct scaleY: -1 suppress untransformed visibility requests.
|
||||
// Keep focus/selection bookkeeping; gestures and explicit commands still scroll.
|
||||
override fun computeScrollDeltaToGetChildRectOnScreen(rect: Rect): Int {
|
||||
matrix.getValues(transformValues)
|
||||
return if (transformValues[Matrix.MSCALE_Y] < 0) 0
|
||||
else super.computeScrollDeltaToGetChildRectOnScreen(rect)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user