Selection extend by word shrink by character
- Changed SelectionAdjustment so that more advanced selection adjustment
is possible.
- Introduced SelectionAdjustment.CharacterWithWordAccelerate which
implemented the Android TextView's selection movement behavior.
- Use SelectionAdjustment.CharacterWithWordAccelerate in
SelectionContainer.
Bug: 137322559
Test: ./gradlew test
Test: ./gradlew compose:foundation:foundation:connectedAndroidTest
RelNote: N/A
Change-Id: Ia4bccabcde17ce235880f4f18d00083304ee77cb
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 e669132..dd8f4ec 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
@@ -242,6 +242,7 @@
selectionRegistrar.>
showSelectionToolbar()
+ finishSelectionUpdate()
}
selectionRegistrar. selectableKey ->
@@ -353,27 +354,27 @@
internal fun mergeSelections(
startPosition: Offset,
endPosition: Offset,
- adjustment: SelectionAdjustment = SelectionAdjustment.NONE,
+ adjustment: SelectionAdjustment = SelectionAdjustment.None,
previousSelection: Selection? = null,
isStartHandle: Boolean = true
): Pair<Selection?, Map<Long, Selection>> {
val subselections = mutableMapOf<Long, Selection>()
val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
.fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
+ val subselection =
+ selectionRegistrar.subselections[selectable.selectableId]
val selection = selectable.getSelection(
startPosition = startPosition,
endPosition = endPosition,
containerLayoutCoordinates = requireContainerCoordinates(),
- previousSelection = previousSelection,
+ adjustment = adjustment,
+ previousSelection = subselection,
isStartHandle = isStartHandle,
- adjustment = adjustment
)
selection?.let { subselections[selectable.selectableId] = it }
merge(mergedSelection, selection)
}
- if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
- HapticFeedbackType.TextHandleMove
- )
+ performHapticFeedbackIfNeeded(newSelection, previousSelection, hapticFeedBack)
return Pair(newSelection, subselections)
}
@@ -389,9 +390,7 @@
selection?.let { subselections[selectable.selectableId] = it }
merge(mergedSelection, selection)
}
- if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
- HapticFeedbackType.TextHandleMove
- )
+ performHapticFeedbackIfNeeded(previousSelection, newSelection, hapticFeedBack)
return Pair(newSelection, subselections)
}
@@ -617,13 +616,14 @@
startPosition = currentStart,
endPosition = currentEnd,
isStartHandle = isStartHandle,
- adjustment = SelectionAdjustment.CHARACTER
+ adjustment = SelectionAdjustment.CharacterWithWordAccelerate
)
return
}
override fun onStop() {
showSelectionToolbar()
+ finishSelectionUpdate()
}
override fun onCancel() {
@@ -663,7 +663,7 @@
private fun updateSelection(
startPosition: Offset?,
endPosition: Offset?,
- adjustment: SelectionAdjustment = SelectionAdjustment.NONE,
+ adjustment: SelectionAdjustment = SelectionAdjustment.None,
isStartHandle: Boolean = true
) {
if (startPosition == null || endPosition == null) return
@@ -671,14 +671,25 @@
startPosition = startPosition,
endPosition = endPosition,
adjustment = adjustment,
- isStartHandle = isStartHandle,
previousSelection = selection,
+ isStartHandle = isStartHandle
)
if (newSelection != selection) {
selectionRegistrar.subselections = newSubselection
onSelectionChange(newSelection)
}
}
+
+ private fun finishSelectionUpdate() {
+ selection?.let {
+ val newSelection = Selection(
+ start = it.start.copy(rawOffset = it.start.offset),
+ end = it.end.copy(rawOffset = it.end.offset),
+ handlesCrossed = it.handlesCrossed
+ )
+ onSelectionChange(newSelection)
+ }
+ }
}
internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
@@ -744,3 +755,33 @@
internal fun Rect.containsInclusive(offset: Offset): Boolean =
offset.x in left..right && offset.y in top..bottom
+
+private fun performHapticFeedbackIfNeeded(
+ previousSelection: Selection?,
+ newSelection: Selection?,
+ hapticFeedback: HapticFeedback?,
+) {
+ if (hapticFeedback == null) {
+ return
+ }
+ if (previousSelection == null) {
+ if (newSelection != null) {
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ }
+ return
+ }
+ if (newSelection == null) {
+ // We know previousSelection is not null, so selection is updated.
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ return
+ }
+ // We don't care if rawOffset of Selection.AnchorInfo is different, so we only compare the
+ // other fields. If any of them changes, we need to perform hapticFeedback.
+ if (previousSelection.start.offset != newSelection.start.offset ||
+ previousSelection.start.selectableId != newSelection.start.selectableId ||
+ previousSelection.end.offset != newSelection.end.offset ||
+ previousSelection.end.selectableId != previousSelection.end.selectableId
+ ) {
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ }
+}