[go: nahoru, domu]

Text Selection] Update FloatingToolbar.

-- Add copy callback to copy text, thus remove the selected text parameter.
-- Hide FloatingToolbar when selection becomes null, for example, click on the unselected region to cancel the selection, then the FloatingToolbar should disappear.
-- Hide FloatingToolbar when existing the activity.
-- Update FloatingToolbar's position when scrolling the screen.

Bug: 139311164
Test: Manually Tested.
Test: ./gradlew test
Test: ./gradlew buildOnServer
Test: ./gradlew :ui:ui-framework:connectedAndroidTest
Test: ./gradlew :ui:ui-platform:connectedAndroidTest

Change-Id: I83bb102017b2bfbc549c31e94e0efb910e252f7f
diff --git a/ui/ui-framework/src/main/java/androidx/ui/core/selection/SelectionManager.kt b/ui/ui-framework/src/main/java/androidx/ui/core/selection/SelectionManager.kt
index d95c3e5..3acc691 100644
--- a/ui/ui-framework/src/main/java/androidx/ui/core/selection/SelectionManager.kt
+++ b/ui/ui-framework/src/main/java/androidx/ui/core/selection/SelectionManager.kt
@@ -28,11 +28,15 @@
 import androidx.ui.core.hapticfeedback.HapticFeedback
 import androidx.ui.core.hapticfeedback.HapticFeedbackType
 import androidx.ui.core.texttoolbar.TextToolbar
+import androidx.ui.core.texttoolbar.TextToolbarStatus
 import androidx.ui.geometry.Rect
 import androidx.ui.text.AnnotatedString
 import androidx.ui.text.length
 import androidx.ui.text.subSequence
+import androidx.ui.unit.Px
 import androidx.ui.unit.PxPosition
+import androidx.ui.unit.max
+import androidx.ui.unit.min
 import androidx.ui.unit.px
 
 /**
@@ -46,6 +50,7 @@
         set(value) {
             field = value
             updateHandleOffsets()
+            hideSelectionToolbar()
         }
 
     /**
@@ -77,6 +82,7 @@
         set(value) {
             field = value
             updateHandleOffsets()
+            updateSelectionToolbarPosition()
         }
 
     /**
@@ -120,6 +126,7 @@
     init {
         selectionRegistrar.>
             updateHandleOffsets()
+            hideSelectionToolbar()
         }
     }
 
@@ -230,26 +237,108 @@
         return selectedText
     }
 
-    /**
-     * This function collects the selected text and the selected region as a Rectangle region,
-     * and pass these information to [TextToolbar] to make the FloatingToolbar show up in the
-     * proper place, and copy the text to the [ClipboardManager].
-     *
-     * @param selectedRegion selected region as [Rect]. The top is the top of the first selected
-     * line, and the bottom is the bottom of the last selected line. The left is the leftmost
-     * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
-     */
-    internal fun showSelectionToolbar(selectedRegion: Rect) {
+    internal fun copy() {
         val selectedText = getSelectedText()
-        selectedText?.let {
+        selectedText?.let { clipboardManager?.setText(it) }
+    }
+
+    /**
+     * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
+     * to make the FloatingToolbar show up in the proper place. In addition, this function passes
+     * the copy method as a callback when "copy" is clicked.
+     */
+    internal fun showSelectionToolbar() {
+        selection?.let {
             textToolbar?.showCopyMenu(
-                rect = selectedRegion,
-                text = it,
+                getContentRect(),
+                 copy() },
                  onRelease() }
             )
         }
     }
 
+    private fun hideSelectionToolbar() {
+        if (textToolbar?.status == TextToolbarStatus.Shown) {
+            val selection = selection
+            if (selection == null) {
+                textToolbar?.hide()
+            }
+        }
+    }
+
+    private fun updateSelectionToolbarPosition() {
+        if (textToolbar?.status == TextToolbarStatus.Shown) {
+            showSelectionToolbar()
+        }
+    }
+
+    /**
+     * Calculate selected region as [Rect]. The top is the top of the first selected
+     * line, and the bottom is the bottom of the last selected line. The left is the leftmost
+     * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
+     */
+    private fun getContentRect(): Rect {
+        val selection = selection ?: return Rect.zero
+        val startLayoutCoordinates =
+            selection.start.selectable.getLayoutCoordinates() ?: return Rect.zero
+        val endLayoutCoordinates =
+            selection.end.selectable.getLayoutCoordinates() ?: return Rect.zero
+
+        val localLayoutCoordinates = containerLayoutCoordinates
+        if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
+            var startOffset = localLayoutCoordinates.childToLocal(
+                startLayoutCoordinates,
+                selection.start.selectable.getHandlePosition(
+                    selection = selection,
+                    isStartHandle = true
+                )
+            )
+            var endOffset = localLayoutCoordinates.childToLocal(
+                endLayoutCoordinates,
+                selection.end.selectable.getHandlePosition(
+                    selection = selection,
+                    isStartHandle = false
+                )
+            )
+
+            startOffset = localLayoutCoordinates.localToRoot(startOffset)
+            endOffset = localLayoutCoordinates.localToRoot(endOffset)
+
+            val left = min(startOffset.x, endOffset.x)
+            val right = max(startOffset.x, endOffset.x)
+
+            var startTop = localLayoutCoordinates.childToLocal(
+                startLayoutCoordinates,
+                PxPosition(
+                    Px.Zero,
+                    selection.start.selectable.getBoundingBox(selection.start.offset).top.px
+                )
+            )
+
+            var endTop = localLayoutCoordinates.childToLocal(
+                endLayoutCoordinates,
+                PxPosition(
+                    Px.Zero,
+                    selection.end.selectable.getBoundingBox(selection.end.offset).top.px
+                )
+            )
+
+            startTop = localLayoutCoordinates.localToRoot(startTop)
+            endTop = localLayoutCoordinates.localToRoot(endTop)
+
+            val top = min(startTop.y, endTop.y)
+            val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).px
+
+            return Rect(
+                left.value,
+                top.value,
+                right.value,
+                bottom.value
+            )
+        }
+        return Rect.zero
+    }
+
     // This is for PressGestureDetector to cancel the selection.
     fun onRelease() {
         // Call mergeSelections with an out of boundary input to inform all text widgets to