[Text Selection] Drag.
This CL adds drag to select feature.
Bug: 134533989
Test: ./gradlew test
Change-Id: Ibb2e3b6c4eabadb70e19517f99034c8eadbd1f9f
diff --git a/ui/framework/src/main/java/androidx/ui/core/selection/SelectionManager.kt b/ui/framework/src/main/java/androidx/ui/core/selection/SelectionManager.kt
new file mode 100644
index 0000000..64727fd
--- /dev/null
+++ b/ui/framework/src/main/java/androidx/ui/core/selection/SelectionManager.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.core.selection
+
+import androidx.compose.Ambient
+import androidx.ui.core.LayoutCoordinates
+import androidx.ui.core.PxPosition
+import androidx.ui.core.gesture.DragObserver
+import androidx.ui.core.px
+import androidx.ui.engine.geometry.Rect
+
+/**
+ * An interface handling selection. Get selection from a widget by passing in the start and end of
+ * selection in a selection container as a pair, and the layout coordinates of the selection
+ * container.
+ */
+internal interface TextSelectionHandler {
+ fun getSelection(
+ selectionCoordinates: Pair<PxPosition, PxPosition>,
+ containerLayoutCoordinates: LayoutCoordinates
+ ): Selection?
+}
+
+/**
+ * An interface allowing a Text composable to "register" and "unregister" itself with the class
+ * implementing the interface.
+ */
+internal interface SelectionRegistrar {
+ // TODO(qqd): Replace Any with a type in future.
+ fun subscribe(handler: TextSelectionHandler): Any
+
+ fun unsubscribe(key: Any)
+}
+
+internal class SelectionManager : SelectionRegistrar {
+ /**
+ * The current selection.
+ */
+ var selection: Selection? = null
+
+ /**
+ * The manager will invoke this every time it comes to the conclusion that the selection should
+ * change. The expectation is that this callback will end up causing `setSelection` to get
+ * called. This is what makes this a "controlled component".
+ */
+ var onSelectionChange: (Selection?) -> Unit = {}
+
+ /**
+ * Layout Coordinates of the selection container.
+ */
+ lateinit var containerLayoutCoordinates: LayoutCoordinates
+
+ /**
+ * This is essentially the list of registered components that want
+ * to handle text selection that are below the SelectionContainer.
+ */
+ private val handlers = mutableSetOf<TextSelectionHandler>()
+
+ /**
+ * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
+ * recalculated.
+ */
+ private var dragBeginPosition = PxPosition.Origin
+
+ /**
+ * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
+ * it will be zeroed out.
+ */
+ private var dragTotalDistance = PxPosition.Origin
+
+ /**
+ * Allow a Text composable to "register" itself with the manager
+ */
+ override fun subscribe(handler: TextSelectionHandler): Any {
+ handlers.add(handler)
+ return handler
+ }
+
+ /**
+ * Allow a Text composable to "unregister" itself with the manager
+ */
+ override fun unsubscribe(key: Any) {
+ handlers.remove(key as TextSelectionHandler)
+ }
+
+ fun onPress(position: PxPosition) {
+ var result: Selection? = null
+ for (handler in handlers) {
+ result = handler.getSelection(Pair(position, position), containerLayoutCoordinates)
+ }
+ onSelectionChange(result)
+ }
+
+ // Get the coordinates of a character. Currently, it's the middle point of the left edge of the
+ // bounding box of the character. This is a temporary solution.
+ // TODO(qqd): Read how Android solve this problem.
+ fun getCoordinatesForCharacter(box: Rect): PxPosition {
+ return PxPosition(box.left.px, box.top.px + (box.bottom.px - box.top.px) / 2)
+ }
+
+ fun handleDragObserver(dragStartHandle: Boolean): DragObserver {
+ return object : DragObserver {
+ override fun onStart() {
+ // The LayoutCoordinates of the widget where the drag gesture should begin. This
+ // is used to convert the position of the beginning of the drag gesture from the
+ // widget coordinates to selection container coordinates.
+ val beginLayoutCoordinates =
+ if (dragStartHandle) {
+ selection!!.startLayoutCoordinates!!
+ } else {
+ selection!!.endLayoutCoordinates!!
+ }
+ // The position of the character where the drag gesture should begin. This is in
+ // the widget coordinates.
+ val beginCoordinates =
+ getCoordinatesForCharacter(
+ if (dragStartHandle) {
+ selection!!.startOffset
+ } else {
+ selection!!.endOffset
+ }
+ )
+ // Convert the position where drag gesture begins from widget coordinates to
+ // selection container coordinates.
+ dragBeginPosition = containerLayoutCoordinates.childToLocal(
+ beginLayoutCoordinates,
+ beginCoordinates
+ )
+
+ // Zero out the total distance that being dragged.
+ dragTotalDistance = PxPosition.Origin
+ }
+
+ override fun onDrag(dragDistance: PxPosition): PxPosition {
+ var result = selection
+ dragTotalDistance += dragDistance
+
+ val currentStart =
+ if (dragStartHandle) {
+ dragBeginPosition + dragTotalDistance
+ } else {
+ containerLayoutCoordinates.childToLocal(
+ selection!!.startLayoutCoordinates!!,
+ getCoordinatesForCharacter(selection!!.startOffset)
+ )
+ }
+
+ val currentEnd =
+ if (dragStartHandle) {
+ containerLayoutCoordinates.childToLocal(
+ selection!!.endLayoutCoordinates!!,
+ getCoordinatesForCharacter(selection!!.endOffset)
+ )
+ } else {
+ dragBeginPosition + dragTotalDistance
+ }
+
+ for (handler in handlers) {
+ result = handler.getSelection(
+ Pair(currentStart, currentEnd),
+ containerLayoutCoordinates)
+ }
+ onSelectionChange(result)
+ return dragDistance
+ }
+ }
+ }
+}
+
+/** Ambient of SelectionRegistrar for SelectionManager. */
+internal val SelectionRegistrarAmbient = Ambient.of<SelectionRegistrar> { SelectionManager() }