From 1d899fff2476754a66d0916f08d0913c98575315 Mon Sep 17 00:00:00 2001 From: Kemal Date: Mon, 22 Aug 2022 01:05:37 +0300 Subject: [PATCH 01/10] Fix infinite recomposition happening when `draggingItemLeft` and `draggingItemTop` values are used in composition --- .../org/burnoutcrew/reorderable/ReorderableState.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt index a6b06f5..13ebfc5 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt @@ -64,13 +64,13 @@ abstract class ReorderableState( internal val interactions = Channel() internal val scrollChannel = Channel() val draggingItemLeft: Float - get() = draggingLayoutInfo?.let { item -> + get() = if(draggingItemKey!=null) draggingLayoutInfo?.let { item -> (selected?.left ?: 0) + draggingDelta.x - item.left - } ?: 0f + } ?: 0f else 0f val draggingItemTop: Float - get() = draggingLayoutInfo?.let { item -> + get() = if(draggingItemKey!=null) draggingLayoutInfo?.let { item -> (selected?.top ?: 0) + draggingDelta.y - item.top - } ?: 0f + } ?: 0f else 0f abstract val isVerticalScroll: Boolean private val draggingLayoutInfo: T? get() = visibleItemsInfo From bb79e0bd86d1e58532d87c37b4f5b11a2e463ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Vesel=C3=BD?= Date: Mon, 24 Apr 2023 20:49:33 +0200 Subject: [PATCH 02/10] Compose 1.4, AGP 8.0, refactored forEachGesture with awaitEachGesture, refactored onDragStart detection --- android/build.gradle.kts | 12 +- build.gradle.kts | 8 +- gradle.properties | 6 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../burnoutcrew/reorderable/DetectReorder.kt | 68 ++++---- .../burnoutcrew/reorderable/DragGesture.kt | 165 ------------------ .../burnoutcrew/reorderable/Reorderable.kt | 79 ++++----- .../reorderable/ReorderableState.kt | 3 +- 8 files changed, 86 insertions(+), 257 deletions(-) delete mode 100644 reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DragGesture.kt diff --git a/android/build.gradle.kts b/android/build.gradle.kts index dbb57f8..e2e0659 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -6,12 +6,12 @@ plugins { dependencies { implementation(project(":reorderable")) - implementation("androidx.compose.runtime:runtime:1.3.3") - implementation("androidx.compose.material:material:1.3.1") - implementation("androidx.activity:activity-compose:1.6.1") + implementation("androidx.compose.runtime:runtime:1.4.2") + implementation("androidx.compose.material:material:1.4.2") + implementation("androidx.activity:activity-compose:1.7.1") implementation("com.google.android.material:material:1.8.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") implementation("androidx.navigation:navigation-compose:2.5.3") implementation("io.coil-kt:coil-compose:2.2.2") } @@ -38,7 +38,7 @@ android { } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } namespace = "org.burnoutcrew.android" } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index d7710e3..e262feb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,9 @@ plugins { `maven-publish` - id("com.android.library") version "7.4.0" apply false - id("org.jetbrains.kotlin.multiplatform") version "1.8.0" apply false - id("org.jetbrains.kotlin.android") version "1.8.0" apply false - id("org.jetbrains.compose") version "1.3.0" apply false + id("com.android.library") version "8.0.0" apply false + id("org.jetbrains.kotlin.multiplatform") version "1.8.20" apply false + id("org.jetbrains.kotlin.android") version "1.8.20" apply false + id("org.jetbrains.compose") version "1.4.0" apply false } ext { diff --git a/gradle.properties b/gradle.properties index 5a66a00..a61d528 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,8 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" android.useAndroidX=true -org.jetbrains.compose.experimental.jscanvas.enabled=true \ No newline at end of file +org.jetbrains.compose.experimental.jscanvas.enabled=true + +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661..da1db5f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt index d73f341..9bbf7e1 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt @@ -15,46 +15,44 @@ */ package org.burnoutcrew.reorderable +import androidx.compose.foundation.gestures.awaitDragOrCancellation +import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.gestures.forEachGesture +import androidx.compose.foundation.gestures.awaitLongPressOrCancellation +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.AwaitPointerEventScope +import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow -fun Modifier.detectReorder(state: ReorderableState<*>) = - this.then( - Modifier.pointerInput(Unit) { - forEachGesture { - awaitPointerEventScope { - val down = awaitFirstDown(requireUnconsumed = false) - var drag: PointerInputChange? - var overSlop = Offset.Zero - do { - drag = awaitPointerSlopOrCancellation(down.id, down.type) { change, over -> - change.consume() - overSlop = over - } - } while (drag != null && !drag.isConsumed) - if (drag != null) { - state.interactions.trySend(StartDrag(down.id, overSlop)) - } - } - } - } - ) - - -fun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) = - this.then( - Modifier.pointerInput(Unit) { - forEachGesture { - val down = awaitPointerEventScope { - awaitFirstDown(requireUnconsumed = false) - } - awaitLongPressOrCancellation(down)?.also { - state.interactions.trySend(StartDrag(down.id)) - } +fun Modifier.detectReorder(state: ReorderableState<*>) = detect(state){ + awaitDragOrCancellation(it) +} + +fun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) = detect(state) { pointerId -> + awaitLongPressOrCancellation(pointerId) +} + + +private fun Modifier.detect(state: ReorderableState<*>, detect: suspend AwaitPointerEventScope.(PointerId)->PointerInputChange?) = composed { + + val itemPosition = remember { mutableStateOf(Offset.Zero) } + + Modifier.onGloballyPositioned { itemPosition.value = it.positionInWindow() }.pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + val start = detect(down.id) + + if (start != null) { + val relativePosition = itemPosition.value - state.layoutWindowPosition.value + start.position + state.onDragStart(relativePosition.x.toInt(), relativePosition.y.toInt()) } } - ) \ No newline at end of file + } +} diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DragGesture.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DragGesture.kt deleted file mode 100644 index 24e90d9..0000000 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DragGesture.kt +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright 2020 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 org.burnoutcrew.reorderable - -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.pointer.AwaitPointerEventScope -import androidx.compose.ui.input.pointer.PointerEvent -import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.input.pointer.PointerId -import androidx.compose.ui.input.pointer.PointerInputChange -import androidx.compose.ui.input.pointer.PointerInputScope -import androidx.compose.ui.input.pointer.PointerType -import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed -import androidx.compose.ui.input.pointer.isOutOfBounds -import androidx.compose.ui.input.pointer.positionChange -import androidx.compose.ui.platform.ViewConfiguration -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastAll -import androidx.compose.ui.util.fastAny -import androidx.compose.ui.util.fastFirstOrNull -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.withTimeout - -// Copied from DragGestureDetector , as long the pointer api isn`t ready. - -internal suspend fun AwaitPointerEventScope.awaitPointerSlopOrCancellation( - pointerId: PointerId, - pointerType: PointerType, - onPointerSlopReached: (change: PointerInputChange, overSlop: Offset) -> Unit -): PointerInputChange? { - if (currentEvent.isPointerUp(pointerId)) { - return null // The pointer has already been lifted, so the gesture is canceled - } - var offset = Offset.Zero - val touchSlop = viewConfiguration.pointerSlop(pointerType) - - var pointer = pointerId - - while (true) { - val event = awaitPointerEvent() - val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null - if (dragEvent.isConsumed) { - return null - } else if (dragEvent.changedToUpIgnoreConsumed()) { - val otherDown = event.changes.fastFirstOrNull { it.pressed } - if (otherDown == null) { - // This is the last "up" - return null - } else { - pointer = otherDown.id - } - } else { - offset += dragEvent.positionChange() - val distance = offset.getDistance() - var acceptedDrag = false - if (distance >= touchSlop) { - val touchSlopOffset = offset / distance * touchSlop - onPointerSlopReached(dragEvent, offset - touchSlopOffset) - if (dragEvent.isConsumed) { - acceptedDrag = true - } else { - offset = Offset.Zero - } - } - - if (acceptedDrag) { - return dragEvent - } else { - awaitPointerEvent(PointerEventPass.Final) - if (dragEvent.isConsumed) { - return null - } - } - } - } -} - -internal suspend fun PointerInputScope.awaitLongPressOrCancellation( - initialDown: PointerInputChange -): PointerInputChange? { - var longPress: PointerInputChange? = null - var currentDown = initialDown - val longPressTimeout = viewConfiguration.longPressTimeoutMillis - return try { - // wait for first tap up or long press - withTimeout(longPressTimeout) { - awaitPointerEventScope { - var finished = false - while (!finished) { - val event = awaitPointerEvent(PointerEventPass.Main) - if (event.changes.fastAll { it.changedToUpIgnoreConsumed() }) { - // All pointers are up - finished = true - } - - if ( - event.changes.fastAny { - it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding) - } - ) { - finished = true // Canceled - } - - // Check for cancel by position consumption. We can look on the Final pass of - // the existing pointer event because it comes after the Main pass we checked - // above. - val consumeCheck = awaitPointerEvent(PointerEventPass.Final) - if (consumeCheck.changes.fastAny { it.isConsumed }) { - finished = true - } - if (!event.isPointerUp(currentDown.id)) { - longPress = event.changes.fastFirstOrNull { it.id == currentDown.id } - } else { - val newPressed = event.changes.fastFirstOrNull { it.pressed } - if (newPressed != null) { - currentDown = newPressed - longPress = currentDown - } else { - // should technically never happen as we checked it above - finished = true - } - } - } - } - } - null - } catch (_: TimeoutCancellationException) { - longPress ?: initialDown - } -} - -private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean = - changes.fastFirstOrNull { it.id == pointerId }?.pressed != true - -// This value was determined using experiments and common sense. -// We can't use zero slop, because some hypothetical desktop/mobile devices can send -// pointer events with a very high precision (but I haven't encountered any that send -// events with less than 1px precision) -private val mouseSlop = 0.125.dp -private val defaultTouchSlop = 18.dp // The default touch slop on Android devices -private val mouseToTouchSlopRatio = mouseSlop / defaultTouchSlop - -// TODO(demin): consider this as part of ViewConfiguration class after we make *PointerSlop* -// functions public (see the comment at the top of the file). -// After it will be a public API, we should get rid of `touchSlop / 144` and return absolute -// value 0.125.dp.toPx(). It is not possible right now, because we can't access density. -private fun ViewConfiguration.pointerSlop(pointerType: PointerType): Float { - return when (pointerType) { - PointerType.Mouse -> touchSlop * mouseToTouchSlopRatio - else -> touchSlop - } -} \ No newline at end of file diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt index e9d6d07..29f246f 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt @@ -15,69 +15,60 @@ */ package org.burnoutcrew.reorderable +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.drag -import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerInputChange -import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.positionChange -import androidx.compose.ui.util.fastFirstOrNull +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow fun Modifier.reorderable( state: ReorderableState<*> ) = then( - Modifier.pointerInput(Unit) { - forEachGesture { - val dragStart = state.interactions.receive() - val down = awaitPointerEventScope { - currentEvent.changes.fastFirstOrNull { it.id == dragStart.id } - } - if (down != null && state.onDragStart(down.position.x.toInt(), down.position.y.toInt())) { - dragStart.offset?.apply { - state.onDrag(x.toInt(), y.toInt()) + Modifier.onGloballyPositioned { state.layoutWindowPosition.value = it.positionInWindow()}.pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + + detectDrag( + down.id, + onDragEnd = state::onDragCanceled, + onDragCancel = state::onDragCanceled, + onDrag = { event, amount -> + if (state.draggingItemIndex != null){ + state.onDrag(amount.x.toInt(), amount.y.toInt()) + event.consume() + } } - detectDrag( - down.id, - onDragEnd = { - state.onDragCanceled() - }, - onDragCancel = { - state.onDragCanceled() - }, - onDrag = { change, dragAmount -> - change.consume() - state.onDrag(dragAmount.x.toInt(), dragAmount.y.toInt()) - }) - } + ) + } - }) + } +) -internal suspend fun PointerInputScope.detectDrag( +internal suspend fun AwaitPointerEventScope.detectDrag( down: PointerId, onDragEnd: () -> Unit = { }, onDragCancel: () -> Unit = { }, onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit, ) { - awaitPointerEventScope { - if ( - drag(down) { - onDrag(it, it.positionChange()) - it.consume() - } - ) { - // consume up if we quit drag gracefully with the up - currentEvent.changes.forEach { - if (it.changedToUp()) it.consume() - } - onDragEnd() - } else { - onDragCancel() + if ( + drag(down) { + onDrag(it, it.positionChange()) } + ) { + // consume up if we quit drag gracefully with the up + currentEvent.changes.forEach { + if (it.changedToUp()) it.consume() + } + onDragEnd() + } else { + onDragCancel() } -} - -internal data class StartDrag(val id: PointerId, val offset: Offset? = null) \ No newline at end of file +} \ No newline at end of file diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt index f4b5c6e..44a954a 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt @@ -44,6 +44,8 @@ abstract class ReorderableState( private val onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))?, val dragCancelledAnimation: DragCancelledAnimation ) { + var layoutWindowPosition = mutableStateOf(Offset.Zero) + var draggingItemIndex by mutableStateOf(null) private set val draggingItemKey: Any? @@ -61,7 +63,6 @@ abstract class ReorderableState( protected abstract val firstVisibleItemScrollOffset: Int protected abstract val viewportStartOffset: Int protected abstract val viewportEndOffset: Int - internal val interactions = Channel() internal val scrollChannel = Channel() val draggingItemLeft: Float get() = draggingLayoutInfo?.let { item -> From 9722e9bf26bf9ae71f1645d1000c4f621b2cffcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Vesel=C3=BD?= Date: Mon, 1 May 2023 10:42:07 +0200 Subject: [PATCH 03/10] Improve code readability --- .../burnoutcrew/reorderable/DetectReorder.kt | 4 +- .../burnoutcrew/reorderable/Reorderable.kt | 48 +++++-------------- 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt index 9bbf7e1..e875f6d 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt @@ -35,8 +35,8 @@ fun Modifier.detectReorder(state: ReorderableState<*>) = detect(state){ awaitDragOrCancellation(it) } -fun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) = detect(state) { pointerId -> - awaitLongPressOrCancellation(pointerId) +fun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) = detect(state) { + awaitLongPressOrCancellation(it) } diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt index 29f246f..e20fa59 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt @@ -19,10 +19,6 @@ import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.drag import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.pointer.AwaitPointerEventScope -import androidx.compose.ui.input.pointer.PointerId -import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.positionChange @@ -36,39 +32,21 @@ fun Modifier.reorderable( awaitEachGesture { val down = awaitFirstDown(requireUnconsumed = false) - detectDrag( - down.id, - onDragEnd = state::onDragCanceled, - onDragCancel = state::onDragCanceled, - onDrag = { event, amount -> - if (state.draggingItemIndex != null){ - state.onDrag(amount.x.toInt(), amount.y.toInt()) - event.consume() - } + val dragResult = drag(down.id) { + if (state.draggingItemIndex != null){ + state.onDrag(it.positionChange().x.toInt(), it.positionChange().y.toInt()) + it.consume() } - ) + } - } - } -) + if (dragResult) { + // consume up if we quit drag gracefully with the up + currentEvent.changes.forEach { + if (it.changedToUp()) it.consume() + } + } -internal suspend fun AwaitPointerEventScope.detectDrag( - down: PointerId, - onDragEnd: () -> Unit = { }, - onDragCancel: () -> Unit = { }, - onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit, -) { - if ( - drag(down) { - onDrag(it, it.positionChange()) - } - ) { - // consume up if we quit drag gracefully with the up - currentEvent.changes.forEach { - if (it.changedToUp()) it.consume() + state.onDragCanceled() } - onDragEnd() - } else { - onDragCancel() } -} \ No newline at end of file +) \ No newline at end of file From 0832d09177697702aea409972aec6512bb7724f2 Mon Sep 17 00:00:00 2001 From: Brill Pappin Date: Fri, 2 Jun 2023 15:38:44 -0400 Subject: [PATCH 04/10] Added support for staggered grid --- .../android/ui/reorderlist/ReorderGrid.kt | 63 ++++++- .../ui/reorderlist/ReorderListViewModel.kt | 6 +- .../reorderable/ReorderableItem.kt | 76 +++++++- .../ReorderableLazyStaggeredGridState.kt | 165 ++++++++++++++++++ 4 files changed, 298 insertions(+), 12 deletions(-) create mode 100644 reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyStaggeredGridState.kt diff --git a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt index 24bc977..bba3fab 100644 --- a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt +++ b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt @@ -16,12 +16,15 @@ package org.burnoutcrew.android.ui.reorderlist import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -29,6 +32,9 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -38,8 +44,10 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import org.burnoutcrew.reorderable.ReorderableItem +import org.burnoutcrew.reorderable.detectReorder import org.burnoutcrew.reorderable.detectReorderAfterLongPress import org.burnoutcrew.reorderable.rememberReorderableLazyGridState +import org.burnoutcrew.reorderable.rememberReorderableLazyVerticalStaggeredGridState import org.burnoutcrew.reorderable.reorderable @Composable @@ -49,10 +57,60 @@ fun ReorderGrid(vm: ReorderListViewModel = viewModel()) { vm = vm, modifier = Modifier.padding(vertical = 16.dp) ) - VerticalGrid(vm = vm) + VerticalGrid(vm = vm, modifier = Modifier.padding(vertical = 16.dp)) + VerticalStaggeredGrid(vm = vm, modifier = Modifier.padding(vertical = 16.dp)) } } +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun VerticalStaggeredGrid( + vm: ReorderListViewModel, + modifier: Modifier = Modifier, +) { + val state = rememberReorderableLazyVerticalStaggeredGridState( + onMove = vm::moveDog, + canDragOver = vm::isDogDragEnabled + ) + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Fixed(4), + state = state.gridState, + contentPadding = PaddingValues(horizontal = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier.reorderable(state), + ) { + items(items = vm.dogs, key = { it.key }) { item -> + if (item.isLocked) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(100.dp) + .background(MaterialTheme.colors.surface) + ) { + Text(item.title) + } + } else { + ReorderableItem(state, item.key) { isDragging -> + val elevation = animateDpAsState(if (isDragging) 8.dp else 0.dp) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .detectReorder(state) + .shadow(elevation.value) + .aspectRatio(1f) + .background(MaterialTheme.colors.primary) + ) { + Text(item.title) + } + } + } + } + } +} + + @Composable private fun HorizontalGrid( vm: ReorderListViewModel, @@ -92,7 +150,8 @@ private fun VerticalGrid( vm: ReorderListViewModel, modifier: Modifier = Modifier, ) { - val state = rememberReorderableLazyGridState(onMove = vm::moveDog, canDragOver = vm::isDogDragEnabled) + val state = + rememberReorderableLazyGridState(onMove = vm::moveDog, canDragOver = vm::isDogDragEnabled) LazyVerticalGrid( columns = GridCells.Fixed(4), state = state.gridState, diff --git a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderListViewModel.kt b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderListViewModel.kt index 68f97f8..3fedbf2 100644 --- a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderListViewModel.kt +++ b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderListViewModel.kt @@ -22,8 +22,8 @@ import androidx.lifecycle.ViewModel import org.burnoutcrew.reorderable.ItemPosition class ReorderListViewModel : ViewModel() { - var cats by mutableStateOf(List(500) { ItemData("Cat $it", "id$it") }) - var dogs by mutableStateOf(List(500) { + var cats by mutableStateOf(List(6) { ItemData("Cat $it", "id$it") }) + var dogs by mutableStateOf(List(6) { if (it.mod(10) == 0) ItemData("Locked", "id$it", true) else ItemData("Dog $it", "id$it") }) @@ -40,4 +40,4 @@ class ReorderListViewModel : ViewModel() { } fun isDogDragEnabled(draggedOver: ItemPosition, dragging: ItemPosition) = dogs.getOrNull(draggedOver.index)?.isLocked != true -} \ No newline at end of file +} diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableItem.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableItem.kt index bbc95d2..346a6b6 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableItem.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableItem.kt @@ -15,14 +15,22 @@ */ package org.burnoutcrew.reorderable +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.grid.LazyGridItemScope +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.zIndex @OptIn(ExperimentalFoundationApi::class) @@ -34,7 +42,15 @@ fun LazyItemScope.ReorderableItem( index: Int? = null, orientationLocked: Boolean = true, content: @Composable BoxScope.(isDragging: Boolean) -> Unit -) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItemPlacement(), orientationLocked, index, content) +) = ReorderableItem( + reorderableState, + key, + modifier, + Modifier.animateItemPlacement(), + orientationLocked, + index, + content +) @OptIn(ExperimentalFoundationApi::class) @Composable @@ -44,7 +60,49 @@ fun LazyGridItemScope.ReorderableItem( modifier: Modifier = Modifier, index: Int? = null, content: @Composable BoxScope.(isDragging: Boolean) -> Unit -) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItemPlacement(), false, index, content) +) = ReorderableItem( + reorderableState, + key, + modifier, + Modifier.animateItemPlacement(), + false, + index, + content +) + + + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun LazyStaggeredGridItemScope.ReorderableItem( + reorderableState: ReorderableState<*>, + key: Any?, + modifier: Modifier = Modifier, + index: Int? = null, + content: @Composable BoxScope.(isDragging: Boolean) -> Unit +) = ReorderableItem( + reorderableState, + key, + modifier, + Modifier.animateDraggeableItemPlacement(), + false, + index, + content +) + +/** + * XXX LazyGridItemScope.Modifier.animateItemPlacement is missing from LazyStaggeredGridItemScope + * XXX Replace this when added to the compose library. + * + * @param animationSpec a finite animation that will be used to animate the item placement. + */ +@ExperimentalFoundationApi +fun Modifier.animateDraggeableItemPlacement( + animationSpec: FiniteAnimationSpec = spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ) +): Modifier = this @Composable fun ReorderableItem( @@ -66,8 +124,10 @@ fun ReorderableItem( Modifier .zIndex(1f) .graphicsLayer { - translationX = if (!orientationLocked || !state.isVerticalScroll) state.draggingItemLeft else 0f - translationY = if (!orientationLocked || state.isVerticalScroll) state.draggingItemTop else 0f + translationX = + if (!orientationLocked || !state.isVerticalScroll) state.draggingItemLeft else 0f + translationY = + if (!orientationLocked || state.isVerticalScroll) state.draggingItemTop else 0f } } else { val cancel = if (index != null) { @@ -78,8 +138,10 @@ fun ReorderableItem( if (cancel) { Modifier.zIndex(1f) .graphicsLayer { - translationX = if (!orientationLocked || !state.isVerticalScroll) state.dragCancelledAnimation.offset.x else 0f - translationY = if (!orientationLocked || state.isVerticalScroll) state.dragCancelledAnimation.offset.y else 0f + translationX = + if (!orientationLocked || !state.isVerticalScroll) state.dragCancelledAnimation.offset.x else 0f + translationY = + if (!orientationLocked || state.isVerticalScroll) state.dragCancelledAnimation.offset.y else 0f } } else { defaultDraggingModifier @@ -88,4 +150,4 @@ fun ReorderableItem( Box(modifier = modifier.then(draggingModifier)) { content(isDragging) } -} \ No newline at end of file +} diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyStaggeredGridState.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyStaggeredGridState.kt new file mode 100644 index 0000000..933b3e6 --- /dev/null +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyStaggeredGridState.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2022 André Claßen + * + * 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 + * + * https://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 org.burnoutcrew.reorderable + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.lazy.grid.LazyGridItemInfo +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState +import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope + + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun rememberReorderableLazyHorizontalStaggeredGridState( + onMove: (ItemPosition, ItemPosition) -> Unit, + gridState: LazyStaggeredGridState = rememberLazyStaggeredGridState(), + canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, + maxScrollPerFrame: Dp = 20.dp, + dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation(), +) = rememberReorderableLazyStaggeredGridState( + onMove = onMove, + gridState = gridState, + canDragOver = canDragOver, + onDragEnd = onDragEnd, + maxScrollPerFrame = maxScrollPerFrame, + dragCancelledAnimation = dragCancelledAnimation, + orientation = Orientation.Horizontal +) + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun rememberReorderableLazyVerticalStaggeredGridState( + onMove: (ItemPosition, ItemPosition) -> Unit, + gridState: LazyStaggeredGridState = rememberLazyStaggeredGridState(), + canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, + maxScrollPerFrame: Dp = 20.dp, + dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation(), +) = rememberReorderableLazyStaggeredGridState( + onMove = onMove, + gridState = gridState, + canDragOver = canDragOver, + onDragEnd = onDragEnd, + maxScrollPerFrame = maxScrollPerFrame, + dragCancelledAnimation = dragCancelledAnimation, + orientation = Orientation.Vertical +) + + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun rememberReorderableLazyStaggeredGridState( + onMove: (ItemPosition, ItemPosition) -> Unit, + gridState: LazyStaggeredGridState = rememberLazyStaggeredGridState(), + canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, + maxScrollPerFrame: Dp = 20.dp, + dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation(), + orientation: Orientation +): ReorderableLazyStaggeredGridState { + val maxScroll = with(LocalDensity.current) { maxScrollPerFrame.toPx() } + val scope = rememberCoroutineScope() + val state = remember(gridState) { + ReorderableLazyStaggeredGridState( + gridState, + scope, + maxScroll, + onMove, + canDragOver, + onDragEnd, + dragCancelledAnimation, + orientation = orientation + ) + } + LaunchedEffect(state) { + state.visibleItemsChanged() + .collect { state.onDrag(0, 0) } + } + + LaunchedEffect(state) { + while (true) { + val diff = state.scrollChannel.receive() + gridState.scrollBy(diff) + } + } + return state +} + +@OptIn(ExperimentalFoundationApi::class) +class ReorderableLazyStaggeredGridState( + val gridState: LazyStaggeredGridState, + scope: CoroutineScope, + maxScrollPerFrame: Float, + onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit), + canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, + dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation(), + val orientation: Orientation +) : ReorderableState( + scope, + maxScrollPerFrame, + onMove, + canDragOver, + onDragEnd, + dragCancelledAnimation +) { + override val isVerticalScroll: Boolean + get() = orientation == Orientation.Vertical // XXX gridState.isVertical is not accessible + override val LazyStaggeredGridItemInfo.left: Int + get() = offset.x + override val LazyStaggeredGridItemInfo.right: Int + get() = offset.x + size.width + override val LazyStaggeredGridItemInfo.top: Int + get() = offset.y + override val LazyStaggeredGridItemInfo.bottom: Int + get() = offset.y + size.height + override val LazyStaggeredGridItemInfo.width: Int + get() = size.width + override val LazyStaggeredGridItemInfo.height: Int + get() = size.height + override val LazyStaggeredGridItemInfo.itemIndex: Int + get() = index + override val LazyStaggeredGridItemInfo.itemKey: Any + get() = key + override val visibleItemsInfo: List + get() = gridState.layoutInfo.visibleItemsInfo + override val viewportStartOffset: Int + get() = gridState.layoutInfo.viewportStartOffset + override val viewportEndOffset: Int + get() = gridState.layoutInfo.viewportEndOffset + override val firstVisibleItemIndex: Int + get() = gridState.firstVisibleItemIndex + override val firstVisibleItemScrollOffset: Int + get() = gridState.firstVisibleItemScrollOffset + + override suspend fun scrollToItem(index: Int, offset: Int) { + gridState.scrollToItem(index, offset) + } +} From c5641634277fd3d42fd6050a0045c8e248acc6a8 Mon Sep 17 00:00:00 2001 From: Leo Cacheux Date: Wed, 16 Aug 2023 14:24:15 +0200 Subject: [PATCH 05/10] Add onDragStart callback --- .../org/burnoutcrew/reorderable/ReorderableLazyGridState.kt | 6 ++++-- .../org/burnoutcrew/reorderable/ReorderableLazyListState.kt | 5 ++++- .../kotlin/org/burnoutcrew/reorderable/ReorderableState.kt | 2 ++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyGridState.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyGridState.kt index b803861..a496fc4 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyGridState.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyGridState.kt @@ -34,6 +34,7 @@ fun rememberReorderableLazyGridState( onMove: (ItemPosition, ItemPosition) -> Unit, gridState: LazyGridState = rememberLazyGridState(), canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragStart: ((startIndex: Int, x: Int, y: Int) -> (Unit))? = null, onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, maxScrollPerFrame: Dp = 20.dp, dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation() @@ -41,7 +42,7 @@ fun rememberReorderableLazyGridState( val maxScroll = with(LocalDensity.current) { maxScrollPerFrame.toPx() } val scope = rememberCoroutineScope() val state = remember(gridState) { - ReorderableLazyGridState(gridState, scope, maxScroll, onMove, canDragOver, onDragEnd, dragCancelledAnimation) + ReorderableLazyGridState(gridState, scope, maxScroll, onMove, canDragOver, onDragStart, onDragEnd, dragCancelledAnimation) } LaunchedEffect(state) { state.visibleItemsChanged() @@ -63,9 +64,10 @@ class ReorderableLazyGridState( maxScrollPerFrame: Float, onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit), canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragStart: ((startIndex: Int, x: Int, y: Int) -> (Unit))? = null, onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation() -) : ReorderableState(scope, maxScrollPerFrame, onMove, canDragOver, onDragEnd, dragCancelledAnimation) { +) : ReorderableState(scope, maxScrollPerFrame, onMove, canDragOver, onDragStart, onDragEnd, dragCancelledAnimation) { override val isVerticalScroll: Boolean get() = gridState.layoutInfo.orientation == Orientation.Vertical override val LazyGridItemInfo.left: Int diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyListState.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyListState.kt index a9dae61..f1f3a9c 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyListState.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyListState.kt @@ -38,6 +38,7 @@ fun rememberReorderableLazyListState( onMove: (ItemPosition, ItemPosition) -> Unit, listState: LazyListState = rememberLazyListState(), canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragStart: ((startIndex: Int, x: Int, y: Int) -> (Unit))? = null, onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, maxScrollPerFrame: Dp = 20.dp, dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation() @@ -45,7 +46,7 @@ fun rememberReorderableLazyListState( val maxScroll = with(LocalDensity.current) { maxScrollPerFrame.toPx() } val scope = rememberCoroutineScope() val state = remember(listState) { - ReorderableLazyListState(listState, scope, maxScroll, onMove, canDragOver, onDragEnd, dragCancelledAnimation) + ReorderableLazyListState(listState, scope, maxScroll, onMove, canDragOver, onDragStart, onDragEnd, dragCancelledAnimation) } val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl LaunchedEffect(state) { @@ -73,6 +74,7 @@ class ReorderableLazyListState( maxScrollPerFrame: Float, onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit), canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragStart: ((startIndex: Int, x: Int, y: Int) -> (Unit))? = null, onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation() ) : ReorderableState( @@ -80,6 +82,7 @@ class ReorderableLazyListState( maxScrollPerFrame, onMove, canDragOver, + onDragStart, onDragEnd, dragCancelledAnimation ) { diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt index f4b5c6e..783a9dc 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt @@ -41,6 +41,7 @@ abstract class ReorderableState( private val maxScrollPerFrame: Float, private val onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit), private val canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)?, + private val onDragStart: ((startIndex: Int, x: Int, y: Int) -> (Unit))? = null, private val onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))?, val dragCancelledAnimation: DragCancelledAnimation ) { @@ -105,6 +106,7 @@ abstract class ReorderableState( ?.also { selected = it draggingItemIndex = it.itemIndex + onDragStart?.invoke(it.itemIndex, offsetX, offsetY) } != null } From 4ee3becc6bf5ae9b013f3205dcccf74d3ca54f2d Mon Sep 17 00:00:00 2001 From: Brill Pappin Date: Fri, 2 Feb 2024 09:23:00 -0500 Subject: [PATCH 06/10] updated the workflow version --- .github/workflows/pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3bb7468..a61c904 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -14,7 +14,7 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Check out code - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v3.5.2 with: fetch-depth: '0' @@ -33,4 +33,4 @@ jobs: with: name: Reports path: '**/build/reports/*' - retention-days: 2 \ No newline at end of file + retention-days: 2 From 8a47e89696b7d477393d927329bba615d7dbefdf Mon Sep 17 00:00:00 2001 From: Brill Pappin Date: Fri, 2 Feb 2024 09:51:58 -0500 Subject: [PATCH 07/10] #5 updated dependencies and code that was out of date. --- android/build.gradle.kts | 24 ++++++++++--------- .../android/ui/reorderlist/ReorderGrid.kt | 1 - build.gradle.kts | 8 +++---- gradle.properties | 13 +++++++++- gradle/wrapper/gradle-wrapper.properties | 2 +- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index dbb57f8..3a656af 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -6,14 +6,14 @@ plugins { dependencies { implementation(project(":reorderable")) - implementation("androidx.compose.runtime:runtime:1.3.3") - implementation("androidx.compose.material:material:1.3.1") - implementation("androidx.activity:activity-compose:1.6.1") - implementation("com.google.android.material:material:1.8.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1") - implementation("androidx.navigation:navigation-compose:2.5.3") - implementation("io.coil-kt:coil-compose:2.2.2") + implementation("androidx.compose.runtime:runtime:1.6.0") + implementation("androidx.compose.material:material:1.6.0") + implementation("androidx.activity:activity-compose:1.8.2") + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") + implementation("androidx.navigation:navigation-compose:2.7.6") + implementation("io.coil-kt:coil-compose:2.5.0") } android { @@ -21,13 +21,14 @@ android { sourceSets { map { it.java.srcDir("src/${it.name}/kotlin") } } + val minSdkVersion: Int by rootProject.extra val targetSdkVersion: Int by rootProject.extra val compileSdkVersion: Int by rootProject.extra - compileSdk = compileSdkVersion defaultConfig { minSdk = minSdkVersion targetSdk = targetSdkVersion + compileSdk = compileSdkVersion versionCode = 1 versionName = "1.0" } @@ -38,7 +39,8 @@ android { } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" //""1.8" } namespace = "org.burnoutcrew.android" -} \ No newline at end of file + buildToolsVersion = "34.0.0" +} diff --git a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt index bba3fab..fb60e85 100644 --- a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt +++ b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt @@ -76,7 +76,6 @@ private fun VerticalStaggeredGrid( columns = StaggeredGridCells.Fixed(4), state = state.gridState, contentPadding = PaddingValues(horizontal = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier.reorderable(state), ) { diff --git a/build.gradle.kts b/build.gradle.kts index d7710e3..615b797 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,15 +1,15 @@ plugins { `maven-publish` - id("com.android.library") version "7.4.0" apply false + id("com.android.library") version "8.2.0" apply false id("org.jetbrains.kotlin.multiplatform") version "1.8.0" apply false id("org.jetbrains.kotlin.android") version "1.8.0" apply false id("org.jetbrains.compose") version "1.3.0" apply false } ext { - extra["compileSdkVersion"] = 33 + extra["compileSdkVersion"] = 34 extra["minSdkVersion"] = 21 - extra["targetSdkVersion"] = 33 + extra["targetSdkVersion"] = 34 } allprojects { @@ -18,4 +18,4 @@ allprojects { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 5a66a00..3f72fbd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,15 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" android.useAndroidX=true -org.jetbrains.compose.experimental.jscanvas.enabled=true \ No newline at end of file +org.jetbrains.compose.experimental.jscanvas.enabled=true + +# Use of implicitly-created components in maven-publish. +# Starting with version 8.0, Android Gradle Plugin will no longer implicitly create +# components for the maven-publish plugin. You will have to adapt the publishing blocks +# to use the new API (and mark the project as migrated +# by adding android.disableAutomaticComponentCreation=true to the project's gradle.properties file). +# See: https://developer.android.com/build/publish-library +#android.disableAutomaticComponentCreation=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661..15de902 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 3fcb3f399ca9e7c897467fdde77754548c24c221 Mon Sep 17 00:00:00 2001 From: Brill Pappin Date: Fri, 2 Feb 2024 10:16:26 -0500 Subject: [PATCH 08/10] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 08be5ef..fab3403 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +**Note:** *gitflow will be used for this project. Make sure your PRs are against the develop branch.* + # Compose LazyList/Grid reorder [![Latest release](https://img.shields.io/github/v/release/aclassen/ComposeReorderable?color=brightgreen&label=latest%20release)](https://github.com/aclassen/ComposeReorderable/releases/latest) From c74b5b3939fc6c59bbb1c820e7b11c2c2385979e Mon Sep 17 00:00:00 2001 From: Brill Pappin Date: Fri, 2 Feb 2024 11:24:04 -0500 Subject: [PATCH 09/10] #8 fixed ui v spacing --- .../kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt index fb60e85..b1897e4 100644 --- a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt +++ b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderGrid.kt @@ -77,6 +77,7 @@ private fun VerticalStaggeredGrid( state = state.gridState, contentPadding = PaddingValues(horizontal = 8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalItemSpacing = 4.dp, modifier = modifier.reorderable(state), ) { items(items = vm.dogs, key = { it.key }) { item -> From 6b11b25bf658c56b714a788c779d4f8f4a3d3068 Mon Sep 17 00:00:00 2001 From: Brill Pappin Date: Mon, 5 Feb 2024 13:22:34 -0500 Subject: [PATCH 10/10] fixed conflicting changes --- .../ReorderableLazyStaggeredGridState.kt | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyStaggeredGridState.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyStaggeredGridState.kt index 933b3e6..88a922e 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyStaggeredGridState.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyStaggeredGridState.kt @@ -41,7 +41,7 @@ fun rememberReorderableLazyHorizontalStaggeredGridState( gridState: LazyStaggeredGridState = rememberLazyStaggeredGridState(), canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, - maxScrollPerFrame: Dp = 20.dp, + maxScrollPerFrame: Float = 20f, dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation(), ) = rememberReorderableLazyStaggeredGridState( onMove = onMove, @@ -60,7 +60,7 @@ fun rememberReorderableLazyVerticalStaggeredGridState( gridState: LazyStaggeredGridState = rememberLazyStaggeredGridState(), canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, - maxScrollPerFrame: Dp = 20.dp, + maxScrollPerFrame: Float = 20f, dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation(), ) = rememberReorderableLazyStaggeredGridState( onMove = onMove, @@ -79,22 +79,24 @@ fun rememberReorderableLazyStaggeredGridState( onMove: (ItemPosition, ItemPosition) -> Unit, gridState: LazyStaggeredGridState = rememberLazyStaggeredGridState(), canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragStart: ((startIndex: Int, x: Int, y: Int) -> (Unit))? = null, onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, - maxScrollPerFrame: Dp = 20.dp, + maxScrollPerFrame: Float = 20F, dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation(), orientation: Orientation ): ReorderableLazyStaggeredGridState { - val maxScroll = with(LocalDensity.current) { maxScrollPerFrame.toPx() } + val maxScroll = with(LocalDensity.current) { maxScrollPerFrame } val scope = rememberCoroutineScope() val state = remember(gridState) { ReorderableLazyStaggeredGridState( - gridState, - scope, - maxScroll, - onMove, - canDragOver, - onDragEnd, - dragCancelledAnimation, + gridState=gridState, + scope = scope, + maxScrollPerFrame = maxScrollPerFrame, + onMove = onMove, + onDragStart = onDragStart, + canDragOver = canDragOver, + onDragEnd = onDragEnd, + dragCancelledAnimation = dragCancelledAnimation, orientation = orientation ) } @@ -119,16 +121,18 @@ class ReorderableLazyStaggeredGridState( maxScrollPerFrame: Float, onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit), canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragStart: ((startIndex: Int, x: Int, y: Int) -> (Unit))? = null, onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation(), val orientation: Orientation ) : ReorderableState( - scope, - maxScrollPerFrame, - onMove, - canDragOver, - onDragEnd, - dragCancelledAnimation + scope = scope, + maxScrollPerFrame = maxScrollPerFrame, + onMove = onMove, + onDragStart = onDragStart, + canDragOver = canDragOver, + onDragEnd = onDragEnd, + dragCancelledAnimation = dragCancelledAnimation ) { override val isVerticalScroll: Boolean get() = orientation == Orientation.Vertical // XXX gridState.isVertical is not accessible