Update dependency sh.calvin.reorderable:reorderable to v2.4.0-alpha01

This commit is contained in:
FooIbar committed 2024-07-29 10:47:13 +08:00
1 parent 9673163e8f
commit 2531fa289b
9 files changed
+14 -1473

No files matched your search

+1 -1
View File
@@ -241,7 +241,7 @@ dependencies {
implementation(libs.aboutlibraries.compose.m3)
implementation(libs.accompanist.drawable.painter)
// implementation(libs.reorderable)
implementation(libs.reorderable)
implementation(platform(libs.arrow.stack))
implementation(libs.arrow.fx.coroutines)
@@ -356,7 +356,12 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
}
}
}
ReorderableItem(reorderableLabelState, id, enabled = editEnable, animated = animateItems) { isDragging ->
ReorderableItem(
reorderableLabelState,
id,
enabled = editEnable,
animateItemModifier = Modifier.thenIf(animateItems) { animateItem() },
) { isDragging ->
SwipeToDismissBox(
state = dismissState,
backgroundContent = {},
@@ -134,6 +134,7 @@ import com.hippo.ehviewer.ui.tools.rememberHapticFeedback
import com.hippo.ehviewer.ui.tools.rememberInVM
import com.hippo.ehviewer.ui.tools.rememberMutableStateInDataStore
import com.hippo.ehviewer.ui.tools.snackBarPadding
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.pickVisualMedia
import com.hippo.ehviewer.util.sha1
@@ -380,7 +381,11 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
) {
itemsIndexed(quickSearchList, key = { _, item -> item.id!! }) { itemIndex, item ->
val index by rememberUpdatedState(itemIndex)
ReorderableItem(reorderableLazyListState, item.id!!, animated = animateItems) { isDragging ->
ReorderableItem(
reorderableLazyListState,
item.id!!,
animateItemModifier = Modifier.thenIf(animateItems) { animateItem() },
) { isDragging ->
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density) }
LaunchedEffect(dismissState) {
@@ -1,809 +0,0 @@
/*
* Copyright 2023 Calvin Liang
*
* 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.
*/
@file:Suppress("ConstPropertyName")
package sh.calvin.reorderable
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
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.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
object ReorderableLazyCollectionDefaults {
val ScrollThreshold = 48.dp
}
internal const val ScrollAmountMultiplier = 0.05f
internal data class AbsolutePixelPadding(
val start: Float,
val end: Float,
val top: Float,
val bottom: Float,
)
internal interface LazyCollectionItemInfo<out T> {
val index: Int
val key: Any
val offset: IntOffset
val size: IntSize
val data: T
val center: IntOffset
get() = IntOffset(offset.x + size.width / 2, offset.y + size.height / 2)
}
internal data class CollectionScrollPadding(
val start: Float,
val end: Float,
) {
companion object {
val Zero = CollectionScrollPadding(0f, 0f)
fun fromAbsolutePixelPadding(
orientation: Orientation,
padding: AbsolutePixelPadding,
reverseLayout: Boolean,
): CollectionScrollPadding = when (orientation) {
Orientation.Vertical -> CollectionScrollPadding(
start = padding.top,
end = padding.bottom,
)
Orientation.Horizontal -> CollectionScrollPadding(
start = padding.start,
end = padding.end,
)
}.let {
when (reverseLayout) {
true -> CollectionScrollPadding(
start = it.end,
end = it.start,
)
false -> it
}
}
}
}
/**
* The offsets in the main axis from the start of the content to the
* start and end of the content minus the padding.
*/
internal data class ScrollAreaOffsets(
val start: Float,
val end: Float,
)
internal interface LazyCollectionLayoutInfo<out T> {
val visibleItemsInfo: List<LazyCollectionItemInfo<T>>
val viewportSize: IntSize
val orientation: Orientation
val reverseLayout: Boolean
val beforeContentPadding: Int
val mainAxisViewportSize: Int
get() = when (orientation) {
Orientation.Vertical -> viewportSize.height
Orientation.Horizontal -> viewportSize.width
}
fun getScrollAreaOffsets(
padding: AbsolutePixelPadding,
) = getScrollAreaOffsets(
CollectionScrollPadding.fromAbsolutePixelPadding(
orientation,
padding,
reverseLayout,
),
)
fun getScrollAreaOffsets(padding: CollectionScrollPadding): ScrollAreaOffsets {
val (startPadding, endPadding) = padding
val contentEndOffset = when (orientation) {
Orientation.Vertical -> viewportSize.height
Orientation.Horizontal -> viewportSize.width
} - endPadding
return ScrollAreaOffsets(
start = startPadding,
end = contentEndOffset,
)
}
/**
* get items that are fully inside the content area
*/
fun getItemsInContentArea(padding: AbsolutePixelPadding) = getItemsInContentArea(
CollectionScrollPadding.fromAbsolutePixelPadding(
orientation,
padding,
reverseLayout,
),
)
/**
* get items that are fully inside the content area
*/
fun getItemsInContentArea(padding: CollectionScrollPadding = CollectionScrollPadding.Zero): List<LazyCollectionItemInfo<T>> {
val (contentStartOffset, contentEndOffset) = getScrollAreaOffsets(
padding,
)
return when (orientation) {
Orientation.Vertical -> {
visibleItemsInfo.filter { item ->
item.offset.y >= contentStartOffset && item.offset.y + item.size.height <= contentEndOffset
}
}
Orientation.Horizontal -> {
visibleItemsInfo.filter { item ->
item.offset.x >= contentStartOffset && item.offset.x + item.size.width <= contentEndOffset
}
}
}
}
}
internal interface LazyCollectionState<out T> {
val firstVisibleItemIndex: Int
val firstVisibleItemScrollOffset: Int
val layoutInfo: LazyCollectionLayoutInfo<T>
suspend fun animateScrollBy(
value: Float,
animationSpec: AnimationSpec<Float> = spring(),
): Float
suspend fun scrollToItem(scrollToIndex: Int, firstVisibleItemScrollOffset: Int)
fun requestScrollToItem(index: Int, scrollOffset: Int = 0)
}
interface ReorderableLazyCollectionStateInterface {
val isAnyItemDragging: Boolean
}
// base on https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt;drc=edde6e8b9d304264598f962a3b0e5c267e1373bb
// and https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyGridDragAndDropDemo.kt;drc=44e4233f7bc0290a1509ef2d448f1309eb63248f
@Stable
open class ReorderableLazyCollectionState<out T> internal constructor(
private val state: LazyCollectionState<T>,
private val scope: CoroutineScope,
private val onMoveState: State<suspend CoroutineScope.(from: T, to: T) -> Unit>,
/**
* The threshold in pixels for scrolling the list when dragging an item.
* If the dragged item is within this threshold of the top or bottom of the list, the list will scroll.
* Must be greater than 0.
*/
private val scrollThreshold: Float,
private val scrollThresholdPadding: AbsolutePixelPadding,
private val scroller: Scroller,
private val layoutDirection: LayoutDirection,
/**
* Whether this is a LazyVerticalStaggeredGrid
*/
private val lazyVerticalStaggeredGridRtlFix: Boolean = false,
/**
* A function that determines whether the `draggingItem` should be moved with the `item`.
* Given their bounding rectangles, return `true` if they should be moved.
* The default implementation is to move if the dragging item's bounding rectangle has crossed the center of the item's bounding rectangle.
*/
private val shouldItemMove: (draggingItem: Rect, item: Rect) -> Boolean = { draggingItem, item ->
draggingItem.contains(item.center)
},
) : ReorderableLazyCollectionStateInterface {
private val onMoveStateMutex: Mutex = Mutex()
internal val orientation: Orientation
get() = state.layoutInfo.orientation
private var draggingItemKey by mutableStateOf<Any?>(null)
private val draggingItemIndex: Int?
get() = draggingItemLayoutInfo?.index
/**
* Whether any item is being dragged. This property is observable.
*/
override val isAnyItemDragging by derivedStateOf {
draggingItemKey != null
}
private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero)
private var draggingItemInitialOffset by mutableStateOf(IntOffset.Zero)
// visibleItemsInfo doesn't update immediately after onMove, draggingItemLayoutInfo.item may be outdated for a short time.
// not a clean solution, but it works.
private var draggingItemTargetIndex by mutableStateOf<Int?>(null)
private var predictedDraggingItemOffset by mutableStateOf<IntOffset?>(null)
private val draggingItemLayoutInfo: LazyCollectionItemInfo<T>?
get() = draggingItemKey?.let { draggingItemKey ->
state.layoutInfo.visibleItemsInfo.firstOrNull { it.key == draggingItemKey }
}
internal val draggingItemOffset: Offset
get() = (
draggingItemLayoutInfo?.let {
val offset =
if (it.index == draggingItemTargetIndex || draggingItemTargetIndex == null) {
draggingItemTargetIndex = null
predictedDraggingItemOffset = null
it.offset
} else {
predictedDraggingItemOffset ?: it.offset
}
draggingItemDraggedDelta +
(draggingItemInitialOffset.toOffset() - offset.toOffset())
.reverseAxisIfNecessary()
.reverseAxisWithLayoutDirectionIfLazyVerticalStaggeredGridRtlFix()
}
) ?: Offset.Zero
// the offset of the handle center from the top left of the dragging item when dragging starts
private var draggingItemHandleOffset = Offset.Zero
internal val reorderableKeys = HashSet<Any?>()
internal var previousDraggingItemKey by mutableStateOf<Any?>(null)
private set
internal var previousDraggingItemOffset = Animatable(Offset.Zero, Offset.VectorConverter)
private set
private fun Offset.reverseAxisWithReverseLayoutIfNecessary() =
when (state.layoutInfo.reverseLayout) {
true -> reverseAxis(orientation)
false -> this
}
private fun Offset.reverseAxisWithLayoutDirectionIfNecessary() = when (orientation) {
Orientation.Vertical -> this
Orientation.Horizontal -> reverseAxisWithLayoutDirection()
}
private fun Offset.reverseAxisWithLayoutDirection() = when (layoutDirection) {
LayoutDirection.Ltr -> this
LayoutDirection.Rtl -> reverseAxis(Orientation.Horizontal)
}
private fun Offset.reverseAxisWithLayoutDirectionIfLazyVerticalStaggeredGridRtlFix() =
when (layoutDirection) {
LayoutDirection.Ltr -> this
LayoutDirection.Rtl -> if (lazyVerticalStaggeredGridRtlFix && orientation == Orientation.Vertical) {
reverseAxis(Orientation.Horizontal)
} else {
this
}
}
private fun Offset.reverseAxisIfNecessary() =
this.reverseAxisWithReverseLayoutIfNecessary()
.reverseAxisWithLayoutDirectionIfNecessary()
private fun Offset.mainAxis() = getAxis(orientation)
private fun IntOffset.mainAxis() = getAxis(orientation)
internal suspend fun onDragStart(key: Any, handleOffset: Offset) {
state.layoutInfo.visibleItemsInfo.firstOrNull { item ->
item.key == key
}?.also {
val mainAxisOffset = it.offset.mainAxis()
if (mainAxisOffset < 0) {
// if item is not fully in view, scroll to it
state.animateScrollBy(mainAxisOffset.toFloat(), spring())
}
draggingItemKey = key
draggingItemInitialOffset = it.offset
draggingItemHandleOffset = handleOffset
}
}
internal fun onDragStop() {
val previousDraggingItemInitialOffset = draggingItemLayoutInfo?.offset
if (draggingItemIndex != null) {
previousDraggingItemKey = draggingItemKey
val startOffset = draggingItemOffset
scope.launch {
previousDraggingItemOffset.snapTo(startOffset)
previousDraggingItemOffset.animateTo(
Offset.Zero,
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Offset.VisibilityThreshold,
),
)
previousDraggingItemKey = null
}
}
draggingItemDraggedDelta = Offset.Zero
draggingItemKey = null
draggingItemInitialOffset = previousDraggingItemInitialOffset ?: IntOffset.Zero
scroller.tryStop()
draggingItemTargetIndex = null
predictedDraggingItemOffset = null
}
internal fun onDrag(offset: Offset) {
draggingItemDraggedDelta += offset
val draggingItem = draggingItemLayoutInfo ?: return
// how far the dragging item is from the original position
val dragOffset = draggingItemOffset.reverseAxisIfNecessary()
.reverseAxisWithLayoutDirectionIfLazyVerticalStaggeredGridRtlFix()
val startOffset = draggingItem.offset.toOffset() + dragOffset
val endOffset = startOffset + draggingItem.size.toSize()
val (contentStartOffset, contentEndOffset) = state.layoutInfo.getScrollAreaOffsets(
scrollThresholdPadding,
)
// the distance from the top or left of the list to the center of the dragging item handle
val handleOffset =
when (
state.layoutInfo.reverseLayout ||
(
layoutDirection == LayoutDirection.Rtl &&
orientation == Orientation.Horizontal
)
) {
true -> endOffset - draggingItemHandleOffset
false -> startOffset + draggingItemHandleOffset
} + IntOffset.fromAxis(
orientation,
state.layoutInfo.beforeContentPadding,
).toOffset()
// check if the handle center is in the scroll threshold
val distanceFromStart = (handleOffset.getAxis(orientation) - contentStartOffset)
.coerceAtLeast(0f)
val distanceFromEnd = (contentEndOffset - handleOffset.getAxis(orientation))
.coerceAtLeast(0f)
val isScrollingStarted = if (distanceFromStart < scrollThreshold) {
scroller.start(
Scroller.Direction.BACKWARD,
getScrollSpeedMultiplier(distanceFromStart),
maxScrollDistanceProvider = {
// distance from the start of the dragging item's stationary position to the end of the list
(
draggingItemLayoutInfo?.let {
state.layoutInfo.mainAxisViewportSize -
it.offset.toOffset().getAxis(orientation) - 1f
}
) ?: 0f
},
onScroll = {
moveDraggingItemToEnd(Scroller.Direction.BACKWARD)
},
)
} else if (distanceFromEnd < scrollThreshold) {
scroller.start(
Scroller.Direction.FORWARD,
getScrollSpeedMultiplier(distanceFromEnd),
maxScrollDistanceProvider = {
// distance from the end of the dragging item's stationary position to the start of the list
(
draggingItemLayoutInfo?.let {
var itemToAlmostScrollOff = it
var scrollDistance =
itemToAlmostScrollOff.offset.toOffset().getAxis(orientation) +
itemToAlmostScrollOff.size.getAxis(orientation) - 1f
if (scrollDistance <= 0f) {
itemToAlmostScrollOff = it
scrollDistance =
itemToAlmostScrollOff.offset.toOffset().getAxis(orientation) +
itemToAlmostScrollOff.size.getAxis(orientation) - 1f
}
scrollDistance
}
) ?: 0f
},
onScroll = {
moveDraggingItemToEnd(Scroller.Direction.FORWARD)
},
)
} else {
scroller.tryStop()
false
}
if (!onMoveStateMutex.tryLock()) return
if (!scroller.isScrolling && !isScrollingStarted) {
val draggingItemRect = Rect(startOffset, endOffset)
// find a target item to move with
val targetItem = findTargetItem(
draggingItemRect,
items = state.layoutInfo.visibleItemsInfo,
) {
it.index != draggingItem.index
}
if (targetItem != null) {
scope.launch {
moveItems(draggingItem, targetItem)
}
}
}
onMoveStateMutex.unlock()
}
// keep dragging item in visible area to prevent it from disappearing
private suspend fun moveDraggingItemToEnd(
direction: Scroller.Direction,
) {
// wait for the current moveItems to finish
onMoveStateMutex.lock()
val draggingItem = draggingItemLayoutInfo
if (draggingItem == null) {
onMoveStateMutex.unlock()
return
}
val isDraggingItemAtEnd = when (direction) {
Scroller.Direction.FORWARD -> draggingItem.index == state.layoutInfo.visibleItemsInfo.lastOrNull()?.index
Scroller.Direction.BACKWARD -> draggingItem.index == state.firstVisibleItemIndex
}
if (isDraggingItemAtEnd) {
onMoveStateMutex.unlock()
return
}
val dragOffset = draggingItemOffset.reverseAxisIfNecessary()
.reverseAxisWithLayoutDirectionIfLazyVerticalStaggeredGridRtlFix()
val startOffset = draggingItem.offset.toOffset() + dragOffset
val endOffset = startOffset + draggingItem.size.toSize()
val draggingItemRect = Rect(startOffset, endOffset).maxOutAxis(orientation)
val targetItem = findTargetItem(
draggingItemRect,
items = state.layoutInfo.getItemsInContentArea(scrollThresholdPadding),
direction.opposite,
) ?: state.layoutInfo.getItemsInContentArea(
scrollThresholdPadding,
).let {
val targetItemFunc = { item: LazyCollectionItemInfo<T> ->
item.key in reorderableKeys && item.index != state.firstVisibleItemIndex
}
when (direction) {
Scroller.Direction.FORWARD -> it.findLast(targetItemFunc)
Scroller.Direction.BACKWARD -> it.find(targetItemFunc)
}
}
val job = scope.launch {
if (targetItem != null) {
moveItems(draggingItem, targetItem)
}
}
onMoveStateMutex.unlock()
job.join()
}
private fun Rect.maxOutAxis(orientation: Orientation): Rect = when (orientation) {
Orientation.Vertical -> copy(
top = Float.NEGATIVE_INFINITY,
bottom = Float.POSITIVE_INFINITY,
)
Orientation.Horizontal -> copy(
left = Float.NEGATIVE_INFINITY,
right = Float.POSITIVE_INFINITY,
)
}
private fun findTargetItem(
draggingItemRect: Rect,
items: List<LazyCollectionItemInfo<T>> = state.layoutInfo.getItemsInContentArea(),
direction: Scroller.Direction = Scroller.Direction.FORWARD,
additionalPredicate: (LazyCollectionItemInfo<T>) -> Boolean = { true },
): LazyCollectionItemInfo<T>? {
val targetItemFunc = { item: LazyCollectionItemInfo<T> ->
val targetItemRect = Rect(item.offset.toOffset(), item.size.toSize())
shouldItemMove(draggingItemRect, targetItemRect) &&
item.key in reorderableKeys &&
additionalPredicate(item)
}
val targetItem = when (direction) {
Scroller.Direction.FORWARD -> items.find(targetItemFunc)
Scroller.Direction.BACKWARD -> items.findLast(targetItemFunc)
}
return targetItem
}
private val layoutInfoFlow = snapshotFlow { state.layoutInfo }
companion object {
const val MoveItemsLayoutInfoUpdateMaxWaitDuration = 1000L
}
private suspend fun moveItems(
draggingItem: LazyCollectionItemInfo<T>,
targetItem: LazyCollectionItemInfo<T>,
) {
if (draggingItem.index == targetItem.index) return
if (
draggingItem.index == state.firstVisibleItemIndex ||
targetItem.index == state.firstVisibleItemIndex
) {
state.requestScrollToItem(
state.firstVisibleItemIndex,
state.firstVisibleItemScrollOffset,
)
}
try {
onMoveStateMutex.withLock {
scope.(onMoveState.value)(draggingItem.data, targetItem.data)
predictedDraggingItemOffset = if (targetItem.index > draggingItem.index) {
(targetItem.offset + targetItem.size) - draggingItem.size
} else {
targetItem.offset
}
draggingItemTargetIndex = targetItem.index
withTimeout(MoveItemsLayoutInfoUpdateMaxWaitDuration) {
// the first result from layoutInfoFlow is the current layoutInfo
// the second result is the updated layoutInfo
layoutInfoFlow.take(2).collect()
}
}
} catch (e: CancellationException) {
// do nothing
}
}
internal fun isItemDragging(key: Any): State<Boolean> = derivedStateOf {
key == draggingItemKey
}
private fun getScrollSpeedMultiplier(distance: Float): Float {
// map distance in scrollThreshold..-scrollThreshold to 1..10
return (
1 - ((distance + scrollThreshold) / (scrollThreshold * 2)).coerceIn(
0f,
1f,
)
) * 10
}
}
interface ReorderableCollectionItemScope {
/**
* Make the UI element the draggable handle for the reorderable item.
*
* This modifier can only be used on the UI element that is a child of [ReorderableItem].
*
* @param enabled Whether or not drag is enabled
* @param interactionSource [MutableInteractionSource] that will be used to emit [DragInteraction.Start] when this draggable is being dragged.
* @param onDragStarted The function that is called when the item starts being dragged
* @param onDragStopped The function that is called when the item stops being dragged
*/
fun Modifier.draggableHandle(
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
onDragStarted: (startedPosition: Offset) -> Unit = {},
onDragStopped: () -> Unit = {},
): Modifier
/**
* Make the UI element the draggable handle for the reorderable item. Drag will start only after a long press.
*
* This modifier can only be used on the UI element that is a child of [ReorderableItem].
*
* @param enabled Whether or not drag is enabled
* @param interactionSource [MutableInteractionSource] that will be used to emit [DragInteraction.Start] when this draggable is being dragged.
* @param onDragStarted The function that is called when the item starts being dragged
* @param onDragStopped The function that is called when the item stops being dragged
*/
fun Modifier.longPressDraggableHandle(
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
onDragStarted: (startedPosition: Offset) -> Unit = {},
onDragStopped: () -> Unit = {},
): Modifier
}
internal class ReorderableCollectionItemScopeImpl(
private val reorderableLazyCollectionState: ReorderableLazyCollectionState<*>,
private val key: Any,
private val itemPositionProvider: () -> Offset,
) : ReorderableCollectionItemScope {
/**
* Make the UI element the draggable handle for the reorderable item.
*
* @param enabled Whether or not drag is enabled
* @param interactionSource [MutableInteractionSource] that will be used to emit [DragInteraction.Start] when this draggable is being dragged.
* @param onDragStarted The function that is called when the item starts being dragged
* @param onDragStopped The function that is called when the item stops being dragged
*/
override fun Modifier.draggableHandle(
enabled: Boolean,
interactionSource: MutableInteractionSource?,
onDragStarted: (startedPosition: Offset) -> Unit,
onDragStopped: () -> Unit,
) = composed {
var handleOffset by remember { mutableStateOf(Offset.Zero) }
var handleSize by remember { mutableStateOf(IntSize.Zero) }
val coroutineScope = rememberCoroutineScope()
onGloballyPositioned {
handleOffset = it.positionInRoot()
handleSize = it.size
}.draggable(
key1 = reorderableLazyCollectionState,
enabled = enabled && (reorderableLazyCollectionState.isItemDragging(key).value || !reorderableLazyCollectionState.isAnyItemDragging),
interactionSource = interactionSource,
onDragStarted = {
coroutineScope.launch {
val handleOffsetRelativeToItem = handleOffset - itemPositionProvider()
val handleCenter = Offset(
handleOffsetRelativeToItem.x + handleSize.width / 2f,
handleOffsetRelativeToItem.y + handleSize.height / 2f,
)
reorderableLazyCollectionState.onDragStart(key, handleCenter)
}
onDragStarted(it)
},
onDragStopped = {
reorderableLazyCollectionState.onDragStop()
onDragStopped()
},
onDrag = { change, dragAmount ->
change.consume()
reorderableLazyCollectionState.onDrag(dragAmount)
},
)
}
/**
* Make the UI element the draggable handle for the reorderable item. Drag will start only after a long press.
*
* @param enabled Whether or not drag is enabled
* @param interactionSource [MutableInteractionSource] that will be used to emit [DragInteraction.Start] when this draggable is being dragged
* @param onDragStarted The function that is called when the item starts being dragged
* @param onDragStopped The function that is called when the item stops being dragged
*/
override fun Modifier.longPressDraggableHandle(
enabled: Boolean,
interactionSource: MutableInteractionSource?,
onDragStarted: (startedPosition: Offset) -> Unit,
onDragStopped: () -> Unit,
) = composed {
var handleOffset by remember { mutableStateOf(Offset.Zero) }
var handleSize by remember { mutableStateOf(IntSize.Zero) }
val coroutineScope = rememberCoroutineScope()
onGloballyPositioned {
handleOffset = it.positionInRoot()
handleSize = it.size
}.longPressDraggable(
key1 = reorderableLazyCollectionState,
enabled = enabled && (reorderableLazyCollectionState.isItemDragging(key).value || !reorderableLazyCollectionState.isAnyItemDragging),
interactionSource = interactionSource,
onDragStarted = {
coroutineScope.launch {
val handleOffsetRelativeToItem = handleOffset - itemPositionProvider()
val handleCenter = Offset(
handleOffsetRelativeToItem.x + handleSize.width / 2f,
handleOffsetRelativeToItem.y + handleSize.height / 2f,
)
reorderableLazyCollectionState.onDragStart(key, handleCenter)
}
onDragStarted(it)
},
onDragStopped = {
reorderableLazyCollectionState.onDragStop()
onDragStopped()
},
onDrag = { change, dragAmount ->
change.consume()
reorderableLazyCollectionState.onDrag(dragAmount)
},
)
}
}
/**
* A composable that allows items to be reordered by dragging.
*
* @param state The return value of [rememberReorderableLazyListState]
* @param key The key of the item, must be the same as the key passed to the parent composable
* @param enabled Whether or this item is reorderable. If true, the item will not move for other items but may still be draggable. To make an item not draggable, set `enable = false` in [Modifier.draggable] or [Modifier.longPressDraggable] instead.
* @param dragging Whether or not this item is currently being dragged
*/
@ExperimentalFoundationApi
@Composable
internal fun ReorderableCollectionItem(
state: ReorderableLazyCollectionState<*>,
key: Any,
modifier: Modifier = Modifier,
enabled: Boolean = true,
dragging: Boolean,
content: @Composable ReorderableCollectionItemScope.(isDragging: Boolean) -> Unit,
) {
var itemPosition by remember { mutableStateOf(Offset.Zero) }
Box(
modifier.onGloballyPositioned {
itemPosition = it.positionInRoot()
},
) {
ReorderableCollectionItemScopeImpl(
reorderableLazyCollectionState = state,
key = key,
itemPositionProvider = { itemPosition },
).content(dragging)
}
LaunchedEffect(state.reorderableKeys, enabled) {
if (enabled) {
state.reorderableKeys.add(key)
} else {
state.reorderableKeys.remove(key)
}
}
}
@@ -1,259 +0,0 @@
/*
* Copyright 2023 Calvin Liang
*
* 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 sh.calvin.reorderable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListLayoutInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import kotlinx.coroutines.CoroutineScope
/**
* Creates a [ReorderableLazyListState] that is remembered across compositions.
*
* Changes to [lazyListState], [scrollThresholdPadding], [scrollThreshold], and [scroller] will result in [ReorderableLazyListState] being updated.
*
* @param lazyListState The return value of [rememberLazyListState](androidx.compose.foundation.lazy.LazyListStateKt.rememberLazyListState)
* @param scrollThresholdPadding The padding that will be added to the top and bottom, or start and end of the list to determine the scrollThreshold. Useful for when the grid is displayed under the navigation bar or notification bar.
* @param scrollThreshold The distance in dp from the top and bottom, or start and end of the list that will trigger scrolling
* @param scroller The [Scroller] that will be used to scroll the list. Use [rememberScroller](sh.calvin.reorderable.ScrollerKt.rememberScroller) to create a [Scroller].
* @param onMove The function that is called when an item is moved. Make sure this function returns only after the items are moved. This suspend function is invoked with the `rememberReorderableLazyListState` scope, allowing for async processing, if desired. Note that the scope used here is the one provided by the composition where `rememberReorderableLazyListState` is called, for long running work that needs to outlast `rememberReorderableLazyListState` being in the composition you should use a scope that fits the lifecycle needed.
*/
@Composable
fun rememberReorderableLazyListState(
lazyListState: LazyListState,
scrollThresholdPadding: PaddingValues = PaddingValues(0.dp),
scrollThreshold: Dp = ReorderableLazyCollectionDefaults.ScrollThreshold,
scroller: Scroller = rememberScroller(
scrollableState = lazyListState,
pixelAmountProvider = { lazyListState.layoutInfo.mainAxisViewportSize * ScrollAmountMultiplier },
),
onMove: suspend CoroutineScope.(from: LazyListItemInfo, to: LazyListItemInfo) -> Unit,
): ReorderableLazyListState {
val density = LocalDensity.current
val scrollThresholdPx = with(density) { scrollThreshold.toPx() }
val scope = rememberCoroutineScope()
val onMoveState = rememberUpdatedState(onMove)
val layoutDirection = LocalLayoutDirection.current
val absoluteScrollThresholdPadding = AbsolutePixelPadding(
start = with(density) {
scrollThresholdPadding.calculateStartPadding(layoutDirection).toPx()
},
end = with(density) {
scrollThresholdPadding.calculateEndPadding(layoutDirection).toPx()
},
top = with(density) { scrollThresholdPadding.calculateTopPadding().toPx() },
bottom = with(density) { scrollThresholdPadding.calculateBottomPadding().toPx() },
)
val orientation by remember { derivedStateOf { lazyListState.layoutInfo.orientation } }
val state = remember(
scope,
lazyListState,
scrollThreshold,
scrollThresholdPadding,
scroller,
orientation,
) {
ReorderableLazyListState(
state = lazyListState,
scope = scope,
onMoveState = onMoveState,
scrollThreshold = scrollThresholdPx,
scrollThresholdPadding = absoluteScrollThresholdPadding,
scroller = scroller,
layoutDirection = layoutDirection,
shouldItemMove = when (orientation) {
Orientation.Vertical -> { draggingItem, item ->
item.center.y in draggingItem.top..<draggingItem.bottom
}
Orientation.Horizontal -> { draggingItem, item ->
item.center.x in draggingItem.left..<draggingItem.right
}
},
)
}
return state
}
private val LazyListLayoutInfo.mainAxisViewportSize: Int
get() = when (orientation) {
Orientation.Vertical -> viewportSize.height
Orientation.Horizontal -> viewportSize.width
}
private fun LazyListItemInfo.toLazyCollectionItemInfo(orientation: Orientation) =
object : LazyCollectionItemInfo<LazyListItemInfo> {
override val index: Int
get() = this@toLazyCollectionItemInfo.index
override val key: Any
get() = this@toLazyCollectionItemInfo.key
override val offset: IntOffset
get() = IntOffset.fromAxis(orientation, this@toLazyCollectionItemInfo.offset)
override val size: IntSize
get() = IntSize.fromAxis(orientation, this@toLazyCollectionItemInfo.size)
override val data: LazyListItemInfo
get() = this@toLazyCollectionItemInfo
}
private fun LazyListLayoutInfo.toLazyCollectionLayoutInfo() =
object : LazyCollectionLayoutInfo<LazyListItemInfo> {
override val visibleItemsInfo: List<LazyCollectionItemInfo<LazyListItemInfo>>
get() = this@toLazyCollectionLayoutInfo.visibleItemsInfo.map {
it.toLazyCollectionItemInfo(orientation)
}
override val viewportSize: IntSize
get() = this@toLazyCollectionLayoutInfo.viewportSize
override val orientation: Orientation
get() = this@toLazyCollectionLayoutInfo.orientation
override val reverseLayout: Boolean
get() = this@toLazyCollectionLayoutInfo.reverseLayout
override val beforeContentPadding: Int
get() = this@toLazyCollectionLayoutInfo.beforeContentPadding
}
private fun LazyListState.toLazyCollectionState() =
object : LazyCollectionState<LazyListItemInfo> {
override val firstVisibleItemIndex: Int
get() = this@toLazyCollectionState.firstVisibleItemIndex
override val firstVisibleItemScrollOffset: Int
get() = this@toLazyCollectionState.firstVisibleItemScrollOffset
override val layoutInfo: LazyCollectionLayoutInfo<LazyListItemInfo>
get() = this@toLazyCollectionState.layoutInfo.toLazyCollectionLayoutInfo()
override suspend fun animateScrollBy(value: Float, animationSpec: AnimationSpec<Float>) =
this@toLazyCollectionState.animateScrollBy(value, animationSpec)
override suspend fun scrollToItem(scrollToIndex: Int, firstVisibleItemScrollOffset: Int) =
this@toLazyCollectionState.scrollToItem(scrollToIndex, firstVisibleItemScrollOffset)
override fun requestScrollToItem(index: Int, scrollOffset: Int) =
this@toLazyCollectionState.requestScrollToItem(index, scrollOffset)
}
class ReorderableLazyListState internal constructor(
state: LazyListState,
scope: CoroutineScope,
onMoveState: State<suspend CoroutineScope.(from: LazyListItemInfo, to: LazyListItemInfo) -> Unit>,
/**
* The threshold in pixels for scrolling the list when dragging an item.
* If the dragged item is within this threshold of the top or bottom of the list, the list will scroll.
* Must be greater than 0.
*/
scrollThreshold: Float,
scrollThresholdPadding: AbsolutePixelPadding,
scroller: Scroller,
layoutDirection: LayoutDirection,
shouldItemMove: (draggingItem: Rect, item: Rect) -> Boolean,
) : ReorderableLazyCollectionState<LazyListItemInfo>(
state.toLazyCollectionState(),
scope,
onMoveState,
scrollThreshold,
scrollThresholdPadding,
scroller,
layoutDirection,
shouldItemMove = shouldItemMove,
)
/**
* A composable that allows an item in LazyColumn or LazyRow to be reordered by dragging.
*
* @param state The return value of [rememberReorderableLazyListState]
* @param key The key of the item, must be the same as the key passed to [LazyListScope.item](androidx.compose.foundation.lazy.item), [LazyListScope.items](androidx.compose.foundation.lazy.items) or similar functions in [LazyListScope](androidx.compose.foundation.lazy.LazyListScope)
* @param enabled Whether or this item is reorderable. If true, the item will not move for other items but may still be draggable. To make an item not draggable, set `enable = false` in [Modifier.draggable] or [Modifier.longPressDraggable] instead.
*/
@ExperimentalFoundationApi
@Composable
fun LazyItemScope.ReorderableItem(
state: ReorderableLazyListState,
key: Any,
modifier: Modifier = Modifier,
enabled: Boolean = true,
animated: Boolean = true,
content: @Composable ReorderableCollectionItemScope.(isDragging: Boolean) -> Unit,
) {
val orientation = state.orientation
val dragging by state.isItemDragging(key)
val offsetModifier = if (dragging) {
Modifier
.zIndex(1f)
.then(
when (orientation) {
Orientation.Vertical -> Modifier.graphicsLayer {
translationY = state.draggingItemOffset.y
}
Orientation.Horizontal -> Modifier.graphicsLayer {
translationX = state.draggingItemOffset.x
}
},
)
} else if (key == state.previousDraggingItemKey) {
Modifier
.zIndex(1f)
.then(
when (orientation) {
Orientation.Vertical -> Modifier.graphicsLayer {
translationY = state.previousDraggingItemOffset.value.y
}
Orientation.Horizontal -> Modifier.graphicsLayer {
translationX = state.previousDraggingItemOffset.value.x
}
},
)
} else {
if (animated) Modifier.animateItem() else Modifier
}
ReorderableCollectionItem(
state = state,
key = key,
modifier = modifier.then(offsetModifier),
enabled = enabled,
dragging = dragging,
content = content,
)
}
@@ -1,183 +0,0 @@
/*
* Copyright 2023 Calvin Liang
*
* 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.
*/
@file:Suppress("ConstPropertyName")
package sh.calvin.reorderable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* A utility to programmatically scroll a [ScrollableState].
*
* @param scrollableState The [ScrollableState] to scroll.
* @param pixelAmountProvider A function that returns the amount of pixels to scroll per duration.
* @param duration The duration of each scroll in milliseconds.
*/
@Composable
fun rememberScroller(
scrollableState: ScrollableState,
pixelAmountProvider: () -> Float,
duration: Long = 100,
): Scroller {
val scope = rememberCoroutineScope()
val pixelAmountProviderUpdated = rememberUpdatedState(pixelAmountProvider)
val durationUpdated = rememberUpdatedState(duration)
return remember(scrollableState, scope, duration) {
Scroller(
scrollableState,
scope,
pixelPerSecondProvider = {
pixelAmountProviderUpdated.value() / (durationUpdated.value / 1000f)
},
)
}
}
@Stable
class Scroller internal constructor(
private val scrollableState: ScrollableState,
private val scope: CoroutineScope,
private val pixelPerSecondProvider: () -> Float,
) {
companion object {
// The maximum duration for a scroll animation in milliseconds.
private const val MaxScrollDuration = 100L
private const val ZeroScrollWaitDuration = 100L
}
internal enum class Direction {
BACKWARD,
FORWARD,
;
val opposite: Direction
get() = when (this) {
BACKWARD -> FORWARD
FORWARD -> BACKWARD
}
}
private data class ScrollInfo(
val direction: Direction,
val speedMultiplier: Float,
val maxScrollDistanceProvider: () -> Float,
val onScroll: suspend () -> Unit,
) {
companion object {
val Null = ScrollInfo(Direction.FORWARD, 0f, { 0f }, {})
}
}
private var programmaticScrollJob: Job? = null
val isScrolling: Boolean
get() = programmaticScrollJob?.isActive == true
private val scrollInfoChannel = Channel<ScrollInfo>(Channel.CONFLATED)
internal fun start(
direction: Direction,
speedMultiplier: Float = 1f,
maxScrollDistanceProvider: () -> Float = { Float.MAX_VALUE },
onScroll: suspend () -> Unit = {},
): Boolean {
if (!canScroll(direction)) return false
if (programmaticScrollJob == null) {
programmaticScrollJob = scope.launch {
scrollLoop()
}
}
val scrollInfo =
ScrollInfo(direction, speedMultiplier, maxScrollDistanceProvider, onScroll)
scrollInfoChannel.trySend(scrollInfo)
return true
}
private suspend fun scrollLoop() {
var scrollInfo: ScrollInfo? = null
while (true) {
scrollInfo = scrollInfoChannel.tryReceive().getOrNull() ?: scrollInfo
if (scrollInfo == null || scrollInfo == ScrollInfo.Null) break
val (direction, speedMultiplier, maxScrollDistanceProvider, onScroll) = scrollInfo
val pixelPerSecond = pixelPerSecondProvider() * speedMultiplier
val pixelPerMs = pixelPerSecond / 1000f
onScroll()
if (!canScroll(direction)) break
val maxScrollDistance = maxScrollDistanceProvider()
if (maxScrollDistance <= 0f) {
delay(ZeroScrollWaitDuration)
continue
}
val maxScrollDistanceDuration = maxScrollDistance / pixelPerMs
val duration =
maxScrollDistanceDuration.toLong().coerceIn(1L, MaxScrollDuration)
val scrollDistance =
maxScrollDistance * (duration / maxScrollDistanceDuration)
val diff = scrollDistance.let {
when (direction) {
Direction.BACKWARD -> -it
Direction.FORWARD -> it
}
}
scrollableState.animateScrollBy(
diff,
tween(durationMillis = duration.toInt(), easing = LinearEasing),
)
}
}
private fun canScroll(direction: Direction): Boolean = when (direction) {
Direction.BACKWARD -> scrollableState.canScrollBackward
Direction.FORWARD -> scrollableState.canScrollForward
}
internal suspend fun stop() {
scrollInfoChannel.send(ScrollInfo.Null)
programmaticScrollJob?.cancelAndJoin()
programmaticScrollJob = null
}
internal fun tryStop() {
scope.launch {
stop()
}
}
}
@@ -1,170 +0,0 @@
@file:Suppress("ktlint:standard:filename")
package sh.calvin.reorderable
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.DisposableEffect
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.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import kotlinx.coroutines.launch
internal fun Modifier.draggable(
key1: Any?,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
onDragStarted: (Offset) -> Unit = { },
onDragStopped: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,
) = composed {
val coroutineScope = rememberCoroutineScope()
var dragInteractionStart by remember { mutableStateOf<DragInteraction.Start?>(null) }
var dragStarted by remember { mutableStateOf(false) }
DisposableEffect(key1) {
onDispose {
if (dragStarted) {
dragInteractionStart?.also {
coroutineScope.launch {
interactionSource?.emit(DragInteraction.Cancel(it))
}
}
if (dragStarted) {
onDragStopped()
}
dragStarted = false
}
}
}
pointerInput(key1, enabled) {
if (enabled) {
detectDragGestures(
onDragStart = {
dragStarted = true
dragInteractionStart = DragInteraction.Start().also {
coroutineScope.launch {
interactionSource?.emit(it)
}
}
onDragStarted(it)
},
onDragEnd = {
dragInteractionStart?.also {
coroutineScope.launch {
interactionSource?.emit(DragInteraction.Stop(it))
}
}
if (dragStarted) {
onDragStopped()
}
dragStarted = false
},
onDragCancel = {
dragInteractionStart?.also {
coroutineScope.launch {
interactionSource?.emit(DragInteraction.Cancel(it))
}
}
if (dragStarted) {
onDragStopped()
}
dragStarted = false
},
onDrag = onDrag,
)
}
}
}
internal fun Modifier.longPressDraggable(
key1: Any?,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
onDragStarted: (Offset) -> Unit = { },
onDragStopped: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,
) = composed {
val coroutineScope = rememberCoroutineScope()
var dragInteractionStart by remember { mutableStateOf<DragInteraction.Start?>(null) }
var dragStarted by remember { mutableStateOf(false) }
DisposableEffect(key1) {
onDispose {
if (dragStarted) {
dragInteractionStart?.also {
coroutineScope.launch {
interactionSource?.emit(DragInteraction.Cancel(it))
}
}
if (dragStarted) {
onDragStopped()
}
dragStarted = false
}
}
}
pointerInput(key1, enabled) {
if (enabled) {
detectDragGesturesAfterLongPress(
onDragStart = {
dragStarted = true
dragInteractionStart = DragInteraction.Start().also {
coroutineScope.launch {
interactionSource?.emit(it)
}
}
onDragStarted(it)
},
onDragEnd = {
dragInteractionStart?.also {
coroutineScope.launch {
interactionSource?.emit(DragInteraction.Stop(it))
}
}
if (dragStarted) {
onDragStopped()
}
dragStarted = false
},
onDragCancel = {
dragInteractionStart?.also {
coroutineScope.launch {
interactionSource?.emit(DragInteraction.Cancel(it))
}
}
if (dragStarted) {
onDragStopped()
}
dragStarted = false
},
onDrag = onDrag,
)
}
}
}
@@ -1,48 +0,0 @@
@file:Suppress("ktlint:standard:filename")
package sh.calvin.reorderable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
internal fun Offset.getAxis(orientation: Orientation) = when (orientation) {
Orientation.Vertical -> y
Orientation.Horizontal -> x
}
internal fun IntOffset.getAxis(orientation: Orientation) = when (orientation) {
Orientation.Vertical -> y
Orientation.Horizontal -> x
}
internal fun IntSize.getAxis(orientation: Orientation) = when (orientation) {
Orientation.Vertical -> height
Orientation.Horizontal -> width
}
internal fun Offset.reverseAxis(orientation: Orientation) =
when (orientation) {
Orientation.Vertical -> Offset(x, -y)
Orientation.Horizontal -> Offset(-x, y)
}
internal fun IntOffset.Companion.fromAxis(orientation: Orientation, value: Int) =
when (orientation) {
Orientation.Vertical -> IntOffset(0, value)
Orientation.Horizontal -> IntOffset(value, 0)
}
internal fun IntSize.Companion.fromAxis(orientation: Orientation, value: Int) =
when (orientation) {
Orientation.Vertical -> IntSize(0, value)
Orientation.Horizontal -> IntSize(value, 0)
}
internal operator fun Offset.plus(size: Size) = Offset(x + size.width, y + size.height)
internal operator fun Offset.minus(size: Size) = Offset(x - size.width, y - size.height)
internal operator fun IntOffset.plus(size: IntSize) = IntOffset(x + size.width, y + size.height)
internal operator fun IntOffset.minus(size: IntSize) = IntOffset(x - size.width, y - size.height)
+1 -1
View File
@@ -99,7 +99,7 @@ logcat = "com.squareup.logcat:logcat:0.1"
okio-jvm = { module = "com.squareup.okio:okio-jvm", version.ref = "okio" }
reorderable = "sh.calvin.reorderable:reorderable:2.3.0"
reorderable = "sh.calvin.reorderable:reorderable:2.4.0-alpha01"
serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinx-serialization" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }