Improve compose-reordering (#176)
This commit is contained in:
@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
@@ -35,10 +34,11 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
|
import it.vfsfitvnm.reordering.ReorderingLazyColumn
|
||||||
import it.vfsfitvnm.reordering.animateItemPlacement
|
import it.vfsfitvnm.reordering.animateItemPlacement
|
||||||
import it.vfsfitvnm.reordering.draggedItem
|
import it.vfsfitvnm.reordering.draggedItem
|
||||||
import it.vfsfitvnm.reordering.rememberReorderingState
|
import it.vfsfitvnm.reordering.rememberReorderingState
|
||||||
import it.vfsfitvnm.reordering.verticalDragToReorder
|
import it.vfsfitvnm.reordering.reorder
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
@@ -92,7 +92,8 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
val thumbnailSize = Dimensions.thumbnails.song.px
|
||||||
|
|
||||||
val reorderingState = rememberReorderingState(
|
val reorderingState = rememberReorderingState(
|
||||||
items = playlistWithSongs.songs,
|
lazyListState = lazyListState,
|
||||||
|
key = playlistWithSongs.songs,
|
||||||
onDragStart = {
|
onDragStart = {
|
||||||
hapticFeedback.performHapticFeedback(
|
hapticFeedback.performHapticFeedback(
|
||||||
HapticFeedbackType.LongPress
|
HapticFeedbackType.LongPress
|
||||||
@@ -123,11 +124,7 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemSizeProvider = { index ->
|
extraItemCount = 3
|
||||||
lazyListState.layoutInfo.visibleItemsInfo.find {
|
|
||||||
it.index == index + 3
|
|
||||||
}?.size
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var isRenaming by rememberSaveable {
|
var isRenaming by rememberSaveable {
|
||||||
@@ -164,8 +161,8 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn(
|
ReorderingLazyColumn(
|
||||||
state = lazyListState,
|
reorderingState = reorderingState,
|
||||||
contentPadding = WindowInsets.systemBars.asPaddingValues()
|
contentPadding = WindowInsets.systemBars.asPaddingValues()
|
||||||
.add(bottom = Dimensions.collapsedPlayer),
|
.add(bottom = Dimensions.collapsedPlayer),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -311,7 +308,7 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { }
|
.clickable { }
|
||||||
.verticalDragToReorder(
|
.reorder(
|
||||||
reorderingState = reorderingState,
|
reorderingState = reorderingState,
|
||||||
index = index
|
index = index
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -37,10 +36,11 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.valentinilk.shimmer.shimmer
|
import com.valentinilk.shimmer.shimmer
|
||||||
|
import it.vfsfitvnm.reordering.ReorderingLazyColumn
|
||||||
import it.vfsfitvnm.reordering.animateItemPlacement
|
import it.vfsfitvnm.reordering.animateItemPlacement
|
||||||
import it.vfsfitvnm.reordering.draggedItem
|
import it.vfsfitvnm.reordering.draggedItem
|
||||||
import it.vfsfitvnm.reordering.rememberReorderingState
|
import it.vfsfitvnm.reordering.rememberReorderingState
|
||||||
import it.vfsfitvnm.reordering.verticalDragToReorder
|
import it.vfsfitvnm.reordering.reorder
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||||
@@ -79,11 +79,9 @@ fun CurrentPlaylistView(
|
|||||||
val windows by rememberWindows(binder.player)
|
val windows by rememberWindows(binder.player)
|
||||||
val shouldBePlaying by rememberShouldBePlaying(binder.player)
|
val shouldBePlaying by rememberShouldBePlaying(binder.player)
|
||||||
|
|
||||||
val lazyListState =
|
|
||||||
rememberLazyListState(initialFirstVisibleItemIndex = mediaItemIndex)
|
|
||||||
|
|
||||||
val reorderingState = rememberReorderingState(
|
val reorderingState = rememberReorderingState(
|
||||||
items = windows,
|
lazyListState = rememberLazyListState(initialFirstVisibleItemIndex = mediaItemIndex),
|
||||||
|
key = windows,
|
||||||
onDragStart = {
|
onDragStart = {
|
||||||
hapticFeedback.performHapticFeedback(
|
hapticFeedback.performHapticFeedback(
|
||||||
HapticFeedbackType.LongPress
|
HapticFeedbackType.LongPress
|
||||||
@@ -92,24 +90,20 @@ fun CurrentPlaylistView(
|
|||||||
onDragEnd = { fromIndex, toIndex ->
|
onDragEnd = { fromIndex, toIndex ->
|
||||||
binder.player.moveMediaItem(fromIndex, toIndex)
|
binder.player.moveMediaItem(fromIndex, toIndex)
|
||||||
},
|
},
|
||||||
itemSizeProvider = { index ->
|
extraItemCount = 0
|
||||||
lazyListState.layoutInfo.visibleItemsInfo.find {
|
|
||||||
it.index == index
|
|
||||||
}?.size
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val paddingValues = WindowInsets.systemBars.asPaddingValues()
|
val paddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
val bottomPadding = paddingValues.calculateBottomPadding()
|
val bottomPadding = paddingValues.calculateBottomPadding()
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
LazyColumn(
|
ReorderingLazyColumn(
|
||||||
state = lazyListState,
|
reorderingState = reorderingState,
|
||||||
contentPadding = paddingValues.add(bottom = -bottomPadding),
|
contentPadding = paddingValues.add(bottom = -bottomPadding),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.nestedScroll(remember {
|
.nestedScroll(remember {
|
||||||
layoutState.nestedScrollConnection(lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0)
|
layoutState.nestedScrollConnection(reorderingState.lazyListState.firstVisibleItemIndex == 0 && reorderingState.lazyListState.firstVisibleItemScrollOffset == 0)
|
||||||
})
|
})
|
||||||
.background(colorPalette.background1)
|
.background(colorPalette.background1)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
@@ -182,7 +176,7 @@ fun CurrentPlaylistView(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { }
|
.clickable { }
|
||||||
.verticalDragToReorder(
|
.reorder(
|
||||||
reorderingState = reorderingState,
|
reorderingState = reorderingState,
|
||||||
index = window.firstPeriodIndex
|
index = window.firstPeriodIndex
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
package it.vfsfitvnm.reordering
|
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
|
||||||
import androidx.compose.ui.input.pointer.PointerInputScope
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlin.reflect.KSuspendFunction5
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
private fun Modifier.dragToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
orientation: Orientation,
|
|
||||||
function: KSuspendFunction5<PointerInputScope, (Offset) -> Unit, () -> Unit, () -> Unit, (change: PointerInputChange, dragAmount: Offset) -> Unit, Unit>,
|
|
||||||
): Modifier = pointerInput(reorderingState) {
|
|
||||||
// require(index in 0..reorderingState.lastIndex)
|
|
||||||
|
|
||||||
var previousItemSize = 0
|
|
||||||
var nextItemSize = 0
|
|
||||||
|
|
||||||
function(
|
|
||||||
this,
|
|
||||||
{
|
|
||||||
reorderingState.onDragStart.invoke()
|
|
||||||
reorderingState.draggingIndex = index
|
|
||||||
reorderingState.reachedIndex = index
|
|
||||||
reorderingState.draggingItemSize = reorderingState.itemSizeProvider?.invoke(index) ?: when (orientation) {
|
|
||||||
Orientation.Vertical -> size.height
|
|
||||||
Orientation.Horizontal -> size.width
|
|
||||||
}
|
|
||||||
|
|
||||||
nextItemSize = reorderingState.draggingItemSize
|
|
||||||
previousItemSize = -reorderingState.draggingItemSize
|
|
||||||
|
|
||||||
reorderingState.offset.updateBounds(
|
|
||||||
lowerBound = -index * reorderingState.draggingItemSize,
|
|
||||||
upperBound = (reorderingState.lastIndex - index) * reorderingState.draggingItemSize
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reorderingState.coroutineScope.launch {
|
|
||||||
reorderingState.offset.animateTo((previousItemSize + nextItemSize) / 2)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
reorderingState.onDragEnd.invoke(index, reorderingState.reachedIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reorderingState.areEquals(
|
|
||||||
reorderingState.draggingIndex,
|
|
||||||
reorderingState.reachedIndex
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
reorderingState.draggingIndex = -1
|
|
||||||
reorderingState.reachedIndex = -1
|
|
||||||
reorderingState.draggingItemSize = 0
|
|
||||||
reorderingState.offset.snapTo(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
{ _, offset ->
|
|
||||||
val delta = when (orientation) {
|
|
||||||
Orientation.Vertical -> offset.y
|
|
||||||
Orientation.Horizontal -> offset.x
|
|
||||||
}.roundToInt()
|
|
||||||
|
|
||||||
val targetOffset = reorderingState.offset.value + delta
|
|
||||||
|
|
||||||
if (targetOffset > nextItemSize) {
|
|
||||||
if (reorderingState.reachedIndex < reorderingState.lastIndex) {
|
|
||||||
reorderingState.reachedIndex += 1
|
|
||||||
nextItemSize += reorderingState.draggingItemSize
|
|
||||||
previousItemSize += reorderingState.draggingItemSize
|
|
||||||
}
|
|
||||||
} else if (targetOffset < previousItemSize) {
|
|
||||||
if (reorderingState.reachedIndex > 0) {
|
|
||||||
reorderingState.reachedIndex -= 1
|
|
||||||
previousItemSize -= reorderingState.draggingItemSize
|
|
||||||
nextItemSize -= reorderingState.draggingItemSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reorderingState.coroutineScope.launch {
|
|
||||||
reorderingState.offset.snapTo(targetOffset)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Modifier.dragToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
orientation: Orientation,
|
|
||||||
): Modifier = dragToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = orientation,
|
|
||||||
function = PointerInputScope::detectDragGestures,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Modifier.verticalDragToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
): Modifier = dragToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Modifier.horizontalDragToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
): Modifier = dragToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = Orientation.Horizontal,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Modifier.dragAfterLongPressToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
orientation: Orientation,
|
|
||||||
): Modifier = dragToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = orientation,
|
|
||||||
function = PointerInputScope::detectDragGesturesAfterLongPress,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Modifier.verticalDragAfterLongPressToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
): Modifier = dragAfterLongPressToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Modifier.horizontalDragAfterLongPressToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int
|
|
||||||
): Modifier = dragAfterLongPressToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = Orientation.Horizontal,
|
|
||||||
)
|
|
||||||
@@ -15,7 +15,7 @@ fun Modifier.draggedItem(
|
|||||||
val translation by reorderingState.translationFor(index)
|
val translation by reorderingState.translationFor(index)
|
||||||
|
|
||||||
offset {
|
offset {
|
||||||
when (reorderingState.orientation) {
|
when (reorderingState.lazyListState.layoutInfo.orientation) {
|
||||||
Orientation.Vertical -> IntOffset(0, translation)
|
Orientation.Vertical -> IntOffset(0, translation)
|
||||||
Orientation.Horizontal -> IntOffset(translation, 0)
|
Orientation.Horizontal -> IntOffset(translation, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package it.vfsfitvnm.reordering
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
|
||||||
|
private fun Modifier.reorder(
|
||||||
|
reorderingState: ReorderingState,
|
||||||
|
index: Int,
|
||||||
|
detectDragGestures: DetectDragGestures,
|
||||||
|
): Modifier = pointerInput(reorderingState) {
|
||||||
|
with(detectDragGestures) {
|
||||||
|
detectDragGestures(
|
||||||
|
onDragStart = { reorderingState.onDragStart(index) },
|
||||||
|
onDrag = reorderingState::onDrag,
|
||||||
|
onDragEnd = reorderingState::onDragEnd,
|
||||||
|
onDragCancel = reorderingState::onDragEnd,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.reorder(
|
||||||
|
reorderingState: ReorderingState,
|
||||||
|
index: Int,
|
||||||
|
): Modifier = reorder(
|
||||||
|
reorderingState = reorderingState,
|
||||||
|
index = index,
|
||||||
|
detectDragGestures = PointerInputScope::detectDragGestures,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Modifier.reorderAfterLongPress(
|
||||||
|
reorderingState: ReorderingState,
|
||||||
|
index: Int
|
||||||
|
): Modifier = reorder(
|
||||||
|
reorderingState = reorderingState,
|
||||||
|
index = index,
|
||||||
|
detectDragGestures = PointerInputScope::detectDragGesturesAfterLongPress,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun interface DetectDragGestures {
|
||||||
|
suspend fun PointerInputScope.detectDragGestures(
|
||||||
|
onDragStart: (Offset) -> Unit,
|
||||||
|
onDragEnd: () -> Unit,
|
||||||
|
onDragCancel: () -> Unit,
|
||||||
|
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||||
|
|
||||||
|
package it.vfsfitvnm.reordering
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.FlingBehavior
|
||||||
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReorderingLazyColumn(
|
||||||
|
reorderingState: ReorderingState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
|
reverseLayout: Boolean = false,
|
||||||
|
verticalArrangement: Arrangement.Vertical =
|
||||||
|
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
|
||||||
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||||
|
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
|
userScrollEnabled: Boolean = true,
|
||||||
|
content: LazyListScope.() -> Unit
|
||||||
|
) {
|
||||||
|
ReorderingLazyList(
|
||||||
|
modifier = modifier,
|
||||||
|
state = reorderingState.lazyListState,
|
||||||
|
reorderingState = reorderingState,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
flingBehavior = flingBehavior,
|
||||||
|
horizontalAlignment = horizontalAlignment,
|
||||||
|
verticalArrangement = verticalArrangement,
|
||||||
|
isVertical = true,
|
||||||
|
reverseLayout = reverseLayout,
|
||||||
|
userScrollEnabled = userScrollEnabled,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||||
|
|
||||||
|
package it.vfsfitvnm.reordering
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.OverscrollEffect
|
||||||
|
import androidx.compose.foundation.checkScrollableContainerConstraints
|
||||||
|
import androidx.compose.foundation.clipScrollableContainer
|
||||||
|
import androidx.compose.foundation.gestures.FlingBehavior
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
|
import androidx.compose.foundation.gestures.scrollable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
|
import androidx.compose.foundation.lazy.DataIndex
|
||||||
|
import androidx.compose.foundation.lazy.LazyListBeyondBoundsInfo
|
||||||
|
import androidx.compose.foundation.lazy.LazyListItemPlacementAnimator
|
||||||
|
import androidx.compose.foundation.lazy.LazyListItemProvider
|
||||||
|
import androidx.compose.foundation.lazy.LazyListMeasureResult
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.LazyMeasuredItem
|
||||||
|
import androidx.compose.foundation.lazy.LazyMeasuredItemProvider
|
||||||
|
import androidx.compose.foundation.lazy.layout.LazyLayout
|
||||||
|
import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
|
||||||
|
import androidx.compose.foundation.lazy.lazyListBeyondBoundsModifier
|
||||||
|
import androidx.compose.foundation.lazy.lazyListPinningModifier
|
||||||
|
import androidx.compose.foundation.lazy.lazyListSemantics
|
||||||
|
import androidx.compose.foundation.lazy.measureLazyList
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListItemProvider
|
||||||
|
import androidx.compose.foundation.overscroll
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.snapshots.Snapshot
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.MeasureResult
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.constrainHeight
|
||||||
|
import androidx.compose.ui.unit.constrainWidth
|
||||||
|
import androidx.compose.ui.unit.offset
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun ReorderingLazyList(
|
||||||
|
modifier: Modifier,
|
||||||
|
state: LazyListState,
|
||||||
|
reorderingState: ReorderingState,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
reverseLayout: Boolean,
|
||||||
|
isVertical: Boolean,
|
||||||
|
flingBehavior: FlingBehavior,
|
||||||
|
userScrollEnabled: Boolean,
|
||||||
|
horizontalAlignment: Alignment.Horizontal? = null,
|
||||||
|
verticalArrangement: Arrangement.Vertical? = null,
|
||||||
|
verticalAlignment: Alignment.Vertical? = null,
|
||||||
|
horizontalArrangement: Arrangement.Horizontal? = null,
|
||||||
|
content: LazyListScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val overscrollEffect = ScrollableDefaults.overscrollEffect()
|
||||||
|
val itemProvider = rememberLazyListItemProvider(state, content)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val placementAnimator = remember(state, isVertical) {
|
||||||
|
LazyListItemPlacementAnimator(scope, isVertical)
|
||||||
|
}
|
||||||
|
state.placementAnimator = placementAnimator
|
||||||
|
|
||||||
|
val measurePolicy = rememberLazyListMeasurePolicy(
|
||||||
|
itemProvider,
|
||||||
|
state,
|
||||||
|
reorderingState.lazyListBeyondBoundsInfo,
|
||||||
|
overscrollEffect,
|
||||||
|
contentPadding,
|
||||||
|
reverseLayout,
|
||||||
|
isVertical,
|
||||||
|
horizontalAlignment,
|
||||||
|
verticalAlignment,
|
||||||
|
horizontalArrangement,
|
||||||
|
verticalArrangement,
|
||||||
|
placementAnimator
|
||||||
|
)
|
||||||
|
|
||||||
|
val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
|
||||||
|
LazyLayout(
|
||||||
|
modifier = modifier
|
||||||
|
.then(state.remeasurementModifier)
|
||||||
|
.then(state.awaitLayoutModifier)
|
||||||
|
.lazyListSemantics(
|
||||||
|
itemProvider = itemProvider,
|
||||||
|
state = state,
|
||||||
|
coroutineScope = scope,
|
||||||
|
isVertical = isVertical,
|
||||||
|
reverseScrolling = reverseLayout,
|
||||||
|
userScrollEnabled = userScrollEnabled
|
||||||
|
)
|
||||||
|
.clipScrollableContainer(orientation)
|
||||||
|
.lazyListBeyondBoundsModifier(state, reorderingState.lazyListBeyondBoundsInfo, reverseLayout)
|
||||||
|
.lazyListPinningModifier(state, reorderingState.lazyListBeyondBoundsInfo)
|
||||||
|
.overscroll(overscrollEffect)
|
||||||
|
.scrollable(
|
||||||
|
orientation = orientation,
|
||||||
|
reverseDirection = ScrollableDefaults.reverseDirection(
|
||||||
|
LocalLayoutDirection.current,
|
||||||
|
orientation,
|
||||||
|
reverseLayout
|
||||||
|
),
|
||||||
|
interactionSource = state.internalInteractionSource,
|
||||||
|
flingBehavior = flingBehavior,
|
||||||
|
state = state,
|
||||||
|
overscrollEffect = overscrollEffect,
|
||||||
|
enabled = userScrollEnabled
|
||||||
|
),
|
||||||
|
prefetchState = state.prefetchState,
|
||||||
|
measurePolicy = measurePolicy,
|
||||||
|
itemProvider = itemProvider
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@Composable
|
||||||
|
private fun rememberLazyListMeasurePolicy(
|
||||||
|
itemProvider: LazyListItemProvider,
|
||||||
|
state: LazyListState,
|
||||||
|
beyondBoundsInfo: LazyListBeyondBoundsInfo,
|
||||||
|
overscrollEffect: OverscrollEffect,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
reverseLayout: Boolean,
|
||||||
|
isVertical: Boolean,
|
||||||
|
horizontalAlignment: Alignment.Horizontal? = null,
|
||||||
|
verticalAlignment: Alignment.Vertical? = null,
|
||||||
|
horizontalArrangement: Arrangement.Horizontal? = null,
|
||||||
|
verticalArrangement: Arrangement.Vertical? = null,
|
||||||
|
placementAnimator: LazyListItemPlacementAnimator
|
||||||
|
) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
|
||||||
|
state,
|
||||||
|
beyondBoundsInfo,
|
||||||
|
overscrollEffect,
|
||||||
|
contentPadding,
|
||||||
|
reverseLayout,
|
||||||
|
isVertical,
|
||||||
|
horizontalAlignment,
|
||||||
|
verticalAlignment,
|
||||||
|
horizontalArrangement,
|
||||||
|
verticalArrangement,
|
||||||
|
placementAnimator
|
||||||
|
) {
|
||||||
|
{ containerConstraints ->
|
||||||
|
checkScrollableContainerConstraints(
|
||||||
|
containerConstraints,
|
||||||
|
if (isVertical) Orientation.Vertical else Orientation.Horizontal
|
||||||
|
)
|
||||||
|
|
||||||
|
// resolve content paddings
|
||||||
|
val startPadding =
|
||||||
|
if (isVertical) {
|
||||||
|
contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
|
||||||
|
} else {
|
||||||
|
// in horizontal configuration, padding is reversed by placeRelative
|
||||||
|
contentPadding.calculateStartPadding(layoutDirection).roundToPx()
|
||||||
|
}
|
||||||
|
|
||||||
|
val endPadding =
|
||||||
|
if (isVertical) {
|
||||||
|
contentPadding.calculateRightPadding(layoutDirection).roundToPx()
|
||||||
|
} else {
|
||||||
|
// in horizontal configuration, padding is reversed by placeRelative
|
||||||
|
contentPadding.calculateEndPadding(layoutDirection).roundToPx()
|
||||||
|
}
|
||||||
|
val topPadding = contentPadding.calculateTopPadding().roundToPx()
|
||||||
|
val bottomPadding = contentPadding.calculateBottomPadding().roundToPx()
|
||||||
|
val totalVerticalPadding = topPadding + bottomPadding
|
||||||
|
val totalHorizontalPadding = startPadding + endPadding
|
||||||
|
val totalMainAxisPadding = if (isVertical) totalVerticalPadding else totalHorizontalPadding
|
||||||
|
val beforeContentPadding = when {
|
||||||
|
isVertical && !reverseLayout -> topPadding
|
||||||
|
isVertical && reverseLayout -> bottomPadding
|
||||||
|
!isVertical && !reverseLayout -> startPadding
|
||||||
|
else -> endPadding // !isVertical && reverseLayout
|
||||||
|
}
|
||||||
|
val afterContentPadding = totalMainAxisPadding - beforeContentPadding
|
||||||
|
val contentConstraints =
|
||||||
|
containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
|
||||||
|
|
||||||
|
// Update the state's cached Density
|
||||||
|
state.density = this
|
||||||
|
|
||||||
|
// this will update the scope used by the item composables
|
||||||
|
itemProvider.itemScope.maxWidth = contentConstraints.maxWidth.toDp()
|
||||||
|
itemProvider.itemScope.maxHeight = contentConstraints.maxHeight.toDp()
|
||||||
|
|
||||||
|
val spaceBetweenItemsDp = if (isVertical) {
|
||||||
|
requireNotNull(verticalArrangement).spacing
|
||||||
|
} else {
|
||||||
|
requireNotNull(horizontalArrangement).spacing
|
||||||
|
}
|
||||||
|
val spaceBetweenItems = spaceBetweenItemsDp.roundToPx()
|
||||||
|
|
||||||
|
val itemsCount = itemProvider.itemCount
|
||||||
|
|
||||||
|
// can be negative if the content padding is larger than the max size from constraints
|
||||||
|
val mainAxisAvailableSize = if (isVertical) {
|
||||||
|
containerConstraints.maxHeight - totalVerticalPadding
|
||||||
|
} else {
|
||||||
|
containerConstraints.maxWidth - totalHorizontalPadding
|
||||||
|
}
|
||||||
|
val visualItemOffset = if (!reverseLayout || mainAxisAvailableSize > 0) {
|
||||||
|
IntOffset(startPadding, topPadding)
|
||||||
|
} else {
|
||||||
|
// When layout is reversed and paddings together take >100% of the available space,
|
||||||
|
// layout size is coerced to 0 when positioning. To take that space into account,
|
||||||
|
// we offset start padding by negative space between paddings.
|
||||||
|
IntOffset(
|
||||||
|
if (isVertical) startPadding else startPadding + mainAxisAvailableSize,
|
||||||
|
if (isVertical) topPadding + mainAxisAvailableSize else topPadding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val measuredItemProvider = LazyMeasuredItemProvider(
|
||||||
|
contentConstraints,
|
||||||
|
isVertical,
|
||||||
|
itemProvider,
|
||||||
|
this
|
||||||
|
) { index, key, placeables ->
|
||||||
|
// we add spaceBetweenItems as an extra spacing for all items apart from the last one so
|
||||||
|
// the lazy list measuring logic will take it into account.
|
||||||
|
val spacing = if (index.value == itemsCount - 1) 0 else spaceBetweenItems
|
||||||
|
LazyMeasuredItem(
|
||||||
|
index = index.value,
|
||||||
|
placeables = placeables,
|
||||||
|
isVertical = isVertical,
|
||||||
|
horizontalAlignment = horizontalAlignment,
|
||||||
|
verticalAlignment = verticalAlignment,
|
||||||
|
layoutDirection = layoutDirection,
|
||||||
|
reverseLayout = reverseLayout,
|
||||||
|
beforeContentPadding = beforeContentPadding,
|
||||||
|
afterContentPadding = afterContentPadding,
|
||||||
|
spacing = spacing,
|
||||||
|
visualOffset = visualItemOffset,
|
||||||
|
key = key,
|
||||||
|
placementAnimator = placementAnimator
|
||||||
|
)
|
||||||
|
}
|
||||||
|
state.premeasureConstraints = measuredItemProvider.childConstraints
|
||||||
|
|
||||||
|
val firstVisibleItemIndex: DataIndex
|
||||||
|
val firstVisibleScrollOffset: Int
|
||||||
|
Snapshot.withoutReadObservation {
|
||||||
|
firstVisibleItemIndex = DataIndex(state.firstVisibleItemIndex)
|
||||||
|
firstVisibleScrollOffset = state.firstVisibleItemScrollOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
measureLazyList(
|
||||||
|
itemsCount = itemsCount,
|
||||||
|
itemProvider = measuredItemProvider,
|
||||||
|
mainAxisAvailableSize = mainAxisAvailableSize,
|
||||||
|
beforeContentPadding = beforeContentPadding,
|
||||||
|
afterContentPadding = afterContentPadding,
|
||||||
|
firstVisibleItemIndex = firstVisibleItemIndex,
|
||||||
|
firstVisibleItemScrollOffset = firstVisibleScrollOffset,
|
||||||
|
scrollToBeConsumed = state.scrollToBeConsumed,
|
||||||
|
constraints = contentConstraints,
|
||||||
|
isVertical = isVertical,
|
||||||
|
headerIndexes = itemProvider.headerIndexes,
|
||||||
|
verticalArrangement = verticalArrangement,
|
||||||
|
horizontalArrangement = horizontalArrangement,
|
||||||
|
reverseLayout = reverseLayout,
|
||||||
|
density = this,
|
||||||
|
placementAnimator = placementAnimator,
|
||||||
|
beyondBoundsInfo = beyondBoundsInfo,
|
||||||
|
layout = { width, height, placement ->
|
||||||
|
layout(
|
||||||
|
containerConstraints.constrainWidth(width + totalHorizontalPadding),
|
||||||
|
containerConstraints.constrainHeight(height + totalVerticalPadding),
|
||||||
|
emptyMap(),
|
||||||
|
placement
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).also {
|
||||||
|
state.applyMeasureResult(it)
|
||||||
|
refreshOverscrollInfo(overscrollEffect, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
private fun refreshOverscrollInfo(
|
||||||
|
overscrollEffect: OverscrollEffect,
|
||||||
|
result: LazyListMeasureResult
|
||||||
|
) {
|
||||||
|
val canScrollForward = result.canScrollForward
|
||||||
|
val canScrollBackward = (result.firstVisibleItem?.index ?: 0) != 0 ||
|
||||||
|
result.firstVisibleItemScrollOffset != 0
|
||||||
|
|
||||||
|
overscrollEffect.isEnabled = canScrollForward || canScrollBackward
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||||
|
|
||||||
package it.vfsfitvnm.reordering
|
package it.vfsfitvnm.reordering
|
||||||
|
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
@@ -5,6 +7,9 @@ import androidx.compose.animation.core.AnimationVector1D
|
|||||||
import androidx.compose.animation.core.VectorConverter
|
import androidx.compose.animation.core.VectorConverter
|
||||||
import androidx.compose.animation.core.animateIntAsState
|
import androidx.compose.animation.core.animateIntAsState
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.lazy.LazyListBeyondBoundsInfo
|
||||||
|
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -12,22 +17,36 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||||
|
import kotlin.math.roundToInt
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class ReorderingState(
|
class ReorderingState(
|
||||||
internal val itemSizeProvider: ((Int) -> Int?)?,
|
val lazyListState: LazyListState,
|
||||||
internal val coroutineScope: CoroutineScope,
|
internal val coroutineScope: CoroutineScope,
|
||||||
internal val lastIndex: Int,
|
private val lastIndex: Int,
|
||||||
internal val areEquals: (Int, Int) -> Boolean,
|
|
||||||
internal val orientation: Orientation,
|
|
||||||
internal val onDragStart: () -> Unit,
|
internal val onDragStart: () -> Unit,
|
||||||
internal val onDragEnd: (Int, Int) -> Unit,
|
internal val onDragEnd: (Int, Int) -> Unit,
|
||||||
|
private val extraItemCount: Int
|
||||||
) {
|
) {
|
||||||
|
private lateinit var lazyListBeyondBoundsInfoInterval: LazyListBeyondBoundsInfo.Interval
|
||||||
|
internal val lazyListBeyondBoundsInfo = LazyListBeyondBoundsInfo()
|
||||||
internal val offset: Animatable<Int, AnimationVector1D> = Animatable(0, Int.VectorConverter)
|
internal val offset: Animatable<Int, AnimationVector1D> = Animatable(0, Int.VectorConverter)
|
||||||
|
|
||||||
internal var draggingIndex by mutableStateOf(-1)
|
internal var draggingIndex by mutableStateOf(-1)
|
||||||
internal var reachedIndex by mutableStateOf(-1)
|
private var reachedIndex by mutableStateOf(-1)
|
||||||
internal var draggingItemSize by mutableStateOf(0)
|
private var draggingItemSize by mutableStateOf(0)
|
||||||
|
|
||||||
|
lateinit var itemInfo: LazyListItemInfo
|
||||||
|
|
||||||
|
var previousItemSize = 0
|
||||||
|
var nextItemSize = 0
|
||||||
|
|
||||||
|
private var overscrolled = 0
|
||||||
|
|
||||||
private val noTranslation = object : State<Int> {
|
private val noTranslation = object : State<Int> {
|
||||||
override val value = 0
|
override val value = 0
|
||||||
@@ -45,27 +64,123 @@ class ReorderingState(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onDragStart(index: Int) {
|
||||||
|
overscrolled = 0
|
||||||
|
itemInfo = lazyListState.layoutInfo.visibleItemsInfo.find {
|
||||||
|
it.index == index + extraItemCount
|
||||||
|
}!!
|
||||||
|
onDragStart.invoke()
|
||||||
|
draggingIndex = index
|
||||||
|
reachedIndex = index
|
||||||
|
draggingItemSize = itemInfo.size
|
||||||
|
|
||||||
|
nextItemSize = draggingItemSize
|
||||||
|
previousItemSize = -draggingItemSize
|
||||||
|
|
||||||
|
offset.updateBounds(
|
||||||
|
lowerBound = -index * draggingItemSize,
|
||||||
|
upperBound = (lastIndex - index) * draggingItemSize
|
||||||
|
)
|
||||||
|
|
||||||
|
lazyListBeyondBoundsInfoInterval =
|
||||||
|
lazyListBeyondBoundsInfo.addInterval(index + extraItemCount, index + extraItemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDrag(change: PointerInputChange, dragAmount: Offset) {
|
||||||
|
change.consume()
|
||||||
|
|
||||||
|
val delta = when (lazyListState.layoutInfo.orientation) {
|
||||||
|
Orientation.Vertical -> dragAmount.y
|
||||||
|
Orientation.Horizontal -> dragAmount.x
|
||||||
|
}.roundToInt()
|
||||||
|
|
||||||
|
val targetOffset = offset.value + delta
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
offset.snapTo(targetOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetOffset > nextItemSize) {
|
||||||
|
if (reachedIndex < lastIndex) {
|
||||||
|
reachedIndex += 1
|
||||||
|
nextItemSize += draggingItemSize
|
||||||
|
previousItemSize += draggingItemSize
|
||||||
|
}
|
||||||
|
} else if (targetOffset < previousItemSize) {
|
||||||
|
if (reachedIndex > 0) {
|
||||||
|
reachedIndex -= 1
|
||||||
|
previousItemSize -= draggingItemSize
|
||||||
|
nextItemSize -= draggingItemSize
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val offsetInViewPort = targetOffset + itemInfo.offset - overscrolled
|
||||||
|
val topOverscroll = lazyListState.layoutInfo.viewportStartOffset - offsetInViewPort
|
||||||
|
val bottomOverscroll =
|
||||||
|
lazyListState.layoutInfo.viewportEndOffset - offsetInViewPort - itemInfo.size
|
||||||
|
|
||||||
|
if (topOverscroll > 0) {
|
||||||
|
overscroll(topOverscroll)
|
||||||
|
} else if (bottomOverscroll < 0) {
|
||||||
|
overscroll(bottomOverscroll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDragEnd() {
|
||||||
|
coroutineScope.launch {
|
||||||
|
offset.animateTo((previousItemSize + nextItemSize) / 2)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onDragEnd.invoke(draggingIndex, reachedIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areEquals()) {
|
||||||
|
draggingIndex = -1
|
||||||
|
reachedIndex = -1
|
||||||
|
draggingItemSize = 0
|
||||||
|
offset.snapTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazyListBeyondBoundsInfo.removeInterval(lazyListBeyondBoundsInfoInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun overscroll(overscroll: Int) {
|
||||||
|
lazyListState.dispatchRawDelta(-overscroll.toFloat())
|
||||||
|
coroutineScope.launch {
|
||||||
|
offset.snapTo(offset.value - overscroll)
|
||||||
|
}
|
||||||
|
overscrolled -= overscroll
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun areEquals(): Boolean {
|
||||||
|
return lazyListState.layoutInfo.visibleItemsInfo.find {
|
||||||
|
it.index + extraItemCount == draggingIndex
|
||||||
|
}?.key == lazyListState.layoutInfo.visibleItemsInfo.find {
|
||||||
|
it.index + extraItemCount == reachedIndex
|
||||||
|
}?.key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberReorderingState(
|
fun rememberReorderingState(
|
||||||
items: List<Any>,
|
lazyListState: LazyListState,
|
||||||
|
key: Any,
|
||||||
onDragEnd: (Int, Int) -> Unit,
|
onDragEnd: (Int, Int) -> Unit,
|
||||||
onDragStart: () -> Unit = {},
|
onDragStart: () -> Unit = {},
|
||||||
orientation: Orientation = Orientation.Vertical,
|
extraItemCount: Int = 0
|
||||||
itemSizeProvider: ((Int) -> Int?)? = null
|
|
||||||
): ReorderingState {
|
): ReorderingState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
return remember(items) {
|
return remember(key) {
|
||||||
ReorderingState(
|
ReorderingState(
|
||||||
itemSizeProvider = itemSizeProvider,
|
lazyListState = lazyListState,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
orientation = orientation,
|
lastIndex = if (key is List<*>) key.lastIndex else lazyListState.layoutInfo.totalItemsCount,
|
||||||
lastIndex = items.lastIndex,
|
|
||||||
areEquals = { i, j -> items[i] == items[j] },
|
|
||||||
onDragStart = onDragStart,
|
onDragStart = onDragStart,
|
||||||
onDragEnd = onDragEnd,
|
onDragEnd = onDragEnd,
|
||||||
|
extraItemCount = extraItemCount,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user