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

Other Decks in Programming

Transcript

  1. Drag & Drop In LazyColumn
    @gotlinan
    2023-11-10 Shibuya.apk #45

    View full-size slide

  2. ⾃⼰紹介
    名前:⻑⾕川 剛太
    所属:KINTOテクノロジーズ株式会社
    :Androidエンジニア
    :myrouteを開発中
    X(旧twitter):@gotlinan
    趣味:バイク(ツーリング)
    最近⼤型免許
    取りました

    View full-size slide

  3. 実装すること
    • アイテムをD&D
    • アイテムのスイッチ
    • 端に到達でスクロール
    https://github.com/goutarouh/DragDropInLazy

    View full-size slide

  4. 前提 表⽰されているアイテムの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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  10. item2
    Item1
    Item0
    Item0を⼀番上から下⽅向にD&D
    Item2と切り替わった時
    ドラッグしたときに取得できる値は?

    View full-size slide

  11. item2
    Item1
    Item0
    Item0を⼀番上から下⽅向にD&D
    取得できるDrag距離
    ドラッグしたときに取得できる値は?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  23. ドラッグ中の処理
    class LazyColumnDragDropState(val lazyListState: LazyListState) {
    var draggedDistance by mutableStateOf(0f)

    fun onDrag(dragAmount: Float) {
    draggedDistance += dragAmount
    switchItemIfNeed() // アイテム交換処理
    scrollIfNeed() // スクロール処理
    }
    }

    View full-size slide

  24. 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をドラッグ中



    View full-size slide

  25. 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をドラッグ中



    View full-size slide

  26. 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をドラッグ中



    View full-size slide

  27. 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をドラッグ中



    View full-size slide

  28. スクロール処理
    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

    View full-size slide

  29. スクロール処理
    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

    View full-size slide

  30. スクロール処理
    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

    View full-size slide

  31. スクロール処理
    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

    View full-size slide

  32. スクロール処理
    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

    View full-size slide

  33. まとめ😄
    • どうしてアイテム毎に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

    View full-size slide