[go: nahoru, domu]

blob: 243df45e4746f68a135b0611e2f11314c9985946 [file] [log] [blame]
Qingqing Deng6f56a912019-05-13 10:10:37 -07001/*
2 * Copyright 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Siyamed Sinir700df8452019-10-22 20:23:58 -070017package androidx.ui.foundation.text
Qingqing Deng6f56a912019-05-13 10:10:37 -070018
Qingqing Deng6f56a912019-05-13 10:10:37 -070019import androidx.ui.core.LayoutCoordinates
20import androidx.ui.core.PxPosition
21import androidx.ui.core.gesture.DragObserver
Qingqing Dengbf370e72019-10-17 11:14:39 -070022import androidx.ui.core.gesture.LongPressDragObserver
Qingqing Deng6f56a912019-05-13 10:10:37 -070023import androidx.ui.core.px
Siyamed Sinir700df8452019-10-22 20:23:58 -070024import androidx.ui.core.selection.Selection
25import androidx.ui.core.selection.SelectionMode
26import androidx.ui.core.selection.TextSelectionHandler
Qingqing Deng6f56a912019-05-13 10:10:37 -070027
Qingqing Deng35f97ea2019-09-18 19:24:37 -070028/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010029 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070030 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070031internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070032 /**
33 * The current selection.
34 */
35 var selection: Selection? = null
36
37 /**
38 * The manager will invoke this every time it comes to the conclusion that the selection should
39 * change. The expectation is that this callback will end up causing `setSelection` to get
40 * called. This is what makes this a "controlled component".
41 */
42 var onSelectionChange: (Selection?) -> Unit = {}
43
44 /**
Siyamed Sinir700df8452019-10-22 20:23:58 -070045 * Selection mode to be used. Default value is Vertical.
Qingqing Dengb745a942019-06-05 18:26:52 -070046 */
47 var mode: SelectionMode = SelectionMode.Vertical
48
49 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070050 * Layout Coordinates of the selection container.
51 */
52 lateinit var containerLayoutCoordinates: LayoutCoordinates
53
54 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070055 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
56 * recalculated.
57 */
58 private var dragBeginPosition = PxPosition.Origin
59
60 /**
61 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
62 * it will be zeroed out.
63 */
64 private var dragTotalDistance = PxPosition.Origin
65
66 /**
Qingqing Deng0cb86fe2019-07-16 15:36:27 -070067 * A flag to check if the selection start or end handle is being dragged.
68 * If this value is true, then onPress will not select any text.
69 * This value will be set to true when either handle is being dragged, and be reset to false
70 * when the dragging is stopped.
71 */
72 private var draggingHandle = false
73
74 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -070075 * Iterates over the handlers, gets the selection for each Composable, and merges all the
76 * returned [Selection]s.
77 *
78 * @param startPosition [PxPosition] for the start of the selection
79 * @param endPosition [PxPosition] for the end of the selection
80 * @param selection initial selection to start with
81 *
82 * @return [Selection] object which is constructed by combining all Composables that are
83 * selected.
84 */
85 private fun mergeSelections(
86 startPosition: PxPosition,
87 endPosition: PxPosition,
88 selection: Selection? = null
89 ): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -070090 val handlers = selectionRegistrar.handlers
Siyamed Sinir472c3162019-10-21 23:41:00 -070091 return handlers.fold(selection) { mergedSelection: Selection?,
92 handler: TextSelectionHandler ->
Siyamed Sinir700df8452019-10-22 20:23:58 -070093 merge(
94 mergedSelection,
95 handler.getSelection(
96 startPosition = startPosition,
97 endPosition = endPosition,
98 containerLayoutCoordinates = containerLayoutCoordinates,
99 mode = mode
100 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700101 )
102 }
103 }
104
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800105 // This is for PressGestureDetector to cancel the selection.
106 fun onRelease() {
107 // Call mergeSelections with an out of boundary input to inform all text widgets to
108 // cancel their individual selection.
109 mergeSelections(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
110 onSelectionChange(null)
111 }
112
Siyamed Sinir700df8452019-10-22 20:23:58 -0700113 val longPressDragObserver = object : LongPressDragObserver {
114 override fun onLongPress(pxPosition: PxPosition) {
115 if (draggingHandle) return
116 val selection = mergeSelections(pxPosition, pxPosition)
117 onSelectionChange(selection)
118 dragBeginPosition = pxPosition
Qingqing Dengbf370e72019-10-17 11:14:39 -0700119 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700120
Siyamed Sinir700df8452019-10-22 20:23:58 -0700121 override fun onDragStart() {
122 super.onDragStart()
123 // Zero out the total distance that being dragged.
124 dragTotalDistance = PxPosition.Origin
125 }
126
127 override fun onDrag(dragDistance: PxPosition): PxPosition {
128 dragTotalDistance += dragDistance
129
130 val selection = mergeSelections(
131 dragBeginPosition,
132 dragBeginPosition + dragTotalDistance
133 )
134 onSelectionChange(selection)
135 return dragDistance
136 }
137 }
138
Qingqing Deng13743f72019-07-15 15:00:45 -0700139 /**
140 * Adjust coordinates for given text offset.
141 *
142 * Currently [android.text.Layout.getLineBottom] returns y coordinates of the next
143 * line's top offset, which is not included in current line's hit area. To be able to
144 * hit current line, move up this y coordinates by 1 pixel.
145 */
Siyamed Sinir472c3162019-10-21 23:41:00 -0700146 private fun getAdjustedCoordinates(position: PxPosition): PxPosition {
147 return PxPosition(position.x, position.y - 1.px)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700148 }
149
Siyamed Sinir472c3162019-10-21 23:41:00 -0700150 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700151 return object : DragObserver {
Andrey Kulikov0e6c40f2019-09-06 18:33:14 +0100152 override fun onStart(downPosition: PxPosition) {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700153 val selection = selection!!
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100154 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700155 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100156 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700157 val beginLayoutCoordinates = if (isStartHandle) {
158 selection.start.layoutCoordinates!!
159 } else {
160 selection.end.layoutCoordinates!!
161 }
162
Qingqing Deng6f56a912019-05-13 10:10:37 -0700163 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100164 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700165 val beginCoordinates = getAdjustedCoordinates(
166 if (isStartHandle) {
167 selection.start.coordinates
168 } else {
169 selection.end.coordinates
170 }
171 )
172
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100173 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700174 // selection container coordinates.
175 dragBeginPosition = containerLayoutCoordinates.childToLocal(
176 beginLayoutCoordinates,
177 beginCoordinates
178 )
179
180 // Zero out the total distance that being dragged.
181 dragTotalDistance = PxPosition.Origin
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700182 draggingHandle = true
Qingqing Deng6f56a912019-05-13 10:10:37 -0700183 }
184
185 override fun onDrag(dragDistance: PxPosition): PxPosition {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700186 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700187 dragTotalDistance += dragDistance
188
Siyamed Sinir472c3162019-10-21 23:41:00 -0700189 val currentStart = if (isStartHandle) {
190 dragBeginPosition + dragTotalDistance
191 } else {
192 containerLayoutCoordinates.childToLocal(
193 selection.start.layoutCoordinates!!,
194 getAdjustedCoordinates(selection.start.coordinates)
195 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700196 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700197
198 val currentEnd = if (isStartHandle) {
199 containerLayoutCoordinates.childToLocal(
200 selection.end.layoutCoordinates!!,
201 getAdjustedCoordinates(selection.end.coordinates)
202 )
203 } else {
204 dragBeginPosition + dragTotalDistance
205 }
206
207 val finalSelection = mergeSelections(currentStart, currentEnd, selection)
208 onSelectionChange(finalSelection)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700209 return dragDistance
210 }
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700211
212 override fun onStop(velocity: PxPosition) {
213 super.onStop(velocity)
214 draggingHandle = false
215 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700216 }
217 }
218}
219
Siyamed Sinir700df8452019-10-22 20:23:58 -0700220private fun merge(lhs: Selection?, rhs: Selection?): Selection? {
221 return lhs?.merge(rhs) ?: rhs
222}