[go: nahoru, domu]

blob: 6e328e03d06354efe63485880e9069af30584b37 [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
George Mount842c8c12020-01-08 16:03:42 -080024import androidx.ui.unit.PxPosition
25import androidx.ui.unit.px
Qingqing Deng6f56a912019-05-13 10:10:37 -070026
Qingqing Deng35f97ea2019-09-18 19:24:37 -070027/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010028 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070029 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070030internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070031 /**
32 * The current selection.
33 */
34 var selection: Selection? = null
35
36 /**
37 * The manager will invoke this every time it comes to the conclusion that the selection should
38 * change. The expectation is that this callback will end up causing `setSelection` to get
39 * called. This is what makes this a "controlled component".
40 */
41 var onSelectionChange: (Selection?) -> Unit = {}
42
43 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080044 * [HapticFeedback] handle to perform haptic feedback.
45 */
46 var hapticFeedBack: HapticFeedback? = null
47
48 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070049 * Layout Coordinates of the selection container.
50 */
51 lateinit var containerLayoutCoordinates: LayoutCoordinates
52
53 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070054 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
55 * recalculated.
56 */
57 private var dragBeginPosition = PxPosition.Origin
58
59 /**
60 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
61 * it will be zeroed out.
62 */
63 private var dragTotalDistance = PxPosition.Origin
64
65 /**
Qingqing Deng0cb86fe2019-07-16 15:36:27 -070066 * A flag to check if the selection start or end handle is being dragged.
67 * If this value is true, then onPress will not select any text.
68 * This value will be set to true when either handle is being dragged, and be reset to false
69 * when the dragging is stopped.
70 */
71 private var draggingHandle = false
72
73 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -070074 * Iterates over the handlers, gets the selection for each Composable, and merges all the
75 * returned [Selection]s.
76 *
77 * @param startPosition [PxPosition] for the start of the selection
78 * @param endPosition [PxPosition] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -080079 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080080 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -070081 *
82 * @return [Selection] object which is constructed by combining all Composables that are
83 * selected.
84 */
Qingqing Deng5819ff62019-11-18 15:26:23 -080085 // This function is internal for testing purposes.
86 internal fun mergeSelections(
Siyamed Sinir472c3162019-10-21 23:41:00 -070087 startPosition: PxPosition,
88 endPosition: PxPosition,
Siyamed Sinir0100f122019-11-16 00:23:12 -080089 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080090 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -080091 isStartHandle: Boolean = true
Siyamed Sinir472c3162019-10-21 23:41:00 -070092 ): Selection? {
Siyamed Sinir0100f122019-11-16 00:23:12 -080093 val handlers = selectionRegistrar.selectables
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080094
95 val newSelection = handlers.fold(null) { mergedSelection: Selection?,
Siyamed Sinir0100f122019-11-16 00:23:12 -080096 handler: Selectable ->
Siyamed Sinir700df8452019-10-22 20:23:58 -070097 merge(
98 mergedSelection,
99 handler.getSelection(
100 startPosition = startPosition,
101 endPosition = endPosition,
102 containerLayoutCoordinates = containerLayoutCoordinates,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800103 longPress = longPress,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800104 previousSelection = previousSelection,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800105 isStartHandle = isStartHandle
Siyamed Sinir700df8452019-10-22 20:23:58 -0700106 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700107 )
108 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800109 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
110 HapticFeedbackType.TextHandleMove
111 )
112 return newSelection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700113 }
114
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800115 // This is for PressGestureDetector to cancel the selection.
116 fun onRelease() {
117 // Call mergeSelections with an out of boundary input to inform all text widgets to
118 // cancel their individual selection.
Qingqing Denga5d80952019-10-11 16:46:52 -0700119 mergeSelections(
Siyamed Sinir0100f122019-11-16 00:23:12 -0800120 startPosition = PxPosition((-1).px, (-1).px),
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800121 endPosition = PxPosition((-1).px, (-1).px),
122 previousSelection = selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800123 )
Siyamed Sinire810eab2019-11-22 12:36:38 -0800124 if (selection != null) onSelectionChange(null)
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800125 }
126
Siyamed Sinire810eab2019-11-22 12:36:38 -0800127 val longPressDragObserver = object : LongPressDragObserver {
128 override fun onLongPress(pxPosition: PxPosition) {
129 if (draggingHandle) return
130 val newSelection = mergeSelections(
131 startPosition = pxPosition,
132 endPosition = pxPosition,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800133 longPress = true,
134 previousSelection = selection
Siyamed Sinire810eab2019-11-22 12:36:38 -0800135 )
136 if (newSelection != selection) onSelectionChange(newSelection)
137 dragBeginPosition = pxPosition
Siyamed Sinir700df8452019-10-22 20:23:58 -0700138 }
Siyamed Sinir700df8452019-10-22 20:23:58 -0700139
Siyamed Sinire810eab2019-11-22 12:36:38 -0800140 override fun onDragStart() {
141 super.onDragStart()
142 // selection never started
143 if (selection == null) return
144 // Zero out the total distance that being dragged.
145 dragTotalDistance = PxPosition.Origin
146 }
147
148 override fun onDrag(dragDistance: PxPosition): PxPosition {
149 // selection never started, did not consume any drag
150 if (selection == null) return PxPosition.Origin
151
152 dragTotalDistance += dragDistance
153 val newSelection = mergeSelections(
154 startPosition = dragBeginPosition,
155 endPosition = dragBeginPosition + dragTotalDistance,
Qingqing Deng74baaa22019-12-12 13:28:25 -0800156 longPress = true,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800157 previousSelection = selection
Siyamed Sinire810eab2019-11-22 12:36:38 -0800158 )
159
160 if (newSelection != selection) onSelectionChange(newSelection)
161 return dragDistance
162 }
163 }
164
Qingqing Deng13743f72019-07-15 15:00:45 -0700165 /**
166 * Adjust coordinates for given text offset.
167 *
168 * Currently [android.text.Layout.getLineBottom] returns y coordinates of the next
169 * line's top offset, which is not included in current line's hit area. To be able to
170 * hit current line, move up this y coordinates by 1 pixel.
171 */
Siyamed Sinir472c3162019-10-21 23:41:00 -0700172 private fun getAdjustedCoordinates(position: PxPosition): PxPosition {
173 return PxPosition(position.x, position.y - 1.px)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700174 }
175
Siyamed Sinir472c3162019-10-21 23:41:00 -0700176 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700177 return object : DragObserver {
Andrey Kulikov0e6c40f2019-09-06 18:33:14 +0100178 override fun onStart(downPosition: PxPosition) {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700179 val selection = selection!!
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100180 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700181 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100182 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700183 val beginLayoutCoordinates = if (isStartHandle) {
184 selection.start.layoutCoordinates!!
185 } else {
186 selection.end.layoutCoordinates!!
187 }
188
Qingqing Deng6f56a912019-05-13 10:10:37 -0700189 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100190 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700191 val beginCoordinates = getAdjustedCoordinates(
192 if (isStartHandle) {
193 selection.start.coordinates
194 } else {
195 selection.end.coordinates
196 }
197 )
198
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100199 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700200 // selection container coordinates.
201 dragBeginPosition = containerLayoutCoordinates.childToLocal(
202 beginLayoutCoordinates,
203 beginCoordinates
204 )
205
206 // Zero out the total distance that being dragged.
207 dragTotalDistance = PxPosition.Origin
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700208 draggingHandle = true
Qingqing Deng6f56a912019-05-13 10:10:37 -0700209 }
210
211 override fun onDrag(dragDistance: PxPosition): PxPosition {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700212 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700213 dragTotalDistance += dragDistance
214
Siyamed Sinir472c3162019-10-21 23:41:00 -0700215 val currentStart = if (isStartHandle) {
216 dragBeginPosition + dragTotalDistance
217 } else {
218 containerLayoutCoordinates.childToLocal(
219 selection.start.layoutCoordinates!!,
220 getAdjustedCoordinates(selection.start.coordinates)
221 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700222 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700223
224 val currentEnd = if (isStartHandle) {
225 containerLayoutCoordinates.childToLocal(
226 selection.end.layoutCoordinates!!,
227 getAdjustedCoordinates(selection.end.coordinates)
228 )
229 } else {
230 dragBeginPosition + dragTotalDistance
231 }
232
Qingqing Denga5d80952019-10-11 16:46:52 -0700233 val finalSelection = mergeSelections(
234 startPosition = currentStart,
235 endPosition = currentEnd,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800236 previousSelection = selection,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800237 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700238 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700239 onSelectionChange(finalSelection)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700240 return dragDistance
241 }
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700242
243 override fun onStop(velocity: PxPosition) {
244 super.onStop(velocity)
245 draggingHandle = false
246 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700247 }
248 }
249}
250
Siyamed Sinir700df8452019-10-22 20:23:58 -0700251private fun merge(lhs: Selection?, rhs: Selection?): Selection? {
252 return lhs?.merge(rhs) ?: rhs
253}