Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Drag & Drop in LazyColumn

gotlin
November 10, 2023

Drag & Drop in LazyColumn

Shibuya.apk #45で発表したLTのスライドです。

https://shibuya-apk.connpass.com/event/299317/presentation/

gotlin

November 10, 2023
Tweet

More Decks by gotlin

Other Decks in Programming

Transcript

  1. 前提 表⽰されているアイテムのoffsetとsizeを取得可能 item1 Item0 画⾯全体のLazyColumnで 並ぶアイテム item3 item2 offset size

    interface LazyListItemInfo { /* アイテムの開始点からの距離 */ val offset: Int /* アイテムの幅 */ val size: Int … } val LazyListItemInfo.offsetEnd: Int get() = offset + size offsetEnd
  2. 実装の概要 LazyColumn { itemsIndexed(items) { index, item -> DragDropItem( item

    = item, modifier = Modifier .pointerInput(Unit) { detectDragGesturesAfterLongPress(…) } .graphicsLayer { translationY = Dragに応じて更新するべき値 (※ドラッグ中のアイテムのみ) } ) } }
  3. item2 Item1 Item0を⼀番上から 下⽅向にD&D item4 Item0 実装の概要 LazyColumn { itemsIndexed(items)

    { index, item -> DragDropItem( item = item, modifier = Modifier .pointerInput(Unit) { detectDragGesturesAfterLongPress(…) } .graphicsLayer { translationY = Dragに応じて更新するべき値 (※ドラッグ中のアイテムのみ) } ) } }
  4. item2 Item1 Item0を⼀番上から 下⽅向にD&D item4 Item0 LazyColumn { itemsIndexed(items) {

    index, item -> DragDropItem( item = item, modifier = Modifier .pointerInput(Unit) { detectDragGesturesAfterLongPress(…) } .graphicsLayer { translationY = Dragに応じて更新するべき値 (※ドラッグ中のアイテムのみ) } ) } } 実装の概要
  5. ドラッグしたときに取得できる値は? item0 Item1 Item2 画⾯全体のLazyColumnで 並ぶアイテム LazyColumn { itemsIndexed(items) {

    index, item -> DragDropItem( item = item, modifier = Modifier .pointerInput(Unit) { detectDragGesturesAfterLongPress(…) } .graphicsLayer { translationY = Dragに応じて更新するべき値 (※ドラッグ中のアイテムのみ) } ) } }
  6. item0 Item1 Item2 LazyColumn { itemsIndexed(items) { index, item ->

    DragDropItem( item = item, modifier = Modifier .pointerInput(Unit) { detectDragGesturesAfterLongPress(…) } .graphicsLayer { translationY = Dragに応じて更新するべき値 (※ドラッグ中のアイテムのみ) } ) } } 各アイテムがDragModifierを持つ! 画⾯全体のLazyColumnで 並ぶアイテム ドラッグしたときに取得できる値は?
  7. item2 Item1 Item0を⼀番上から 下⽅向にD&D item4 Item0 求めたい値 ① Drag距離 ②ドラッグを開始し

    たアイテムのoffset ドラッグを開始したアイテムからのドラッグ距離
  8. item2 Item1 Item0を⼀番上から 下⽅向にD&D item4 Item0 求めたい値 ① Drag距離 ②ドラッグを開始し

    たアイテムのoffset ③操作中の アイテムのoffset ドラッグを開始したアイテムからのドラッグ距離
  9. item2 Item1 Item0を⼀番上から 下⽅向にD&D ① Drag距離 ②ドラッグを開始し たアイテムのoffset ③操作中の アイテムのoffset

    item4 Item0 求めたい値 求めたい値 = ① + ② - ③ ドラッグを開始したアイテムからのドラッグ距離
  10. 必要な状態の管理 class LazyColumnDragDropState(val lazyListState: LazyListState) { var draggedDistance by mutableStateOf(0f)

    var draggingItemIndex by mutableStateOf<Int?>(null) var initialDraggedItem: LazyListItemInfo? = null val draggedItemY: Float get() { val draggedItemOffset = lazyListState.findVisibleItemInfoByIndex(draggingItemIndex)?.offset ?: 0 return draggedDistance + (initialDraggedItem?.offset ?: 0f).toFloat() - draggedItemOffset } }
  11. 必要な状態の管理 class LazyColumnDragDropState(val lazyListState: LazyListState) { var draggedDistance by mutableStateOf(0f)

    var draggingItemIndex by mutableStateOf<Int?>(null) var initialDraggedItem: LazyListItemInfo? = null val draggedItemY: Float get() { val draggedItemOffset = lazyListState.findVisibleItemInfoByIndex(draggingItemIndex)?.offset ?: 0 return draggedDistance + (initialDraggedItem?.offset ?: 0f).toFloat() - draggedItemOffset } }
  12. 必要な状態の管理 class LazyColumnDragDropState(val lazyListState: LazyListState) { var draggedDistance by mutableStateOf(0f)

    var draggingItemIndex by mutableStateOf<Int?>(null) var initialDraggedItem: LazyListItemInfo? = null val draggedItemY: Float get() { val draggedItemOffset = lazyListState.findVisibleItemInfoByIndex(draggingItemIndex)?.offset ?: 0 return draggedDistance + (initialDraggedItem?.offset ?: 0f).toFloat() - draggedItemOffset } }
  13. 必要な状態の管理 class LazyColumnDragDropState(val lazyListState: LazyListState) { var draggedDistance by mutableStateOf(0f)

    var draggingItemIndex by mutableStateOf<Int?>(null) var initialDraggedItem: LazyListItemInfo? = null val draggedItemY: Float get() { val draggedItemOffset = lazyListState.findVisibleItemInfoByIndex(draggingItemIndex)?.offset ?: 0 return draggedDistance + (initialDraggedItem?.offset ?: 0f).toFloat() - draggedItemOffset } }
  14. 必要な状態の管理 class LazyColumnDragDropState(val lazyListState: LazyListState) { var draggedDistance by mutableStateOf(0f)

    var draggingItemIndex by mutableStateOf<Int?>(null) var initialDraggedItem: LazyListItemInfo? = null val draggedItemY: Float get() { val draggedItemOffset = lazyListState.findVisibleItemInfoByIndex(draggingItemIndex)?.offset ?: 0 return draggedDistance + (initialDraggedItem?.offset ?: 0f).toFloat() - draggedItemOffset } }
  15. 必要な状態の管理 class LazyColumnDragDropState(val lazyListState: LazyListState) { var draggedDistance by mutableStateOf(0f)

    var draggingItemIndex by mutableStateOf<Int?>(null) var initialDraggedItem: LazyListItemInfo? = null val draggedItemY: Float get() { val draggedItemOffset = lazyListState.findVisibleItemInfoByIndex(draggingItemIndex)?.offset ?: 0 return draggedDistance + (initialDraggedItem?.offset ?: 0f).toFloat() - draggedItemOffset } } ① ② ③
  16. ドラッグ中の処理 class LazyColumnDragDropState(val lazyListState: LazyListState) { var draggedDistance by mutableStateOf(0f)

    … fun onDrag(dragAmount: Float) { draggedDistance += dragAmount switchItemIfNeed() // アイテム交換処理 scrollIfNeed() // スクロール処理 } }
  17. private fun findSwitchItem( draggedItemStartOffset: Float, draggedItemEndOffset: Float, currentItemOffset: Float ):

    LazyListItemInfo? { return lazyListState.layoutInfo.visibleItemsInfo .filterNot { item -> item.offsetEnd < draggedItemStartOffset || item.offset > draggedItemEndOffset || draggedItemIndex == item.index } .firstOrNull { item -> val delta = draggedItemStartOffset - currentItem.offset when { delta > 0 -> (draggedItemEndOffset > currentItemOffset) else -> (draggedItemStartOffset < item.offset) } } } アイテム交換処理 ② ① ③ Item0 Item2 item1 Item1をドラッグ中 ③ ① ②
  18. private fun findSwitchItem( draggedItemStartOffset: Float, draggedItemEndOffset: Float, currentItemOffset: Float ):

    LazyListItemInfo? { return lazyListState.layoutInfo.visibleItemsInfo .filterNot { item -> item.offsetEnd < draggedItemStartOffset || item.offset > draggedItemEndOffset || draggedItemIndex == item.index } .firstOrNull { item -> val delta = draggedItemStartOffset - currentItem.offset when { delta > 0 -> (draggedItemEndOffset > currentItemOffset) else -> (draggedItemStartOffset < item.offset) } } } アイテム交換処理 ② ① ③ Item0 Item2 item1 Item1をドラッグ中 ③ ① ②
  19. private fun findSwitchItem( draggedItemStartOffset: Float, draggedItemEndOffset: Float, currentItemOffset: Float ):

    LazyListItemInfo? { return lazyListState.layoutInfo.visibleItemsInfo .filterNot { item -> item.offsetEnd < draggedItemStartOffset || item.offset > draggedItemEndOffset || draggedItemIndex == item.index } .firstOrNull { item -> val delta = draggedItemStartOffset - currentItem.offset when { delta > 0 -> (draggedItemEndOffset > item.offsetEnd) else -> (draggedItemStartOffset < item.offset) } } } アイテム交換処理 ② ① ③ Item0 Item2 item1 Item1をドラッグ中 ③ ① ②
  20. private fun findSwitchItem( draggedItemStartOffset: Float, draggedItemEndOffset: Float, currentItemOffset: Float ):

    LazyListItemInfo? { return lazyListState.layoutInfo.visibleItemsInfo .filterNot { item -> item.offsetEnd < draggedItemStartOffset || item.offset > draggedItemEndOffset || draggedItemIndex == item.index } .firstOrNull { item -> val delta = draggedItemStartOffset - currentItem.offset when { delta > 0 -> (draggedItemEndOffset > item.offsetEnd) else -> (draggedItemStartOffset < item.offset) } } } アイテム交換処理 ② ① ③ Item0 Item2 item1 Item1をドラッグ中 ③ ① ②
  21. スクロール処理 Item1 Item3 item2 Item0を上端からドラッグ中 Item4 Item5 Item6 Item7 fun

    calculateScrollAmount( draggedItemStartOffset: Float, draggedItemEndOffset: Float ): Float { val viewportEndOffset = lazyListState.layoutInfo.viewportEndOffset val viewportStartOffset = lazyListState.layoutInfo.viewportStartOffset return when { draggedDistance > 0 -> (draggedItemEndOffset - viewportEndOffset).takeIf { diff -> diff > 0 } draggedDistance < 0 -> (draggedItemStartOffset - viewportStartOffset).takeIf { diff -> diff < 0 } else -> null } ?: 0f } lazyListState.scrollBy(calculateScrollAmount()) Item0
  22. スクロール処理 Item1 Item3 item2 Item0を上端からドラッグ中 Item4 Item5 Item6 Item7 fun

    calculateScrollAmount( draggedItemStartOffset: Float, draggedItemEndOffset: Float ): Float { val viewportEndOffset = lazyListState.layoutInfo.viewportEndOffset val viewportStartOffset = lazyListState.layoutInfo.viewportStartOffset return when { draggedDistance > 0 -> (draggedItemEndOffset - viewportEndOffset).takeIf { diff -> diff > 0 } draggedDistance < 0 -> (draggedItemStartOffset - viewportStartOffset).takeIf { diff -> diff < 0 } else -> null } ?: 0f } lazyListState.scrollBy(calculateScrollAmount()) スクロール量 Item0
  23. スクロール処理 Item1 Item3 item2 Item0を上端からドラッグ中 Item4 Item5 Item6 Item7 fun

    calculateScrollAmount( draggedItemStartOffset: Float, draggedItemEndOffset: Float ): Float { val viewportEndOffset = lazyListState.layoutInfo.viewportEndOffset val viewportStartOffset = lazyListState.layoutInfo.viewportStartOffset return when { draggedDistance > 0 -> (draggedItemEndOffset - viewportEndOffset).takeIf { diff -> diff > 0 } draggedDistance < 0 -> (draggedItemStartOffset - viewportStartOffset).takeIf { diff -> diff < 0 } else -> null } ?: 0f } lazyListState.scrollBy(calculateScrollAmount()) スクロール量 Item0
  24. スクロール処理 Item1 Item3 item2 Item0を上端からドラッグ中 Item4 Item5 Item6 Item7 fun

    calculateScrollAmount( draggedItemStartOffset: Float, draggedItemEndOffset: Float ): Float { val viewportEndOffset = lazyListState.layoutInfo.viewportEndOffset val viewportStartOffset = lazyListState.layoutInfo.viewportStartOffset return when { draggedDistance > 0 -> (draggedItemEndOffset - viewportEndOffset).takeIf { diff -> diff > 0 } draggedDistance < 0 -> (draggedItemStartOffset - viewportStartOffset).takeIf { diff -> diff < 0 } else -> null } ?: 0f } lazyListState.scrollBy(calculateScrollAmount()) スクロール量 Item0
  25. スクロール処理 Item1 Item3 item2 Item0を上端からドラッグ中 Item4 Item5 Item6 Item7 fun

    calculateScrollAmount( draggedItemStartOffset: Float, draggedItemEndOffset: Float ): Float { val viewportEndOffset = lazyListState.layoutInfo.viewportEndOffset val viewportStartOffset = lazyListState.layoutInfo.viewportStartOffset return when { draggedDistance > 0 -> (draggedItemEndOffset - viewportEndOffset).takeIf { diff -> diff > 0 } draggedDistance < 0 -> (draggedItemStartOffset - viewportStartOffset).takeIf { diff -> diff < 0 } else -> null } ?: 0f } lazyListState.scrollBy(calculateScrollAmount()) スクロール量 Item0
  26. まとめ😄 • どうしてアイテム毎にDragModifierを定義したの? • 各アイテムの⼀部分がDrag可能だったため(業務開発中のアプリ) • Aclassen/ComposeReorderableというライブラリも発⾒した • LazyRowやLazyGridにも対応 •

    LazyLayout全体にDragModifierを定義しています。 • 発表した内容以外にも、もう少し必要な実装があります。 • PinnableContainer (Compose 1.4~) • スクロールのコルーチンjobの管理 • Modifier.Nodeを使⽤ or Compose1.5~でパフォーマンス向上 Item1 Item0 Item2 Item3 Item4 Item5 Item0 Item1 Item2 Item3 Item4 Item5 今回の実装 各アイテムがDragModifierを持つ 紹介したライブラリの実装 LazyColumnがDragModifierを持つ アイテムの⼀部分がDrag可能な場合 参考 • https://developer.android.com/jetpack/compose/touch-input • https://www.youtube.com/watch?v=1tkVjBxdGrk