Noa*_*oah 12 android android-jetpack-compose
我想创建一个LazyColumn可以通过拖放重新排序的项目。如果没有 compose,我的方法是使用ItemTouchHelper.SimpleCallback,但我还没有找到类似的东西用于 compose。
我试过使用Modifier.longPressDragGestureFilterand Modifier.draggable,但这仅允许我使用偏移量拖动卡片。它没有给我一个列表索引(比如fromPosition/ toPositionin ItemTouchHelper.SimpleCallback),我需要交换列表中的项目。
有没有撰写相当于ItemTouchHelper.SimpleCallback的onMove功能?如果不是,它是计划中的功能吗?
自己尝试实施这种事情是否可能/可行?
Jam*_*nny 10
到目前为止,我可以告诉 Compose 还没有提供处理这个问题的方法,尽管我假设这将在工作中,因为他们已经添加了draggable 和 longPressDragGestureFilter 修饰符,正如您已经提到的。因为他们已经添加了这些,也许这是在惰性列中拖放的前兆。
2021 年 2 月,谷歌提出了一个问题,他们的回应是他们不会为 1.0 版本提供官方解决方案,尽管在此期间他们提供了一些有关如何处理解决方案的指导。现在看起来最好的解决方案是使用 ItemTouchHelper 使用 RecyclerView。
这是提到的问题:https : //issuetracker.google.com/issues/181282427
Jur*_*lja 10
根据谷歌样本和 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,
modifier = Modifier
) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
Card(elevation = CardDefaults.cardElevation(defaultElevation = elevation)) {
itemContent(item)
}
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
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
}
}
Run Code Online (Sandbox Code Playgroud)
DragDropExt.kt ->
@Composable
fun rememberDragDropState(
lazyListState: LazyListState,
onSwap: (Int, Int) -> Unit
): DragDropState {
val scope = rememberCoroutineScope()
val state = remember(lazyListState) {
DragDropState(
state = lazyListState,
onSwap = 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)
}
}
Run Code Online (Sandbox Code Playgroud)
使用它看起来像:
@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)
)
}
}
}
Run Code Online (Sandbox Code Playgroud)
这个实现非常适合我的用例。但是,请随意发表评论并进行改进
可以使用detectDragGesturesAfterLongPressand构建一个简单(不完美)的可重排序列表rememberLazyListState。
基本思想是向 LazyColumn 添加拖动手势修改器并检测我们自己的拖动项,而不是为每个项添加修改器。
val listState: LazyListState = rememberLazyListState()
...
LazyColumn(
state = listState,
modifier = Modifier.pointerInput(Unit) {
detectDragGesturesAfterLongPress(....)
Run Code Online (Sandbox Code Playgroud)
使用 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
}
}
Run Code Online (Sandbox Code Playgroud)
更新每次拖动的位置:
onDrag = { change, dragAmount ->
change.consumeAllChanges()
position = position?.plus(dragAmount.y)
// Start autoscrolling if position is out of bounds
}
Run Code Online (Sandbox Code Playgroud)
为了在滚动时支持重新排序,我们不能在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 -> ...}
Run Code Online (Sandbox Code Playgroud)
更新拖动的项目索引并移动 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] }
}
}
}
Run Code Online (Sandbox Code Playgroud)
计算相对项目偏移量:
val indexWithOffset by derivedStateOf {
draggedItem
?.let { listState.layoutInfo.visibleItemsInfo.getOrNull(it - listState.firstVisibleItemIndex) }
?.let { Pair(it.index, (position ?: 0f) - it.offset - it.size / 2f) }
}
Run Code Online (Sandbox Code Playgroud)
然后可以用于将偏移应用于拖动的项目(不要使用项目键!):
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
}
)
....
}
Run Code Online (Sandbox Code Playgroud)
可以在此处找到示例实现
| 归档时间: |
|
| 查看次数: |
1434 次 |
| 最近记录: |