Improve compose-reordering (#176)
This commit is contained in:
parent
4ae3c604e4
commit
1682228ece
8 changed files with 539 additions and 194 deletions
|
@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import it.vfsfitvnm.reordering.ReorderingLazyColumn
|
||||
import it.vfsfitvnm.reordering.animateItemPlacement
|
||||
import it.vfsfitvnm.reordering.draggedItem
|
||||
import it.vfsfitvnm.reordering.rememberReorderingState
|
||||
import it.vfsfitvnm.reordering.verticalDragToReorder
|
||||
import it.vfsfitvnm.reordering.reorder
|
||||
import it.vfsfitvnm.route.RouteHandler
|
||||
import it.vfsfitvnm.vimusic.Database
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||
|
@ -92,7 +92,8 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||
val thumbnailSize = Dimensions.thumbnails.song.px
|
||||
|
||||
val reorderingState = rememberReorderingState(
|
||||
items = playlistWithSongs.songs,
|
||||
lazyListState = lazyListState,
|
||||
key = playlistWithSongs.songs,
|
||||
onDragStart = {
|
||||
hapticFeedback.performHapticFeedback(
|
||||
HapticFeedbackType.LongPress
|
||||
|
@ -123,11 +124,7 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||
)
|
||||
}
|
||||
},
|
||||
itemSizeProvider = { index ->
|
||||
lazyListState.layoutInfo.visibleItemsInfo.find {
|
||||
it.index == index + 3
|
||||
}?.size
|
||||
}
|
||||
extraItemCount = 3
|
||||
)
|
||||
|
||||
var isRenaming by rememberSaveable {
|
||||
|
@ -164,8 +161,8 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
ReorderingLazyColumn(
|
||||
reorderingState = reorderingState,
|
||||
contentPadding = WindowInsets.systemBars.asPaddingValues()
|
||||
.add(bottom = Dimensions.collapsedPlayer),
|
||||
modifier = Modifier
|
||||
|
@ -311,7 +308,7 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||
modifier = Modifier
|
||||
.clickable { }
|
||||
.verticalDragToReorder(
|
||||
.reorder(
|
||||
reorderingState = reorderingState,
|
||||
index = index
|
||||
)
|
||||
|
|
|
@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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.unit.dp
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
import it.vfsfitvnm.reordering.ReorderingLazyColumn
|
||||
import it.vfsfitvnm.reordering.animateItemPlacement
|
||||
import it.vfsfitvnm.reordering.draggedItem
|
||||
import it.vfsfitvnm.reordering.rememberReorderingState
|
||||
import it.vfsfitvnm.reordering.verticalDragToReorder
|
||||
import it.vfsfitvnm.reordering.reorder
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||
|
@ -79,11 +79,9 @@ fun CurrentPlaylistView(
|
|||
val windows by rememberWindows(binder.player)
|
||||
val shouldBePlaying by rememberShouldBePlaying(binder.player)
|
||||
|
||||
val lazyListState =
|
||||
rememberLazyListState(initialFirstVisibleItemIndex = mediaItemIndex)
|
||||
|
||||
val reorderingState = rememberReorderingState(
|
||||
items = windows,
|
||||
lazyListState = rememberLazyListState(initialFirstVisibleItemIndex = mediaItemIndex),
|
||||
key = windows,
|
||||
onDragStart = {
|
||||
hapticFeedback.performHapticFeedback(
|
||||
HapticFeedbackType.LongPress
|
||||
|
@ -92,24 +90,20 @@ fun CurrentPlaylistView(
|
|||
onDragEnd = { fromIndex, toIndex ->
|
||||
binder.player.moveMediaItem(fromIndex, toIndex)
|
||||
},
|
||||
itemSizeProvider = { index ->
|
||||
lazyListState.layoutInfo.visibleItemsInfo.find {
|
||||
it.index == index
|
||||
}?.size
|
||||
}
|
||||
extraItemCount = 0
|
||||
)
|
||||
|
||||
val paddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||
val bottomPadding = paddingValues.calculateBottomPadding()
|
||||
|
||||
Column {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
ReorderingLazyColumn(
|
||||
reorderingState = reorderingState,
|
||||
contentPadding = paddingValues.add(bottom = -bottomPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.nestedScroll(remember {
|
||||
layoutState.nestedScrollConnection(lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0)
|
||||
layoutState.nestedScrollConnection(reorderingState.lazyListState.firstVisibleItemIndex == 0 && reorderingState.lazyListState.firstVisibleItemScrollOffset == 0)
|
||||
})
|
||||
.background(colorPalette.background1)
|
||||
.weight(1f)
|
||||
|
@ -182,7 +176,7 @@ fun CurrentPlaylistView(
|
|||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||
modifier = Modifier
|
||||
.clickable { }
|
||||
.verticalDragToReorder(
|
||||
.reorder(
|
||||
reorderingState = reorderingState,
|
||||
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)
|
||||
|
||||
offset {
|
||||
when (reorderingState.orientation) {
|
||||
when (reorderingState.lazyListState.layoutInfo.orientation) {
|
||||
Orientation.Vertical -> IntOffset(0, translation)
|
||||
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
|
||||
|
||||
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.animateIntAsState
|
||||
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.State
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -12,22 +17,36 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ReorderingState(
|
||||
internal val itemSizeProvider: ((Int) -> Int?)?,
|
||||
val lazyListState: LazyListState,
|
||||
internal val coroutineScope: CoroutineScope,
|
||||
internal val lastIndex: Int,
|
||||
internal val areEquals: (Int, Int) -> Boolean,
|
||||
internal val orientation: Orientation,
|
||||
private val lastIndex: Int,
|
||||
internal val onDragStart: () -> 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 var draggingIndex by mutableStateOf(-1)
|
||||
internal var reachedIndex by mutableStateOf(-1)
|
||||
internal var draggingItemSize by mutableStateOf(0)
|
||||
private var reachedIndex by mutableStateOf(-1)
|
||||
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> {
|
||||
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
|
||||
fun rememberReorderingState(
|
||||
items: List<Any>,
|
||||
lazyListState: LazyListState,
|
||||
key: Any,
|
||||
onDragEnd: (Int, Int) -> Unit,
|
||||
onDragStart: () -> Unit = {},
|
||||
orientation: Orientation = Orientation.Vertical,
|
||||
itemSizeProvider: ((Int) -> Int?)? = null
|
||||
extraItemCount: Int = 0
|
||||
): ReorderingState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
return remember(items) {
|
||||
return remember(key) {
|
||||
ReorderingState(
|
||||
itemSizeProvider = itemSizeProvider,
|
||||
lazyListState = lazyListState,
|
||||
coroutineScope = coroutineScope,
|
||||
orientation = orientation,
|
||||
lastIndex = items.lastIndex,
|
||||
areEquals = { i, j -> items[i] == items[j] },
|
||||
lastIndex = if (key is List<*>) key.lastIndex else lazyListState.layoutInfo.totalItemsCount,
|
||||
onDragStart = onDragStart,
|
||||
onDragEnd = onDragEnd,
|
||||
extraItemCount = extraItemCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue