Polish selection magnification behavior.
Text field selection magnification was implemented in aosp/1844741.
Selection container selection magnification was implemented in aosp/1890195.
This doc has a description of the behaviors that this CL implements:
https://docs.google.com/document/d/1NRWiCH2NL-XPPP0wYe8Ql6mnUJ9pwBcisgCxl2WhPXg/edit?usp=sharing&resourcekey=0-Hwm4817Nn_FF9Hdxk3rO7A
Fixes: b/206833278
Test: Replaced too-detailed unit tests for pieces of selection
implementations with UI tests for actual behavior:
./gradlew :compose:foundation:foundation:cAT
Relnote: "Text selection magnifier behavior has been polished to
match the platform magnifier."
Change-Id: Idd918c3b321e8d7626e47a027e0b19640085a2fa
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index 1a2ec05..7538250 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -50,6 +50,8 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.TextToolbarStatus
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.IntSize
+import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.coroutineScope
@@ -790,25 +792,50 @@
internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
-internal fun calculateSelectionMagnifierCenterAndroid(manager: SelectionManager): Offset {
+internal fun calculateSelectionMagnifierCenterAndroid(
+ manager: SelectionManager,
+ magnifierSize: IntSize
+): Offset {
fun getMagnifierCenter(anchor: AnchorInfo, isStartHandle: Boolean): Offset {
- // TODO(b/206833278) Clamp x to the end of the selectable area, hide after a threshold.
- // TODO(b/206833278) Animate x when jumping lines.
- // TODO(b/206833278) X should track drag exactly, not clamp to cursor positions.
+ val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
+ val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
+ val selectableCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified
+ // The end offset is exclusive.
+ val offset = if (isStartHandle) anchor.offset else anchor.offset - 1
+
+ // The horizontal position doesn't snap to cursor positions but should directly track the
+ // actual drag.
+ val localDragPosition = selectableCoordinates.localPositionOf(
+ containerCoordinates,
+ manager.currentDragPosition!!
+ )
+ val dragX = localDragPosition.x
+ // But it is constrained by the horizontal bounds of the current line.
+ val centerX = selectable.getRangeOfLineContaining(offset).let { line ->
+ val lineMin = selectable.getBoundingBox(line.min)
+ // line.end is exclusive, but we want the bounding box of the actual last character in
+ // the line.
+ val lineMax = selectable.getBoundingBox((line.max - 1).coerceAtLeast(line.min))
+ val minX = minOf(lineMin.left, lineMax.left)
+ val maxX = maxOf(lineMin.right, lineMax.right)
+ dragX.coerceIn(minX, maxX)
+ }
+
+ // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
+ // magnifier actually is). See
+ // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
+ if ((dragX - centerX).absoluteValue > magnifierSize.width / 2) {
+ return Offset.Unspecified
+ }
// Let the selectable determine the vertical position of the magnifier, since it should be
// clamped to the center of text lines.
- val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
- // The end offset is exclusive.
- val selectionOffset = if (isStartHandle) anchor.offset else anchor.offset - 1
- val bounds = selectable.getBoundingBox(selectionOffset)
- // Don't need to account for Rtl here because the bounds will already be adjusted for that.
- // (i.e. in Rtl, left > right)
- val localCenter = if (isStartHandle) bounds.centerLeft else bounds.centerRight
- val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
+ val anchorBounds = selectable.getBoundingBox(offset)
+ val centerY = anchorBounds.center.y
+
return containerCoordinates.localPositionOf(
- sourceCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified,
- relativeToSource = localCenter
+ sourceCoordinates = selectableCoordinates,
+ relativeToSource = Offset(centerX, centerY)
)
}