[go: nahoru, domu]

blob: 72a186b0b91ffc84dfb1100fd6f917782fbff7d0 [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
17package androidx.ui.core.selection
18
19import androidx.compose.Ambient
20import androidx.ui.core.LayoutCoordinates
21import androidx.ui.core.PxPosition
22import androidx.ui.core.gesture.DragObserver
23import androidx.ui.core.px
Qingqing Deng6f56a912019-05-13 10:10:37 -070024
Qingqing Deng35f97ea2019-09-18 19:24:37 -070025/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010026 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070027 */
28class SelectionManager : SelectionRegistrar {
Qingqing Deng6f56a912019-05-13 10:10:37 -070029 /**
30 * The current selection.
31 */
32 var selection: Selection? = null
33
34 /**
35 * The manager will invoke this every time it comes to the conclusion that the selection should
36 * change. The expectation is that this callback will end up causing `setSelection` to get
37 * called. This is what makes this a "controlled component".
38 */
39 var onSelectionChange: (Selection?) -> Unit = {}
40
41 /**
Qingqing Dengb745a942019-06-05 18:26:52 -070042 * The selection mode. The default value is Vertical.
43 */
44 var mode: SelectionMode = SelectionMode.Vertical
45
46 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070047 * Layout Coordinates of the selection container.
48 */
49 lateinit var containerLayoutCoordinates: LayoutCoordinates
50
51 /**
52 * This is essentially the list of registered components that want
53 * to handle text selection that are below the SelectionContainer.
54 */
55 private val handlers = mutableSetOf<TextSelectionHandler>()
56
57 /**
58 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
59 * recalculated.
60 */
61 private var dragBeginPosition = PxPosition.Origin
62
63 /**
64 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
65 * it will be zeroed out.
66 */
67 private var dragTotalDistance = PxPosition.Origin
68
69 /**
Qingqing Deng0cb86fe2019-07-16 15:36:27 -070070 * A flag to check if the selection start or end handle is being dragged.
71 * If this value is true, then onPress will not select any text.
72 * This value will be set to true when either handle is being dragged, and be reset to false
73 * when the dragging is stopped.
74 */
75 private var draggingHandle = false
76
77 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070078 * Allow a Text composable to "register" itself with the manager
79 */
80 override fun subscribe(handler: TextSelectionHandler): Any {
81 handlers.add(handler)
82 return handler
83 }
84
85 /**
86 * Allow a Text composable to "unregister" itself with the manager
87 */
88 override fun unsubscribe(key: Any) {
89 handlers.remove(key as TextSelectionHandler)
90 }
91
92 fun onPress(position: PxPosition) {
Qingqing Deng0cb86fe2019-07-16 15:36:27 -070093 if (draggingHandle) return
Qingqing Deng6f56a912019-05-13 10:10:37 -070094 var result: Selection? = null
95 for (handler in handlers) {
Qingqing Dengb745a942019-06-05 18:26:52 -070096 result += handler.getSelection(
97 Pair(position, position),
98 containerLayoutCoordinates,
99 mode)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700100 }
101 onSelectionChange(result)
102 }
103
Qingqing Deng13743f72019-07-15 15:00:45 -0700104 /**
105 * Adjust coordinates for given text offset.
106 *
107 * Currently [android.text.Layout.getLineBottom] returns y coordinates of the next
108 * line's top offset, which is not included in current line's hit area. To be able to
109 * hit current line, move up this y coordinates by 1 pixel.
110 */
111 fun getAdjustedCoordinates(p: PxPosition): PxPosition {
112 return PxPosition(p.x, p.y - 1.px)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700113 }
114
115 fun handleDragObserver(dragStartHandle: Boolean): DragObserver {
116 return object : DragObserver {
Andrey Kulikov0e6c40f2019-09-06 18:33:14 +0100117 override fun onStart(downPosition: PxPosition) {
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100118 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700119 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100120 // composable coordinates to selection container coordinates.
Qingqing Deng6f56a912019-05-13 10:10:37 -0700121 val beginLayoutCoordinates =
122 if (dragStartHandle) {
123 selection!!.startLayoutCoordinates!!
124 } else {
125 selection!!.endLayoutCoordinates!!
126 }
127 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100128 // the composable coordinates.
Qingqing Deng6f56a912019-05-13 10:10:37 -0700129 val beginCoordinates =
Qingqing Deng13743f72019-07-15 15:00:45 -0700130 getAdjustedCoordinates(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700131 if (dragStartHandle) {
Qingqing Deng13743f72019-07-15 15:00:45 -0700132 selection!!.startCoordinates
Qingqing Deng6f56a912019-05-13 10:10:37 -0700133 } else {
Qingqing Deng13743f72019-07-15 15:00:45 -0700134 selection!!.endCoordinates
Qingqing Deng6f56a912019-05-13 10:10:37 -0700135 }
136 )
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100137 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700138 // selection container coordinates.
139 dragBeginPosition = containerLayoutCoordinates.childToLocal(
140 beginLayoutCoordinates,
141 beginCoordinates
142 )
143
144 // Zero out the total distance that being dragged.
145 dragTotalDistance = PxPosition.Origin
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700146 draggingHandle = true
Qingqing Deng6f56a912019-05-13 10:10:37 -0700147 }
148
149 override fun onDrag(dragDistance: PxPosition): PxPosition {
150 var result = selection
151 dragTotalDistance += dragDistance
152
153 val currentStart =
154 if (dragStartHandle) {
155 dragBeginPosition + dragTotalDistance
156 } else {
157 containerLayoutCoordinates.childToLocal(
158 selection!!.startLayoutCoordinates!!,
Qingqing Deng13743f72019-07-15 15:00:45 -0700159 getAdjustedCoordinates(selection!!.startCoordinates)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700160 )
161 }
162
163 val currentEnd =
164 if (dragStartHandle) {
165 containerLayoutCoordinates.childToLocal(
166 selection!!.endLayoutCoordinates!!,
Qingqing Deng13743f72019-07-15 15:00:45 -0700167 getAdjustedCoordinates(selection!!.endCoordinates)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700168 )
169 } else {
170 dragBeginPosition + dragTotalDistance
171 }
172
173 for (handler in handlers) {
Qingqing Dengb745a942019-06-05 18:26:52 -0700174 result += handler.getSelection(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700175 Pair(currentStart, currentEnd),
Qingqing Dengb745a942019-06-05 18:26:52 -0700176 containerLayoutCoordinates,
177 mode)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700178 }
179 onSelectionChange(result)
180 return dragDistance
181 }
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700182
183 override fun onStop(velocity: PxPosition) {
184 super.onStop(velocity)
185 draggingHandle = false
186 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700187 }
188 }
189}
190
Qingqing Deng35f97ea2019-09-18 19:24:37 -0700191/**
192 * Ambient of SelectionRegistrar for SelectionManager.
193 */
194val SelectionRegistrarAmbient = Ambient.of<SelectionRegistrar> { SelectionManager() }