[go: nahoru, domu]

blob: cd65aae50b993e4040c25dedab17fb4b9291fd94 [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 Sinirfd8bc422019-11-21 18:23:58 -080017package androidx.ui.core.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070018
Qingqing Deng6f56a912019-05-13 10:10:37 -070019import androidx.ui.core.LayoutCoordinates
Qingqing Deng6f56a912019-05-13 10:10:37 -070020import androidx.ui.core.gesture.DragObserver
Qingqing Dengbf370e72019-10-17 11:14:39 -070021import androidx.ui.core.gesture.LongPressDragObserver
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080022import androidx.ui.core.hapticfeedback.HapticFeedback
23import androidx.ui.core.hapticfeedback.HapticFeedbackType
Qingqing Deng648e1bf2019-12-30 11:49:48 -080024import androidx.ui.text.AnnotatedString
25import androidx.ui.text.length
26import androidx.ui.text.subSequence
George Mount842c8c12020-01-08 16:03:42 -080027import androidx.ui.unit.PxPosition
28import androidx.ui.unit.px
Qingqing Deng6f56a912019-05-13 10:10:37 -070029
Qingqing Deng35f97ea2019-09-18 19:24:37 -070030/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010031 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070032 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070033internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070034 /**
35 * The current selection.
36 */
37 var selection: Selection? = null
38
39 /**
40 * The manager will invoke this every time it comes to the conclusion that the selection should
41 * change. The expectation is that this callback will end up causing `setSelection` to get
42 * called. This is what makes this a "controlled component".
43 */
44 var onSelectionChange: (Selection?) -> Unit = {}
45
46 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080047 * [HapticFeedback] handle to perform haptic feedback.
48 */
49 var hapticFeedBack: HapticFeedback? = null
50
51 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070052 * Layout Coordinates of the selection container.
53 */
54 lateinit var containerLayoutCoordinates: LayoutCoordinates
55
56 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070057 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
58 * recalculated.
59 */
60 private var dragBeginPosition = PxPosition.Origin
61
62 /**
63 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
64 * it will be zeroed out.
65 */
66 private var dragTotalDistance = PxPosition.Origin
67
68 /**
Qingqing Deng0cb86fe2019-07-16 15:36:27 -070069 * A flag to check if the selection start or end handle is being dragged.
70 * If this value is true, then onPress will not select any text.
71 * This value will be set to true when either handle is being dragged, and be reset to false
72 * when the dragging is stopped.
73 */
74 private var draggingHandle = false
75
76 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -070077 * Iterates over the handlers, gets the selection for each Composable, and merges all the
78 * returned [Selection]s.
79 *
80 * @param startPosition [PxPosition] for the start of the selection
81 * @param endPosition [PxPosition] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -080082 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080083 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -070084 *
85 * @return [Selection] object which is constructed by combining all Composables that are
86 * selected.
87 */
Qingqing Deng5819ff62019-11-18 15:26:23 -080088 // This function is internal for testing purposes.
89 internal fun mergeSelections(
Siyamed Sinir472c3162019-10-21 23:41:00 -070090 startPosition: PxPosition,
91 endPosition: PxPosition,
Siyamed Sinir0100f122019-11-16 00:23:12 -080092 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080093 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -080094 isStartHandle: Boolean = true
Siyamed Sinir472c3162019-10-21 23:41:00 -070095 ): Selection? {
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080096
Qingqing Deng247f2b42019-12-12 19:48:37 -080097 val newSelection = selectionRegistrar.sort(containerLayoutCoordinates)
98 .fold(null) { mergedSelection: Selection?,
99 handler: Selectable ->
100 merge(
101 mergedSelection,
102 handler.getSelection(
103 startPosition = startPosition,
104 endPosition = endPosition,
105 containerLayoutCoordinates = containerLayoutCoordinates,
106 longPress = longPress,
107 previousSelection = previousSelection,
108 isStartHandle = isStartHandle
109 )
Siyamed Sinir700df8452019-10-22 20:23:58 -0700110 )
Qingqing Deng247f2b42019-12-12 19:48:37 -0800111 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800112 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
113 HapticFeedbackType.TextHandleMove
114 )
115 return newSelection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700116 }
117
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800118 internal fun getSelectedText(): AnnotatedString? {
119 val selectables = selectionRegistrar.sort(containerLayoutCoordinates)
120 var selectedText: AnnotatedString? = null
121
122 selection?.let {
123 for (handler in selectables) {
124 // Continue if the current selectable is before the selection starts.
125 if (handler != it.start.selectable && handler != it.end.selectable &&
126 selectedText == null
127 ) continue
128
129 val currentSelectedText = getCurrentSelectedText(
130 selectable = handler,
131 selection = it
132 )
133 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
134
135 // Break if the current selectable is the last selected selectable.
136 if (handler == it.end.selectable && !it.handlesCrossed ||
137 handler == it.start.selectable && it.handlesCrossed
138 ) break
139 }
140 }
141 return selectedText
142 }
143
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800144 // This is for PressGestureDetector to cancel the selection.
145 fun onRelease() {
146 // Call mergeSelections with an out of boundary input to inform all text widgets to
147 // cancel their individual selection.
Qingqing Denga5d80952019-10-11 16:46:52 -0700148 mergeSelections(
Siyamed Sinir0100f122019-11-16 00:23:12 -0800149 startPosition = PxPosition((-1).px, (-1).px),
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800150 endPosition = PxPosition((-1).px, (-1).px),
151 previousSelection = selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800152 )
Siyamed Sinire810eab2019-11-22 12:36:38 -0800153 if (selection != null) onSelectionChange(null)
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800154 }
155
Siyamed Sinire810eab2019-11-22 12:36:38 -0800156 val longPressDragObserver = object : LongPressDragObserver {
157 override fun onLongPress(pxPosition: PxPosition) {
158 if (draggingHandle) return
159 val newSelection = mergeSelections(
160 startPosition = pxPosition,
161 endPosition = pxPosition,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800162 longPress = true,
163 previousSelection = selection
Siyamed Sinire810eab2019-11-22 12:36:38 -0800164 )
165 if (newSelection != selection) onSelectionChange(newSelection)
166 dragBeginPosition = pxPosition
Siyamed Sinir700df8452019-10-22 20:23:58 -0700167 }
Siyamed Sinir700df8452019-10-22 20:23:58 -0700168
Siyamed Sinire810eab2019-11-22 12:36:38 -0800169 override fun onDragStart() {
170 super.onDragStart()
171 // selection never started
172 if (selection == null) return
173 // Zero out the total distance that being dragged.
174 dragTotalDistance = PxPosition.Origin
175 }
176
177 override fun onDrag(dragDistance: PxPosition): PxPosition {
178 // selection never started, did not consume any drag
179 if (selection == null) return PxPosition.Origin
180
181 dragTotalDistance += dragDistance
182 val newSelection = mergeSelections(
183 startPosition = dragBeginPosition,
184 endPosition = dragBeginPosition + dragTotalDistance,
Qingqing Deng74baaa22019-12-12 13:28:25 -0800185 longPress = true,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800186 previousSelection = selection
Siyamed Sinire810eab2019-11-22 12:36:38 -0800187 )
188
189 if (newSelection != selection) onSelectionChange(newSelection)
190 return dragDistance
191 }
192 }
193
Qingqing Deng13743f72019-07-15 15:00:45 -0700194 /**
195 * Adjust coordinates for given text offset.
196 *
197 * Currently [android.text.Layout.getLineBottom] returns y coordinates of the next
198 * line's top offset, which is not included in current line's hit area. To be able to
199 * hit current line, move up this y coordinates by 1 pixel.
200 */
Siyamed Sinir472c3162019-10-21 23:41:00 -0700201 private fun getAdjustedCoordinates(position: PxPosition): PxPosition {
202 return PxPosition(position.x, position.y - 1.px)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700203 }
204
Siyamed Sinir472c3162019-10-21 23:41:00 -0700205 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700206 return object : DragObserver {
Andrey Kulikov0e6c40f2019-09-06 18:33:14 +0100207 override fun onStart(downPosition: PxPosition) {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700208 val selection = selection!!
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100209 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700210 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100211 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700212 val beginLayoutCoordinates = if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800213 selection.start.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700214 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800215 selection.end.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700216 }
217
Qingqing Deng6f56a912019-05-13 10:10:37 -0700218 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100219 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700220 val beginCoordinates = getAdjustedCoordinates(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800221 if (isStartHandle)
222 selection.start.selectable.getHandlePosition(
223 selection = selection, isStartHandle = true
224 ) else
225 selection.end.selectable.getHandlePosition(
226 selection = selection, isStartHandle = false
227 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700228 )
229
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100230 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700231 // selection container coordinates.
232 dragBeginPosition = containerLayoutCoordinates.childToLocal(
233 beginLayoutCoordinates,
234 beginCoordinates
235 )
236
237 // Zero out the total distance that being dragged.
238 dragTotalDistance = PxPosition.Origin
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700239 draggingHandle = true
Qingqing Deng6f56a912019-05-13 10:10:37 -0700240 }
241
242 override fun onDrag(dragDistance: PxPosition): PxPosition {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700243 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700244 dragTotalDistance += dragDistance
245
Siyamed Sinir472c3162019-10-21 23:41:00 -0700246 val currentStart = if (isStartHandle) {
247 dragBeginPosition + dragTotalDistance
248 } else {
249 containerLayoutCoordinates.childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800250 selection.start.selectable.getLayoutCoordinates()!!,
251 getAdjustedCoordinates(
252 selection.start.selectable.getHandlePosition(
253 selection = selection,
254 isStartHandle = true
255 )
256 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700257 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700258 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700259
260 val currentEnd = if (isStartHandle) {
261 containerLayoutCoordinates.childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800262 selection.end.selectable.getLayoutCoordinates()!!,
263 getAdjustedCoordinates(
264 selection.end.selectable.getHandlePosition(
265 selection = selection,
266 isStartHandle = false
267 )
268 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700269 )
270 } else {
271 dragBeginPosition + dragTotalDistance
272 }
273
Qingqing Denga5d80952019-10-11 16:46:52 -0700274 val finalSelection = mergeSelections(
275 startPosition = currentStart,
276 endPosition = currentEnd,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800277 previousSelection = selection,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800278 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700279 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700280 onSelectionChange(finalSelection)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700281 return dragDistance
282 }
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700283
284 override fun onStop(velocity: PxPosition) {
285 super.onStop(velocity)
286 draggingHandle = false
287 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700288 }
289 }
290}
291
Siyamed Sinir700df8452019-10-22 20:23:58 -0700292private fun merge(lhs: Selection?, rhs: Selection?): Selection? {
293 return lhs?.merge(rhs) ?: rhs
294}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800295
296private fun getCurrentSelectedText(
297 selectable: Selectable,
298 selection: Selection
299): AnnotatedString {
300 val currentText = selectable.getText()
301
302 return if (
303 selectable != selection.start.selectable &&
304 selectable != selection.end.selectable
305 ) {
306 // Select the full text content if the current selectable is between the
307 // start and the end selectables.
308 currentText
309 } else if (
310 selectable == selection.start.selectable &&
311 selectable == selection.end.selectable
312 ) {
313 // Select partial text content if the current selectable is the start and
314 // the end selectable.
315 if (selection.handlesCrossed) {
316 currentText.subSequence(selection.end.offset, selection.start.offset)
317 } else {
318 currentText.subSequence(selection.start.offset, selection.end.offset)
319 }
320 } else if (selectable == selection.start.selectable) {
321 // Select partial text content if the current selectable is the start
322 // selectable.
323 if (selection.handlesCrossed) {
324 currentText.subSequence(0, selection.start.offset)
325 } else {
326 currentText.subSequence(selection.start.offset, currentText.length)
327 }
328 } else {
329 // Selectable partial text content if the current selectable is the end
330 // selectable.
331 if (selection.handlesCrossed) {
332 currentText.subSequence(selection.end.offset, currentText.length)
333 } else {
334 currentText.subSequence(0, selection.end.offset)
335 }
336 }
337}