Improve compose-reordering code
This commit is contained in:
@@ -35,8 +35,10 @@ 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.animateItemPlacement
|
||||||
|
import it.vfsfitvnm.reordering.draggedItem
|
||||||
import it.vfsfitvnm.reordering.rememberReorderingState
|
import it.vfsfitvnm.reordering.rememberReorderingState
|
||||||
import it.vfsfitvnm.reordering.verticalDragAfterLongPressToReorder
|
import it.vfsfitvnm.reordering.verticalDragToReorder
|
||||||
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
|
||||||
@@ -89,7 +91,44 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||||||
|
|
||||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
val thumbnailSize = Dimensions.thumbnails.song.px
|
||||||
|
|
||||||
val reorderingState = rememberReorderingState(playlistWithSongs.songs)
|
val reorderingState = rememberReorderingState(
|
||||||
|
items = playlistWithSongs.songs,
|
||||||
|
onDragStart = {
|
||||||
|
hapticFeedback.performHapticFeedback(
|
||||||
|
HapticFeedbackType.LongPress
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDragEnd = { fromIndex, toIndex ->
|
||||||
|
transaction {
|
||||||
|
if (fromIndex > toIndex) {
|
||||||
|
Database.incrementSongPositions(
|
||||||
|
playlistId = playlistWithSongs.playlist.id,
|
||||||
|
fromPosition = toIndex,
|
||||||
|
toPosition = fromIndex - 1
|
||||||
|
)
|
||||||
|
} else if (fromIndex < toIndex) {
|
||||||
|
Database.decrementSongPositions(
|
||||||
|
playlistId = playlistWithSongs.playlist.id,
|
||||||
|
fromPosition = fromIndex + 1,
|
||||||
|
toPosition = toIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Database.update(
|
||||||
|
SongPlaylistMap(
|
||||||
|
songId = playlistWithSongs.songs[fromIndex].id,
|
||||||
|
playlistId = playlistWithSongs.playlist.id,
|
||||||
|
position = toIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemSizeProvider = { index ->
|
||||||
|
lazyListState.layoutInfo.visibleItemsInfo.find {
|
||||||
|
it.index == index + 3
|
||||||
|
}?.size
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
var isRenaming by rememberSaveable {
|
var isRenaming by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
@@ -99,9 +138,7 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||||||
TextFieldDialog(
|
TextFieldDialog(
|
||||||
hintText = "Enter the playlist name",
|
hintText = "Enter the playlist name",
|
||||||
initialTextInput = playlistWithSongs.playlist.name,
|
initialTextInput = playlistWithSongs.playlist.name,
|
||||||
onDismiss = {
|
onDismiss = { isRenaming = false },
|
||||||
isRenaming = false
|
|
||||||
},
|
|
||||||
onDone = { text ->
|
onDone = { text ->
|
||||||
query {
|
query {
|
||||||
Database.update(playlistWithSongs.playlist.copy(name = text))
|
Database.update(playlistWithSongs.playlist.copy(name = text))
|
||||||
@@ -117,9 +154,7 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||||||
if (isDeleting) {
|
if (isDeleting) {
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
text = "Do you really want to delete this playlist?",
|
text = "Do you really want to delete this playlist?",
|
||||||
onDismiss = {
|
onDismiss = { isDeleting = false },
|
||||||
isDeleting = false
|
|
||||||
},
|
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
query {
|
query {
|
||||||
Database.delete(playlistWithSongs.playlist)
|
Database.delete(playlistWithSongs.playlist)
|
||||||
@@ -131,7 +166,8 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
contentPadding = WindowInsets.systemBars.asPaddingValues().add(bottom = Dimensions.collapsedPlayer),
|
contentPadding = WindowInsets.systemBars.asPaddingValues()
|
||||||
|
.add(bottom = Dimensions.collapsedPlayer),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(colorPalette.background0)
|
.background(colorPalette.background0)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -274,47 +310,18 @@ fun LocalPlaylistScreen(playlistId: Long) {
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {}
|
.clickable { }
|
||||||
|
.verticalDragToReorder(
|
||||||
|
reorderingState = reorderingState,
|
||||||
|
index = index
|
||||||
|
)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
.size(20.dp)
|
.size(20.dp)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.animateItemPlacement()
|
.animateItemPlacement(reorderingState = reorderingState)
|
||||||
.verticalDragAfterLongPressToReorder(
|
.draggedItem(reorderingState = reorderingState, index = index)
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
onDragStart = {
|
|
||||||
hapticFeedback.performHapticFeedback(
|
|
||||||
HapticFeedbackType.LongPress
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onDragEnd = { reachedIndex ->
|
|
||||||
transaction {
|
|
||||||
if (index > reachedIndex) {
|
|
||||||
Database.incrementSongPositions(
|
|
||||||
playlistId = playlistWithSongs.playlist.id,
|
|
||||||
fromPosition = reachedIndex,
|
|
||||||
toPosition = index - 1
|
|
||||||
)
|
|
||||||
} else if (index < reachedIndex) {
|
|
||||||
Database.decrementSongPositions(
|
|
||||||
playlistId = playlistWithSongs.playlist.id,
|
|
||||||
fromPosition = index + 1,
|
|
||||||
toPosition = reachedIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Database.update(
|
|
||||||
SongPlaylistMap(
|
|
||||||
songId = playlistWithSongs.songs[index].id,
|
|
||||||
playlistId = playlistWithSongs.playlist.id,
|
|
||||||
position = reachedIndex
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package it.vfsfitvnm.vimusic.ui.views
|
|||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -36,8 +37,10 @@ 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.animateItemPlacement
|
||||||
|
import it.vfsfitvnm.reordering.draggedItem
|
||||||
import it.vfsfitvnm.reordering.rememberReorderingState
|
import it.vfsfitvnm.reordering.rememberReorderingState
|
||||||
import it.vfsfitvnm.reordering.verticalDragAfterLongPressToReorder
|
import it.vfsfitvnm.reordering.verticalDragToReorder
|
||||||
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
|
||||||
@@ -56,6 +59,7 @@ import it.vfsfitvnm.vimusic.utils.rememberShouldBePlaying
|
|||||||
import it.vfsfitvnm.vimusic.utils.rememberWindows
|
import it.vfsfitvnm.vimusic.utils.rememberWindows
|
||||||
import it.vfsfitvnm.vimusic.utils.shuffleQueue
|
import it.vfsfitvnm.vimusic.utils.shuffleQueue
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun CurrentPlaylistView(
|
fun CurrentPlaylistView(
|
||||||
@@ -78,7 +82,22 @@ fun CurrentPlaylistView(
|
|||||||
val lazyListState =
|
val lazyListState =
|
||||||
rememberLazyListState(initialFirstVisibleItemIndex = mediaItemIndex)
|
rememberLazyListState(initialFirstVisibleItemIndex = mediaItemIndex)
|
||||||
|
|
||||||
val reorderingState = rememberReorderingState(windows)
|
val reorderingState = rememberReorderingState(
|
||||||
|
items = windows,
|
||||||
|
onDragStart = {
|
||||||
|
hapticFeedback.performHapticFeedback(
|
||||||
|
HapticFeedbackType.LongPress
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDragEnd = { fromIndex, toIndex ->
|
||||||
|
binder.player.moveMediaItem(fromIndex, toIndex)
|
||||||
|
},
|
||||||
|
itemSizeProvider = { index ->
|
||||||
|
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()
|
||||||
@@ -162,24 +181,20 @@ fun CurrentPlaylistView(
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {}
|
.clickable { }
|
||||||
|
.verticalDragToReorder(
|
||||||
|
reorderingState = reorderingState,
|
||||||
|
index = window.firstPeriodIndex
|
||||||
|
)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
.size(20.dp)
|
.size(20.dp)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// .animateItemPlacement()
|
.animateItemPlacement(reorderingState)
|
||||||
.verticalDragAfterLongPressToReorder(
|
.draggedItem(
|
||||||
reorderingState = reorderingState,
|
reorderingState = reorderingState,
|
||||||
index = window.firstPeriodIndex,
|
index = window.firstPeriodIndex
|
||||||
onDragStart = {
|
|
||||||
hapticFeedback.performHapticFeedback(
|
|
||||||
HapticFeedbackType.LongPress
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onDragEnd = { reachedIndex ->
|
|
||||||
binder.player.moveMediaItem(window.firstPeriodIndex, reachedIndex)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.views
|
package it.vfsfitvnm.vimusic.ui.views
|
||||||
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -17,6 +18,7 @@ import it.vfsfitvnm.vimusic.ui.components.BottomSheet
|
|||||||
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
|
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun PlayerBottomSheet(
|
fun PlayerBottomSheet(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.widget.Toast
|
|||||||
import androidx.activity.compose.LocalActivityResultRegistryOwner
|
import androidx.activity.compose.LocalActivityResultRegistryOwner
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -72,6 +73,7 @@ import it.vfsfitvnm.vimusic.utils.thumbnail
|
|||||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun PlayerView(
|
fun PlayerView(
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
|
freeCompilerArgs += "-Xcontext-receivers"
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package it.vfsfitvnm.reordering
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
context(LazyItemScope)
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
fun Modifier.animateItemPlacement(reorderingState: ReorderingState) =
|
||||||
|
if (reorderingState.draggingIndex == -1) animateItemPlacement() else this
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package it.vfsfitvnm.reordering
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
|
||||||
|
fun Modifier.draggedItem(
|
||||||
|
reorderingState: ReorderingState,
|
||||||
|
index: Int
|
||||||
|
): Modifier = composed {
|
||||||
|
val translation by reorderingState.translationFor(index)
|
||||||
|
|
||||||
|
offset {
|
||||||
|
when (reorderingState.orientation) {
|
||||||
|
Orientation.Vertical -> IntOffset(0, translation)
|
||||||
|
Orientation.Horizontal -> IntOffset(translation, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.zIndex(if (reorderingState.draggingIndex == index) 1f else 0f)
|
||||||
|
}
|
||||||
@@ -4,23 +4,38 @@ import androidx.compose.animation.core.Animatable
|
|||||||
import androidx.compose.animation.core.AnimationVector1D
|
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.runtime.*
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
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 kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
class ReorderingState(
|
class ReorderingState(
|
||||||
draggingIndexState: MutableState<Int>,
|
internal val itemSizeProvider: ((Int) -> Int?)?,
|
||||||
reachedIndexState: MutableState<Int>,
|
internal val coroutineScope: CoroutineScope,
|
||||||
draggingItemSizeState: MutableState<Int>,
|
|
||||||
internal val offset: Animatable<Int, AnimationVector1D>,
|
|
||||||
internal val lastIndex: Int,
|
internal val lastIndex: Int,
|
||||||
internal val areEquals: (Int, Int) -> Boolean
|
internal val areEquals: (Int, Int) -> Boolean,
|
||||||
|
internal val orientation: Orientation,
|
||||||
|
internal val onDragStart: () -> Unit,
|
||||||
|
internal val onDragEnd: (Int, Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
internal var draggingIndex by draggingIndexState
|
internal val offset: Animatable<Int, AnimationVector1D> = Animatable(0, Int.VectorConverter)
|
||||||
internal var reachedIndex by reachedIndexState
|
|
||||||
internal var draggingItemSize by draggingItemSizeState
|
internal var draggingIndex by mutableStateOf(-1)
|
||||||
|
internal var reachedIndex by mutableStateOf(-1)
|
||||||
|
internal var draggingItemSize by mutableStateOf(0)
|
||||||
|
|
||||||
|
private val noTranslation = object : State<Int> {
|
||||||
|
override val value = 0
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun translationFor(index: Int): State<Int> = when (draggingIndex) {
|
internal fun translationFor(index: Int): State<Int> = when (draggingIndex) {
|
||||||
-1 -> derivedStateOf { 0 }
|
-1 -> noTranslation
|
||||||
index -> offset.asState()
|
index -> offset.asState()
|
||||||
else -> animateIntAsState(
|
else -> animateIntAsState(
|
||||||
when (index) {
|
when (index) {
|
||||||
@@ -33,33 +48,24 @@ class ReorderingState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberReorderingState(items: List<Any>): ReorderingState {
|
fun rememberReorderingState(
|
||||||
val draggingIndexState = remember(items) {
|
items: List<Any>,
|
||||||
mutableStateOf(-1)
|
onDragEnd: (Int, Int) -> Unit,
|
||||||
}
|
onDragStart: () -> Unit = {},
|
||||||
|
orientation: Orientation = Orientation.Vertical,
|
||||||
val reachedIndexState = remember(items) {
|
itemSizeProvider: ((Int) -> Int?)? = null
|
||||||
mutableStateOf(-1)
|
): ReorderingState {
|
||||||
}
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val draggingItemHeightState = remember {
|
|
||||||
mutableStateOf(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val offset = remember(items) {
|
|
||||||
Animatable(0, Int.VectorConverter)
|
|
||||||
}
|
|
||||||
|
|
||||||
return remember(items) {
|
return remember(items) {
|
||||||
ReorderingState(
|
ReorderingState(
|
||||||
draggingIndexState = draggingIndexState,
|
itemSizeProvider = itemSizeProvider,
|
||||||
reachedIndexState = reachedIndexState,
|
coroutineScope = coroutineScope,
|
||||||
draggingItemSizeState = draggingItemHeightState,
|
orientation = orientation,
|
||||||
offset = offset,
|
|
||||||
lastIndex = items.lastIndex,
|
lastIndex = items.lastIndex,
|
||||||
areEquals = { i, j ->
|
areEquals = { i, j -> items[i] == items[j] },
|
||||||
items[i] == items[j]
|
onDragStart = onDragStart,
|
||||||
}
|
onDragEnd = onDragEnd,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,208 +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.foundation.layout.offset
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
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.PointerInputScope
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlin.reflect.KSuspendFunction5
|
|
||||||
|
|
||||||
private fun Modifier.dragToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
orientation: Orientation,
|
|
||||||
function: KSuspendFunction5<PointerInputScope, (Offset) -> Unit, () -> Unit, () -> Unit, (change: PointerInputChange, dragAmount: Offset) -> Unit, Unit>,
|
|
||||||
onDragStart: (() -> Unit)? = null,
|
|
||||||
onMove: (() -> Unit)? = null,
|
|
||||||
onDragEnd: ((Int) -> Unit)? = null
|
|
||||||
): Modifier = composed {
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val translation by reorderingState.translationFor(index)
|
|
||||||
|
|
||||||
pointerInput(reorderingState) {
|
|
||||||
// require(index in 0..reorderingState.lastIndex)
|
|
||||||
|
|
||||||
var previousItemSize = 0
|
|
||||||
var nextItemSize = 0
|
|
||||||
|
|
||||||
function(
|
|
||||||
this,
|
|
||||||
{
|
|
||||||
onDragStart?.invoke()
|
|
||||||
reorderingState.draggingIndex = index
|
|
||||||
reorderingState.reachedIndex = index
|
|
||||||
reorderingState.draggingItemSize = size.height
|
|
||||||
|
|
||||||
nextItemSize = reorderingState.draggingItemSize
|
|
||||||
previousItemSize = -reorderingState.draggingItemSize
|
|
||||||
|
|
||||||
reorderingState.offset.updateBounds(
|
|
||||||
lowerBound = -index * reorderingState.draggingItemSize,
|
|
||||||
upperBound = (reorderingState.lastIndex - index) * reorderingState.draggingItemSize
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
coroutineScope.launch {
|
|
||||||
reorderingState.offset.animateTo((previousItemSize + nextItemSize) / 2)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
onDragEnd?.invoke(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
|
|
||||||
onMove?.invoke()
|
|
||||||
}
|
|
||||||
} else if (targetOffset < previousItemSize) {
|
|
||||||
if (reorderingState.reachedIndex > 0) {
|
|
||||||
reorderingState.reachedIndex -= 1
|
|
||||||
previousItemSize -= reorderingState.draggingItemSize
|
|
||||||
nextItemSize -= reorderingState.draggingItemSize
|
|
||||||
onMove?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coroutineScope.launch {
|
|
||||||
reorderingState.offset.snapTo(targetOffset)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.offset {
|
|
||||||
when (orientation) {
|
|
||||||
Orientation.Vertical -> IntOffset(0, translation)
|
|
||||||
Orientation.Horizontal -> IntOffset(translation, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.zIndex(if (reorderingState.draggingIndex == index) 1f else 0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Modifier.dragToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
orientation: Orientation,
|
|
||||||
onDragStart: (() -> Unit)? = null,
|
|
||||||
onMove: (() -> Unit)? = null,
|
|
||||||
onDragEnd: ((Int) -> Unit)? = null
|
|
||||||
): Modifier = dragToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = orientation,
|
|
||||||
function = PointerInputScope::detectDragGestures,
|
|
||||||
onDragStart = onDragStart,
|
|
||||||
onMove = onMove,
|
|
||||||
onDragEnd = onDragEnd,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Modifier.verticalDragToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
onDragStart: (() -> Unit)? = null,
|
|
||||||
onMove: (() -> Unit)? = null,
|
|
||||||
onDragEnd: ((Int) -> Unit)? = null
|
|
||||||
): Modifier = dragToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
onDragStart = onDragStart,
|
|
||||||
onMove = onMove,
|
|
||||||
onDragEnd = onDragEnd,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Modifier.horizontalDragToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
onDragStart: (() -> Unit)? = null,
|
|
||||||
onMove: (() -> Unit)? = null,
|
|
||||||
onDragEnd: ((Int) -> Unit)? = null
|
|
||||||
): Modifier = dragToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = Orientation.Horizontal,
|
|
||||||
onDragStart = onDragStart,
|
|
||||||
onMove = onMove,
|
|
||||||
onDragEnd = onDragEnd,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Modifier.dragAfterLongPressToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
orientation: Orientation,
|
|
||||||
onDragStart: (() -> Unit)? = null,
|
|
||||||
onMove: (() -> Unit)? = null,
|
|
||||||
onDragEnd: ((Int) -> Unit)? = null
|
|
||||||
): Modifier = dragToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = orientation,
|
|
||||||
function = PointerInputScope::detectDragGesturesAfterLongPress,
|
|
||||||
onDragStart = onDragStart,
|
|
||||||
onMove = onMove,
|
|
||||||
onDragEnd = onDragEnd,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Modifier.verticalDragAfterLongPressToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
onDragStart: (() -> Unit)? = null,
|
|
||||||
onMove: (() -> Unit)? = null,
|
|
||||||
onDragEnd: ((Int) -> Unit)? = null
|
|
||||||
): Modifier = dragAfterLongPressToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
onDragStart = onDragStart,
|
|
||||||
onMove = onMove,
|
|
||||||
onDragEnd = onDragEnd,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Modifier.horizontalDragAfterLongPressToReorder(
|
|
||||||
reorderingState: ReorderingState,
|
|
||||||
index: Int,
|
|
||||||
onDragStart: (() -> Unit)? = null,
|
|
||||||
onMove: (() -> Unit)? = null,
|
|
||||||
onDragEnd: ((Int) -> Unit)? = null
|
|
||||||
): Modifier = dragAfterLongPressToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
orientation = Orientation.Horizontal,
|
|
||||||
onDragStart = onDragStart,
|
|
||||||
onMove = onMove,
|
|
||||||
onDragEnd = onDragEnd,
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user