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
)