Improve compose-reordering code
This commit is contained in:
@@ -37,6 +37,7 @@ android {
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += "-Xcontext-receivers"
|
||||
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.VectorConverter
|
||||
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(
|
||||
draggingIndexState: MutableState<Int>,
|
||||
reachedIndexState: MutableState<Int>,
|
||||
draggingItemSizeState: MutableState<Int>,
|
||||
internal val offset: Animatable<Int, AnimationVector1D>,
|
||||
internal val itemSizeProvider: ((Int) -> Int?)?,
|
||||
internal val coroutineScope: CoroutineScope,
|
||||
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 var reachedIndex by reachedIndexState
|
||||
internal var draggingItemSize by draggingItemSizeState
|
||||
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 val noTranslation = object : State<Int> {
|
||||
override val value = 0
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun translationFor(index: Int): State<Int> = when (draggingIndex) {
|
||||
-1 -> derivedStateOf { 0 }
|
||||
-1 -> noTranslation
|
||||
index -> offset.asState()
|
||||
else -> animateIntAsState(
|
||||
when (index) {
|
||||
@@ -33,33 +48,24 @@ class ReorderingState(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberReorderingState(items: List<Any>): ReorderingState {
|
||||
val draggingIndexState = remember(items) {
|
||||
mutableStateOf(-1)
|
||||
}
|
||||
|
||||
val reachedIndexState = remember(items) {
|
||||
mutableStateOf(-1)
|
||||
}
|
||||
|
||||
val draggingItemHeightState = remember {
|
||||
mutableStateOf(0)
|
||||
}
|
||||
|
||||
val offset = remember(items) {
|
||||
Animatable(0, Int.VectorConverter)
|
||||
}
|
||||
fun rememberReorderingState(
|
||||
items: List<Any>,
|
||||
onDragEnd: (Int, Int) -> Unit,
|
||||
onDragStart: () -> Unit = {},
|
||||
orientation: Orientation = Orientation.Vertical,
|
||||
itemSizeProvider: ((Int) -> Int?)? = null
|
||||
): ReorderingState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
return remember(items) {
|
||||
ReorderingState(
|
||||
draggingIndexState = draggingIndexState,
|
||||
reachedIndexState = reachedIndexState,
|
||||
draggingItemSizeState = draggingItemHeightState,
|
||||
offset = offset,
|
||||
itemSizeProvider = itemSizeProvider,
|
||||
coroutineScope = coroutineScope,
|
||||
orientation = orientation,
|
||||
lastIndex = items.lastIndex,
|
||||
areEquals = { i, j ->
|
||||
items[i] == items[j]
|
||||
}
|
||||
areEquals = { i, 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