繁体   English   中英

通过拖放重新排序 LazyColumn 项目

[英]Reorder LazyColumn items with drag & drop

我想创建一个包含可以通过拖放重新排序的项目的LazyColumn 如果没有撰写,我的方法是使用ItemTouchHelper.SimpleCallback ,但我还没有找到类似的撰写。

我尝试过使用Modifier.longPressDragGestureFilterModifier.draggable ,但这仅允许我使用偏移量拖动卡片。 它没有给我一个列表索引(如fromPosition ItemTouchHelper.SimpleCallback toPosition ,我需要交换列表中的项目。

是否有相当于ItemTouchHelper.SimpleCallbackonMove function 的组合? 如果没有,这是计划中的功能吗?

自己尝试和实施这种事情是否可能/可行?

到目前为止,我可以告诉 Compose 还没有提供处理这个问题的方法,尽管我假设这将在工作中,因为他们已经添加了draggable 和 longPressDragGestureFilter 修饰符,正如您已经提到的。 因为他们已经添加了这些,也许这是在惰性列中拖放的前兆。

2021 年 2 月,谷歌提出了一个问题,他们的回应是他们不会为 1.0 版本提供官方解决方案,尽管在此期间他们提供了一些有关如何处理解决方案的指导。 现在看起来最好的解决方案是使用 ItemTouchHelper 使用 RecyclerView。

这是提到的问题: https : //issuetracker.google.com/issues/181282427

可以使用detectDragGesturesAfterLongPressrememberLazyListState构建一个简单(不完美)的可重排序列表。

基本思想是向 LazyColumn 添加拖动手势修改器并检测我们自己的拖动项,而不是为每个项添加修改器。

   val listState: LazyListState = rememberLazyListState()
   ...
   LazyColumn(
        state = listState,
        modifier = Modifier.pointerInput(Unit) {
            detectDragGesturesAfterLongPress(....)

使用 LazyListState 提供的 layoutInfo 查找项目:

var position by remember {
    mutableStateOf<Float?>(null)
}
...
onDragStart = { offset ->
    listState.layoutInfo.visibleItemsInfo
        .firstOrNull { offset.y.toInt() in it.offset..it.offset + it.size }
        ?.also {
            position = it.offset + it.size / 2f
        }
}

更新每次拖动的位置:

onDrag = { change, dragAmount ->
    change.consumeAllChanges()
    position = position?.plus(dragAmount.y)
    // Start autoscrolling if position is out of bounds
}

为了在滚动时支持重新排序,我们不能在 onDrag 中进行onDrag 为此,我们创建了一个流程来在每个位置/滚动更新中找到最近的项目:

var draggedItem by remember {
    mutableStateOf<Int?>(null)
}
....
snapshotFlow { listState.layoutInfo }
.combine(snapshotFlow { position }.distinctUntilChanged()) { state, pos ->
    pos?.let { draggedCenter ->
        state.visibleItemsInfo
            .minByOrNull { (draggedCenter - (it.offset + it.size / 2f)).absoluteValue }
    }?.index
}
.distinctUntilChanged()
.collect { near -> ...}

更新拖动的项目索引并移动 MutableStateList 中的项目。

draggedItem = when {
    near == null -> null
    draggedItem == null -> near
    else -> near.also { items.move(draggedItem, it) }
}

fun <T> MutableList<T>.move(fromIdx: Int, toIdx: Int) {
    if (toIdx > fromIdx) {
        for (i in fromIdx until toIdx) {
            this[i] = this[i + 1].also { this[i + 1] = this[i] }
        }
    } else {
        for (i in fromIdx downTo toIdx + 1) {
            this[i] = this[i - 1].also { this[i - 1] = this[i] }
        }
    }
}

计算相对项目偏移量:

val indexWithOffset by derivedStateOf {
    draggedItem
        ?.let { listState.layoutInfo.visibleItemsInfo.getOrNull(it - listState.firstVisibleItemIndex) }
        ?.let { Pair(it.index, (position ?: 0f) - it.offset - it.size / 2f) }
}

然后可以用于将偏移应用于拖动的项目(不要使用项目键!):

itemsIndexed(items) { idx, item ->
    val offset by remember {
        derivedStateOf { state.indexWithOffset?.takeIf { it.first == idx }?.second }
    }
    Column(
        modifier = Modifier
            .zIndex(offset?.let { 1f } ?: 0f)
            .graphicsLayer {
                translationY = offset ?: 0f
            }
    )
        ....
}

可以在此处找到示例实现

基于谷歌示例和 Medium 中的一些帖子,我想出了这个实现:

DragDropColumn.kt ->

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun <T : Any> DragDropColumn(
    items: List<T>,
    onSwap: (Int, Int) -> Unit,
    itemContent: @Composable LazyItemScope.(item: T) -> Unit
) {
    var overscrollJob by remember { mutableStateOf<Job?>(null) }
    val listState = rememberLazyListState()
    val scope = rememberCoroutineScope()
    val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex ->
        onSwap(fromIndex, toIndex)
    }

    LazyColumn(
        modifier = Modifier
            .pointerInput(dragDropState) {
                detectDragGesturesAfterLongPress(
                    onDrag = { change, offset ->
                        change.consume()
                        dragDropState.onDrag(offset = offset)

                        if (overscrollJob?.isActive == true)
                            return@detectDragGesturesAfterLongPress

                        dragDropState
                            .checkForOverScroll()
                            .takeIf { it != 0f }
                            ?.let {
                                overscrollJob =
                                    scope.launch {
                                        dragDropState.state.animateScrollBy(
                                            it*1.3f, tween(easing = FastOutLinearInEasing)
                                        )
                                    }
                            }
                            ?: run { overscrollJob?.cancel() }
                    },
                    onDragStart = { offset -> dragDropState.onDragStart(offset) },
                    onDragEnd = {
                        dragDropState.onDragInterrupted()
                        overscrollJob?.cancel()
                    },
                    onDragCancel = {
                        dragDropState.onDragInterrupted()
                        overscrollJob?.cancel()
                    }
                )
            },
        state = listState,
        contentPadding = PaddingValues(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        itemsIndexed(items = items) { index, item ->
            DraggableItem(
                dragDropState = dragDropState,
                index = index
            ) { isDragging ->
                val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
                Card(elevation = elevation) {
                    itemContent(item)
                }
            }
        }
    }
}

DragDropState.kt ->

class DragDropState internal constructor(
    val state: LazyListState,
    private val scope: CoroutineScope,
    private val onSwap: (Int, Int) -> Unit
) {
    private var draggedDistance by mutableStateOf(0f)
    private var draggingItemInitialOffset by mutableStateOf(0)
    internal val draggingItemOffset: Float
        get() = draggingItemLayoutInfo?.let { item ->
            draggingItemInitialOffset + draggedDistance - item.offset
        } ?: 0f
    private val draggingItemLayoutInfo: LazyListItemInfo?
        get() = state.layoutInfo.visibleItemsInfo
            .firstOrNull { it.index == currentIndexOfDraggedItem }

    internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
        private set
    internal var previousItemOffset = Animatable(0f)
        private set

    // used to obtain initial offsets on drag start
    private var initiallyDraggedElement by mutableStateOf<LazyListItemInfo?>(null)
    
    var currentIndexOfDraggedItem by mutableStateOf<Int?>(null)

    private val initialOffsets: Pair<Int, Int>?
        get() = initiallyDraggedElement?.let { Pair(it.offset, it.offsetEnd) }

    private val currentElement: LazyListItemInfo?
        get() = currentIndexOfDraggedItem?.let {
            state.getVisibleItemInfoFor(absoluteIndex = it)
        }


    fun onDragStart(offset: Offset) {
        state.layoutInfo.visibleItemsInfo
            .firstOrNull { item -> offset.y.toInt() in item.offset..(item.offset + item.size) }
            ?.also {
                currentIndexOfDraggedItem = it.index
                initiallyDraggedElement = it
                draggingItemInitialOffset = it.offset
            }
    }
    
    fun onDragInterrupted() {
        if (currentIndexOfDraggedItem != null) {
            previousIndexOfDraggedItem = currentIndexOfDraggedItem
           // val startOffset = draggingItemOffset
            scope.launch {
                //previousItemOffset.snapTo(startOffset)
                previousItemOffset.animateTo(
                    0f,
                    tween(easing = FastOutLinearInEasing)
                )
                previousIndexOfDraggedItem = null
            }
        }
        draggingItemInitialOffset = 0
        draggedDistance = 0f
        currentIndexOfDraggedItem = null
        initiallyDraggedElement = null
    }
    
    fun onDrag(offset: Offset) {
        draggedDistance += offset.y

        initialOffsets?.let { (topOffset, bottomOffset) ->
            val startOffset = topOffset + draggedDistance
            val endOffset = bottomOffset + draggedDistance

            currentElement?.let { hovered ->
                state.layoutInfo.visibleItemsInfo
                    .filterNot { item -> item.offsetEnd < startOffset || item.offset > endOffset || hovered.index == item.index }
                    .firstOrNull { item ->
                        val delta = (startOffset - hovered.offset)
                        when {
                            delta > 0 -> (endOffset > item.offsetEnd)
                            else -> (startOffset < item.offset)
                        }
                    }
                    ?.also { item ->
                        currentIndexOfDraggedItem?.let { current ->
                            scope.launch {
                                onSwap.invoke(
                                    current,
                                    item.index
                                )
                            }
                        }
                        currentIndexOfDraggedItem = item.index
                    }
            }
        }
    }

    fun checkForOverScroll(): Float {
        return initiallyDraggedElement?.let {
            val startOffset = it.offset + draggedDistance
            val endOffset = it.offsetEnd + draggedDistance
            return@let when {
                draggedDistance > 0 -> (endOffset - state.layoutInfo.viewportEndOffset+50f).takeIf { diff -> diff > 0 }
                draggedDistance < 0 -> (startOffset - state.layoutInfo.viewportStartOffset-50f).takeIf { diff -> diff < 0 }
                else -> null
            }
        } ?: 0f
    }
}

DragDropExt.kt ->

@Composable
fun rememberDragDropState(
    lazyListState: LazyListState,
    onSwap: (Int, Int) -> Unit
): DragDropState {
    val scope = rememberCoroutineScope()
    val state = remember(lazyListState) {
        DragDropState(
            state = lazyListState,
            onMove = onSwap,
            scope = scope
        )
    }
    return state
}

fun LazyListState.getVisibleItemInfoFor(absoluteIndex: Int): LazyListItemInfo? {
    return this
        .layoutInfo
        .visibleItemsInfo
        .getOrNull(absoluteIndex - this.layoutInfo.visibleItemsInfo.first().index)
}

val LazyListItemInfo.offsetEnd: Int
    get() = this.offset + this.size

@ExperimentalFoundationApi
@Composable
fun LazyItemScope.DraggableItem(
    dragDropState: DragDropState,
    index: Int,
    modifier: Modifier,
    content: @Composable ColumnScope.(isDragging: Boolean) -> Unit
) {
    val current: Float by animateFloatAsState(dragDropState.draggingItemOffset * 0.67f)
    val previous: Float by animateFloatAsState(dragDropState.previousItemOffset.value * 0.67f)
    val dragging = index == dragDropState.currentIndexOfDraggedItem
    val draggingModifier = if (dragging) {
        Modifier
            .zIndex(1f)
            .graphicsLayer {
                translationY = current
            }
    } else if (index == dragDropState.previousIndexOfDraggedItem) {
        Modifier
            .zIndex(1f)
            .graphicsLayer {
                translationY = previous
            }
    } else {
        Modifier.animateItemPlacement(
            tween(easing = FastOutLinearInEasing)
        )
    }
    Column(modifier = modifier.then(draggingModifier)) {
        content(dragging)
    }
}

使用它看起来像:

@Composable
fun SectionListUI() {

    val viewModel: SectionListViewModel = hiltViewModel()
    val uiState by viewModel.uiState.collectAsState()

    DragDropColumn(items = uiState.sections, onSwap = viewModel::swapSections) { item ->
        Card(
            modifier = Modifier
            .clickable { viewModel.sectionClick(item) },
        ) {
            Text(
                text = item.title,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)
            )
        }
    }
}

此实现非常适合我的用例。但是,请随时发表评论并进行改进

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM