Merge "Drag and drop for LazyVerticalGrid demo" into androidx-main
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
index 838e3f6..f96933f4 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
@@ -169,19 +169,11 @@
         val draggingItem = draggingItemLayoutInfo ?: return
         val startOffset = draggingItem.offset + draggingItemOffset
         val endOffset = startOffset + draggingItem.size
+        val middleOffset = startOffset + (endOffset - startOffset) / 2f
 
         val targetItem = state.layoutInfo.visibleItemsInfo.find { item ->
-            if (item.offsetEnd > startOffset && item.offset < endOffset &&
+            middleOffset.toInt() in item.offset..item.offsetEnd &&
                 draggingItem.index != item.index
-            ) {
-                val delta = startOffset - draggingItem.offset
-                when {
-                    delta > 0 -> (endOffset > item.offsetEnd)
-                    else -> (startOffset < item.offset)
-                }
-            } else {
-                false
-            }
         }
         if (targetItem != null) {
             val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyGridDragAndDropDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyGridDragAndDropDemo.kt
new file mode 100644
index 0000000..b9c0354
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyGridDragAndDropDemo.kt
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.GridCells
+import androidx.compose.foundation.lazy.LazyGridItemInfo
+import androidx.compose.foundation.lazy.LazyGridItemScope
+import androidx.compose.foundation.lazy.LazyGridState
+import androidx.compose.foundation.lazy.LazyVerticalGrid
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyGridState
+import androidx.compose.material.Card
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.consumeAllChanges
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.toOffset
+import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.zIndex
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun LazyGridDragAndDropDemo() {
+    var list by remember { mutableStateOf(List(50) { it }) }
+
+    val gridState = rememberLazyGridState()
+    val dragDropState = rememberGridDragDropState(gridState) { fromIndex, toIndex ->
+        list = list.toMutableList().apply {
+            add(toIndex, removeAt(fromIndex))
+        }
+    }
+
+    LazyVerticalGrid(
+        cells = GridCells.Fixed(3),
+        modifier = Modifier.dragContainer(dragDropState),
+        state = gridState,
+        contentPadding = PaddingValues(16.dp),
+        verticalArrangement = Arrangement.spacedBy(16.dp),
+        horizontalArrangement = Arrangement.spacedBy(16.dp),
+    ) {
+        itemsIndexed(list, key = { _, item -> item }) { index, item ->
+            DraggableItem(dragDropState, index) { isDragging ->
+                val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp)
+                Card(elevation = elevation) {
+                    Text(
+                        "Item $item",
+                        textAlign = TextAlign.Center,
+                        modifier = Modifier.fillMaxWidth().padding(vertical = 40.dp)
+                    )
+                }
+            }
+        }
+    }
+}
+
+@ExperimentalFoundationApi
+@Composable
+fun rememberGridDragDropState(
+    gridState: LazyGridState,
+    onMove: (Int, Int) -> Unit
+): GridDragDropState {
+    val scope = rememberCoroutineScope()
+    val state = remember(gridState) {
+        GridDragDropState(
+            state = gridState,
+            onMove = onMove,
+            scope = scope
+        )
+    }
+    LaunchedEffect(state) {
+        while (true) {
+            val diff = state.scrollChannel.receive()
+            gridState.scrollBy(diff)
+        }
+    }
+    return state
+}
+
+@ExperimentalFoundationApi
+class GridDragDropState internal constructor(
+    private val state: LazyGridState,
+    private val scope: CoroutineScope,
+    private val onMove: (Int, Int) -> Unit
+) {
+    var draggingItemIndex by mutableStateOf<Int?>(null)
+        private set
+
+    internal val scrollChannel = Channel<Float>()
+
+    private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero)
+    private var draggingItemInitialOffset by mutableStateOf(Offset.Zero)
+    internal val draggingItemOffset: Offset
+        get() = draggingItemLayoutInfo?.let { item ->
+            draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset()
+        } ?: Offset.Zero
+
+    private val draggingItemLayoutInfo: LazyGridItemInfo?
+        get() = state.layoutInfo.visibleItemsInfo
+            .firstOrNull { it.index == draggingItemIndex }
+
+    internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
+        private set
+    internal var previousItemOffset = Animatable(Offset.Zero, Offset.VectorConverter)
+        private set
+
+    internal fun onDragStart(offset: Offset) {
+        state.layoutInfo.visibleItemsInfo
+            .firstOrNull { item ->
+                offset.x.toInt() in item.offset.x..item.offsetEnd.x &&
+                    offset.y.toInt() in item.offset.y..item.offsetEnd.y
+            }?.also {
+                draggingItemIndex = it.index
+                draggingItemInitialOffset = it.offset.toOffset()
+            }
+    }
+
+    internal fun onDragInterrupted() {
+        if (draggingItemIndex != null) {
+            previousIndexOfDraggedItem = draggingItemIndex
+            val startOffset = draggingItemOffset
+            scope.launch {
+                previousItemOffset.snapTo(startOffset)
+                previousItemOffset.animateTo(
+                    Offset.Zero,
+                    spring(
+                        stiffness = Spring.StiffnessMediumLow,
+                        visibilityThreshold = Offset.VisibilityThreshold
+                    )
+                )
+                previousIndexOfDraggedItem = null
+            }
+        }
+        draggingItemDraggedDelta = Offset.Zero
+        draggingItemIndex = null
+        draggingItemInitialOffset = Offset.Zero
+    }
+
+    internal fun onDrag(offset: Offset) {
+        draggingItemDraggedDelta += offset
+
+        val draggingItem = draggingItemLayoutInfo ?: return
+        val startOffset = draggingItem.offset.toOffset() + draggingItemOffset
+        val endOffset = startOffset + draggingItem.size.toSize()
+        val middleOffset = startOffset + (endOffset - startOffset) / 2f
+
+        val targetItem = state.layoutInfo.visibleItemsInfo.find { item ->
+            middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x &&
+                middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y &&
+                draggingItem.index != item.index
+        }
+        if (targetItem != null) {
+            val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) {
+                draggingItem.index
+            } else if (draggingItem.index == state.firstVisibleItemIndex) {
+                targetItem.index
+            } else {
+                null
+            }
+            if (scrollToIndex != null) {
+                scope.launch {
+                    // this is needed to neutralize automatic keeping the first item first.
+                    state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
+                    onMove.invoke(draggingItem.index, targetItem.index)
+                }
+            } else {
+                onMove.invoke(draggingItem.index, targetItem.index)
+            }
+            draggingItemIndex = targetItem.index
+        } else {
+            val overscroll = when {
+                draggingItemDraggedDelta.y > 0 ->
+                    (endOffset.y - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
+                draggingItemDraggedDelta.y < 0 ->
+                    (startOffset.y - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
+                else -> 0f
+            }
+            if (overscroll != 0f) {
+                scrollChannel.trySend(overscroll)
+            }
+        }
+    }
+
+    private val LazyGridItemInfo.offsetEnd: IntOffset
+        get() = this.offset + this.size
+}
+
+private operator fun IntOffset.plus(size: IntSize): IntOffset {
+    return IntOffset(x + size.width, y + size.height)
+}
+
+private operator fun Offset.plus(size: Size): Offset {
+    return Offset(x + size.width, y + size.height)
+}
+
+@ExperimentalFoundationApi
+fun Modifier.dragContainer(dragDropState: GridDragDropState): Modifier {
+    return pointerInput(dragDropState) {
+        detectDragGesturesAfterLongPress(
+            onDrag = { change, offset ->
+                change.consumeAllChanges()
+                dragDropState.onDrag(offset = offset)
+            },
+            onDragStart = { offset -> dragDropState.onDragStart(offset) },
+            onDragEnd = { dragDropState.onDragInterrupted() },
+            onDragCancel = { dragDropState.onDragInterrupted() }
+        )
+    }
+}
+
+@ExperimentalFoundationApi
+@Composable
+fun LazyGridItemScope.DraggableItem(
+    dragDropState: GridDragDropState,
+    index: Int,
+    modifier: Modifier = Modifier,
+    content: @Composable (isDragging: Boolean) -> Unit
+) {
+    val dragging = index == dragDropState.draggingItemIndex
+    val draggingModifier = if (dragging) {
+        Modifier
+            .zIndex(1f)
+            .graphicsLayer {
+                translationX = dragDropState.draggingItemOffset.x
+                translationY = dragDropState.draggingItemOffset.y
+            }
+    } else if (index == dragDropState.previousIndexOfDraggedItem) {
+        Modifier.zIndex(1f)
+            .graphicsLayer {
+                translationX = dragDropState.previousItemOffset.value.x
+                translationY = dragDropState.previousItemOffset.value.y
+            }
+    } else {
+        Modifier.animateItemPlacement()
+    }
+    Box(modifier = modifier.then(draggingModifier), propagateMinConstraints = true) {
+        content(dragging)
+    }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
index eca4e16..5453664 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
@@ -111,7 +111,8 @@
     ComposableDemo("Custom keys") { ReorderWithCustomKeys() },
     ComposableDemo("Fling Config") { LazyWithFlingConfig() },
     ComposableDemo("Item reordering") { PopularBooksDemo() },
-    ComposableDemo("Drag and drop") { LazyColumnDragAndDropDemo() },
+    ComposableDemo("List drag and drop") { LazyColumnDragAndDropDemo() },
+    ComposableDemo("Grid drag and drop") { LazyGridDragAndDropDemo() },
     PagingDemos
 )