1 /*
<lambda>null2 * Copyright (C) 2023 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
17 package com.android.systemui.communal.ui.compose
18
19 import android.content.ClipDescription
20 import android.view.DragEvent
21 import androidx.compose.foundation.ExperimentalFoundationApi
22 import androidx.compose.foundation.draganddrop.dragAndDropTarget
23 import androidx.compose.foundation.gestures.Orientation
24 import androidx.compose.foundation.gestures.scrollBy
25 import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
26 import androidx.compose.foundation.lazy.grid.LazyGridState
27 import androidx.compose.runtime.Composable
28 import androidx.compose.runtime.LaunchedEffect
29 import androidx.compose.runtime.MutableState
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.mutableFloatStateOf
32 import androidx.compose.runtime.remember
33 import androidx.compose.runtime.rememberCoroutineScope
34 import androidx.compose.runtime.rememberUpdatedState
35 import androidx.compose.ui.Modifier
36 import androidx.compose.ui.draganddrop.DragAndDropEvent
37 import androidx.compose.ui.draganddrop.DragAndDropTarget
38 import androidx.compose.ui.draganddrop.mimeTypes
39 import androidx.compose.ui.draganddrop.toAndroidDragEvent
40 import androidx.compose.ui.geometry.Offset
41 import androidx.compose.ui.platform.LocalDensity
42 import androidx.compose.ui.unit.dp
43 import com.android.systemui.communal.domain.model.CommunalContentModel
44 import com.android.systemui.communal.ui.compose.extensions.plus
45 import com.android.systemui.communal.util.WidgetPickerIntentUtils
46 import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtraFromIntent
47 import kotlinx.coroutines.CoroutineScope
48 import kotlinx.coroutines.delay
49 import kotlinx.coroutines.isActive
50 import kotlinx.coroutines.launch
51
52 /**
53 * Holds state associated with dragging and dropping items from other activities into the lazy grid.
54 *
55 * @see dragAndDropTarget
56 */
57 @Composable
58 internal fun rememberDragAndDropTargetState(
59 gridState: LazyGridState,
60 contentListState: ContentListState,
61 updateDragPositionForRemove: (offset: Offset) -> Boolean,
62 ): DragAndDropTargetState {
63 val scope = rememberCoroutineScope()
64 val autoScrollSpeed = remember { mutableFloatStateOf(0f) }
65 // Threshold of distance from edges that should start auto-scroll - chosen to be a narrow value
66 // that allows differentiating intention of scrolling from intention of dragging over the first
67 // visible item.
68 val autoScrollThreshold = with(LocalDensity.current) { 60.dp.toPx() }
69 val state =
70 remember(gridState, contentListState) {
71 DragAndDropTargetState(
72 state = gridState,
73 contentListState = contentListState,
74 scope = scope,
75 autoScrollSpeed = autoScrollSpeed,
76 autoScrollThreshold = autoScrollThreshold,
77 updateDragPositionForRemove = updateDragPositionForRemove,
78 )
79 }
80 LaunchedEffect(autoScrollSpeed.floatValue) {
81 if (autoScrollSpeed.floatValue != 0f) {
82 while (isActive) {
83 gridState.scrollBy(autoScrollSpeed.floatValue)
84 delay(10)
85 }
86 }
87 }
88 return state
89 }
90
91 /**
92 * Attaches a listener for drag and drop events from other activities.
93 *
94 * @see androidx.compose.foundation.draganddrop.dragAndDropTarget
95 * @see DragEvent
96 */
97 @OptIn(ExperimentalFoundationApi::class)
98 @Composable
dragAndDropTargetnull99 internal fun Modifier.dragAndDropTarget(
100 dragDropTargetState: DragAndDropTargetState,
101 ): Modifier {
102 val state by rememberUpdatedState(dragDropTargetState)
103
104 return this then
105 Modifier.dragAndDropTarget(
106 shouldStartDragAndDrop = accept@{ startEvent ->
107 startEvent.mimeTypes().any { it == ClipDescription.MIMETYPE_TEXT_INTENT }
108 },
109 target =
110 object : DragAndDropTarget {
111 override fun onStarted(event: DragAndDropEvent) {
112 state.onStarted()
113 }
114
115 override fun onMoved(event: DragAndDropEvent) {
116 state.onMoved(event)
117 }
118
119 override fun onDrop(event: DragAndDropEvent): Boolean {
120 return state.onDrop(event)
121 }
122
123 override fun onEnded(event: DragAndDropEvent) {
124 state.onEnded()
125 }
126 }
127 )
128 }
129
130 /**
131 * Handles dropping of an item coming from a different activity (e.g. widget picker) in to the grid
132 * corresponding to the provided [LazyGridState].
133 *
134 * Adds a placeholder container to highlight the anticipated location the widget will be dropped to.
135 * When the item is held over an empty area, the placeholder appears at the end of the grid if one
136 * didn't exist already. As user moves the item over an existing item, the placeholder appears in
137 * place of that existing item. And then, the existing item is pushed over as part of re-ordering.
138 *
139 * Once item is dropped, new ordering along with the dropped item is persisted. See
140 * [ContentListState.onSaveList].
141 *
142 * Difference between this and [GridDragDropState] is that, this is used for listening to drops from
143 * other activities. [GridDragDropState] on the other hand, handles dragging of existing items in
144 * the communal hub grid.
145 */
146 internal class DragAndDropTargetState(
147 private val state: LazyGridState,
148 private val contentListState: ContentListState,
149 private val scope: CoroutineScope,
150 private val autoScrollSpeed: MutableState<Float>,
151 private val autoScrollThreshold: Float,
152 private val updateDragPositionForRemove: (offset: Offset) -> Boolean,
153 ) {
154 /**
155 * The placeholder item that is treated as if it is being dragged across the grid. It is added
156 * to grid once drag and drop event is started and removed when event ends.
157 */
158 private var placeHolder = CommunalContentModel.WidgetPlaceholder()
159
160 private var placeHolderIndex: Int? = null
161 private var isOnRemoveButton = false
162
onStartednull163 fun onStarted() {
164 // assume item will be added to the end.
165 contentListState.list.add(placeHolder)
166 placeHolderIndex = contentListState.list.size - 1
167 }
168
onMovednull169 fun onMoved(event: DragAndDropEvent) {
170 val dragEvent = event.toAndroidDragEvent()
171 isOnRemoveButton = updateDragPositionForRemove(Offset(dragEvent.x, dragEvent.y))
172 if (!isOnRemoveButton) {
173 findTargetItem(dragEvent)?.apply {
174 var scrollIndex: Int? = null
175 var scrollOffset: Int? = null
176 if (placeHolderIndex == state.firstVisibleItemIndex) {
177 // Save info about the first item before the move, to neutralize the automatic
178 // keeping first item first.
179 scrollIndex = placeHolderIndex
180 scrollOffset = state.firstVisibleItemScrollOffset
181 }
182
183 autoScrollIfNearEdges(dragEvent)
184
185 if (contentListState.isItemEditable(this.index)) {
186 movePlaceholderTo(this.index)
187 placeHolderIndex = this.index
188 }
189
190 if (scrollIndex != null && scrollOffset != null) {
191 // this is needed to neutralize automatic keeping the first item first.
192 scope.launch { state.scrollToItem(scrollIndex, scrollOffset) }
193 }
194 }
195 }
196 }
197
onDropnull198 fun onDrop(event: DragAndDropEvent): Boolean {
199 autoScrollSpeed.value = 0f
200 if (isOnRemoveButton) {
201 return false
202 }
203 return placeHolderIndex?.let { dropIndex ->
204 val widgetExtra = event.maybeWidgetExtra() ?: return false
205 val (componentName, user) = widgetExtra
206 if (componentName != null && user != null) {
207 // Placeholder isn't removed yet to allow the setting the right priority for items
208 // before adding in the new item.
209 contentListState.onSaveList(
210 newItemComponentName = componentName,
211 newItemUser = user,
212 newItemIndex = dropIndex
213 )
214 return@let true
215 }
216 return false
217 }
218 ?: false
219 }
220
onEndednull221 fun onEnded() {
222 autoScrollSpeed.value = 0f
223 placeHolderIndex = null
224 contentListState.list.remove(placeHolder)
225 isOnRemoveButton = updateDragPositionForRemove(Offset.Zero)
226 }
227
autoScrollIfNearEdgesnull228 private fun autoScrollIfNearEdges(dragEvent: DragEvent) {
229 val orientation = state.layoutInfo.orientation
230 val distanceFromStart =
231 if (orientation == Orientation.Horizontal) {
232 dragEvent.x
233 } else {
234 dragEvent.y
235 }
236 val distanceFromEnd =
237 if (orientation == Orientation.Horizontal) {
238 state.layoutInfo.viewportSize.width - dragEvent.x
239 } else {
240 state.layoutInfo.viewportSize.height - dragEvent.y
241 }
242 autoScrollSpeed.value =
243 when {
244 distanceFromEnd < autoScrollThreshold -> autoScrollThreshold - distanceFromEnd
245 distanceFromStart < autoScrollThreshold ->
246 -(autoScrollThreshold - distanceFromStart)
247 else -> 0f
248 }
249 }
250
findTargetItemnull251 private fun findTargetItem(dragEvent: DragEvent): LazyGridItemInfo? =
252 state.layoutInfo.visibleItemsInfo.firstOrNull { item ->
253 dragEvent.x.toInt() in item.offset.x..(item.offset + item.size).x &&
254 dragEvent.y.toInt() in item.offset.y..(item.offset + item.size).y
255 }
256
movePlaceholderTonull257 private fun movePlaceholderTo(index: Int) {
258 val currentIndex = contentListState.list.indexOf(placeHolder)
259 if (currentIndex != index) {
260 contentListState.onMove(currentIndex, index)
261 }
262 }
263
264 /**
265 * Parses and returns the intent extra associated with the widget that is dropped into the grid.
266 *
267 * Returns null if the drop event didn't include intent information.
268 */
DragAndDropEventnull269 private fun DragAndDropEvent.maybeWidgetExtra(): WidgetPickerIntentUtils.WidgetExtra? {
270 val clipData = this.toAndroidDragEvent().clipData.takeIf { it.itemCount != 0 }
271 return clipData?.getItemAt(0)?.intent?.let { intent -> getWidgetExtraFromIntent(intent) }
272 }
273 }
274