簡體   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