Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7b5de42c authored by William Leshner's avatar William Leshner Committed by Android (Google) Code Review
Browse files

Merge "Fix drag reordering on mobile." into main

parents 42631942 91e3b02d
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -77,6 +77,16 @@ internal constructor(
        list.apply { add(toIndex, removeAt(fromIndex)) }
    }

    /** Swap the two items in the list with the given indices. */
    fun swapItems(index1: Int, index2: Int) {
        list.apply {
            val item1 = get(index1)
            val item2 = get(index2)
            set(index2, item1)
            set(index1, item2)
        }
    }

    /** Remove widget from the list and the database. */
    fun onRemove(indexToRemove: Int) {
        if (list[indexToRemove].isWidgetContent()) {
+266 −22
Original line number Diff line number Diff line
@@ -18,8 +18,10 @@ package com.android.systemui.communal.ui.compose

import android.content.ClipDescription
import android.view.DragEvent
import androidx.compose.animation.core.tween
import androidx.compose.foundation.draganddrop.dragAndDropTarget
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
@@ -37,6 +39,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.android.systemui.Flags.communalWidgetResizing
import com.android.systemui.Flags.glanceableHubV2
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
import com.android.systemui.communal.util.WidgetPickerIntentUtils
@@ -51,13 +54,14 @@ import kotlinx.coroutines.launch
 * @see dragAndDropTarget
 */
@Composable
internal fun rememberDragAndDropTargetState(
fun rememberDragAndDropTargetState(
    gridState: LazyGridState,
    contentOffset: Offset,
    contentListState: ContentListState,
): DragAndDropTargetState {
    val scope = rememberCoroutineScope()
    val autoScrollThreshold = with(LocalDensity.current) { 60.dp.toPx() }

    val state =
        remember(gridState, contentOffset, contentListState, autoScrollThreshold, scope) {
            DragAndDropTargetState(
@@ -68,11 +72,9 @@ internal fun rememberDragAndDropTargetState(
                scope = scope,
            )
        }
    LaunchedEffect(state) {
        for (diff in state.scrollChannel) {
            gridState.scrollBy(diff)
        }
    }

    LaunchedEffect(state) { state.processScrollRequests(scope) }

    return state
}

@@ -83,7 +85,7 @@ internal fun rememberDragAndDropTargetState(
 * @see DragEvent
 */
@Composable
internal fun Modifier.dragAndDropTarget(dragDropTargetState: DragAndDropTargetState): Modifier {
fun Modifier.dragAndDropTarget(dragDropTargetState: DragAndDropTargetState): Modifier {
    val state by rememberUpdatedState(dragDropTargetState)

    return this then
@@ -132,13 +134,79 @@ internal fun Modifier.dragAndDropTarget(dragDropTargetState: DragAndDropTargetSt
 * other activities. [GridDragDropState] on the other hand, handles dragging of existing items in
 * the communal hub grid.
 */
internal class DragAndDropTargetState(
class DragAndDropTargetState(
    state: LazyGridState,
    contentOffset: Offset,
    contentListState: ContentListState,
    autoScrollThreshold: Float,
    scope: CoroutineScope,
) {
    private val dragDropState: DragAndDropTargetStateInternal =
        if (glanceableHubV2()) {
            DragAndDropTargetStateV2(
                state = state,
                contentListState = contentListState,
                scope = scope,
                autoScrollThreshold = autoScrollThreshold,
                contentOffset = contentOffset,
            )
        } else {
            DragAndDropTargetStateV1(
                state = state,
                contentListState = contentListState,
                scope = scope,
                autoScrollThreshold = autoScrollThreshold,
                contentOffset = contentOffset,
            )
        }

    fun onStarted() = dragDropState.onStarted()

    fun onMoved(event: DragAndDropEvent) = dragDropState.onMoved(event)

    fun onDrop(event: DragAndDropEvent) = dragDropState.onDrop(event)

    fun onEnded() = dragDropState.onEnded()

    fun onExited() = dragDropState.onExited()

    suspend fun processScrollRequests(coroutineScope: CoroutineScope) =
        dragDropState.processScrollRequests(coroutineScope)
}

/**
 * A private interface defining the API for handling drag-and-drop operations. There will be two
 * implementations of this interface: V1 for devices that do not have the glanceable_hub_v2 flag
 * enabled, and V2 for devices that do have that flag enabled.
 *
 * TODO(b/400789179): Remove this interface and the V1 implementation once glanceable_hub_v2 has
 *   shipped.
 */
private interface DragAndDropTargetStateInternal {
    fun onStarted() = Unit

    fun onMoved(event: DragAndDropEvent) = Unit

    fun onDrop(event: DragAndDropEvent): Boolean = false

    fun onEnded() = Unit

    fun onExited() = Unit

    suspend fun processScrollRequests(coroutineScope: CoroutineScope) = Unit
}

/**
 * The V1 implementation of DragAndDropTargetStateInternal to be used when the glanceable_hub_v2
 * flag is disabled.
 */
private class DragAndDropTargetStateV1(
    private val state: LazyGridState,
    private val contentOffset: Offset,
    private val contentListState: ContentListState,
    private val autoScrollThreshold: Float,
    private val scope: CoroutineScope,
) {
) : DragAndDropTargetStateInternal {
    /**
     * The placeholder item that is treated as if it is being dragged across the grid. It is added
     * to grid once drag and drop event is started and removed when event ends.
@@ -147,15 +215,21 @@ internal class DragAndDropTargetState(
    private var placeHolderIndex: Int? = null
    private var previousTargetItemKey: Any? = null

    internal val scrollChannel = Channel<Float>()
    private val scrollChannel = Channel<Float>()

    override suspend fun processScrollRequests(coroutineScope: CoroutineScope) {
        for (diff in scrollChannel) {
            state.scrollBy(diff)
        }
    }

    fun onStarted() {
    override fun onStarted() {
        // assume item will be added to the end.
        contentListState.list.add(placeHolder)
        placeHolderIndex = contentListState.list.size - 1
    }

    fun onMoved(event: DragAndDropEvent) {
    override fun onMoved(event: DragAndDropEvent) {
        val dragOffset = event.toOffset()

        val targetItem =
@@ -201,7 +275,7 @@ internal class DragAndDropTargetState(
        }
    }

    fun onDrop(event: DragAndDropEvent): Boolean {
    override fun onDrop(event: DragAndDropEvent): Boolean {
        return placeHolderIndex?.let { dropIndex ->
            val widgetExtra = event.maybeWidgetExtra() ?: return false
            val (componentName, user) = widgetExtra
@@ -219,13 +293,13 @@ internal class DragAndDropTargetState(
        } ?: false
    }

    fun onEnded() {
    override fun onEnded() {
        placeHolderIndex = null
        previousTargetItemKey = null
        contentListState.list.remove(placeHolder)
    }

    fun onExited() {
    override fun onExited() {
        onEnded()
    }

@@ -257,6 +331,177 @@ internal class DragAndDropTargetState(
            contentListState.onMove(currentIndex, index)
        }
    }
}

/**
 * The V2 implementation of DragAndDropTargetStateInternal to be used when the glanceable_hub_v2
 * flag is enabled.
 */
private class DragAndDropTargetStateV2(
    private val state: LazyGridState,
    private val contentOffset: Offset,
    private val contentListState: ContentListState,
    private val autoScrollThreshold: Float,
    private val scope: CoroutineScope,
) : DragAndDropTargetStateInternal {
    /**
     * The placeholder item that is treated as if it is being dragged across the grid. It is added
     * to grid once drag and drop event is started and removed when event ends.
     */
    private var placeHolder = CommunalContentModel.WidgetPlaceholder()
    private var placeHolderIndex: Int? = null
    private var previousTargetItemKey: Any? = null
    private var dragOffset = Offset.Zero
    private var columnWidth = 0

    private val scrollChannel = Channel<Float>()

    override suspend fun processScrollRequests(coroutineScope: CoroutineScope) {
        while (true) {
            val amount = scrollChannel.receive()

            if (state.isScrollInProgress) {
                // Ignore overscrolling if a scroll is already in progress (but we still want to
                // consume the scroll event so that we don't end up processing a bunch of old
                // events after scrolling has finished).
                continue
            }

            // Perform the rest of the drag operation after scrolling has finished (or immediately
            // if there will be no scrolling).
            if (amount != 0f) {
                scope.launch {
                    state.animateScrollBy(amount, tween(delayMillis = 250, durationMillis = 1000))
                    performDragAction()
                }
            } else {
                performDragAction()
            }
        }
    }

    override fun onStarted() {
        // assume item will be added to the end.
        contentListState.list.add(placeHolder)
        placeHolderIndex = contentListState.list.size - 1

        // Use the width of the first item as the column width.
        columnWidth =
            state.layoutInfo.visibleItemsInfo.first().size.width +
                state.layoutInfo.beforeContentPadding +
                state.layoutInfo.afterContentPadding
    }

    override fun onMoved(event: DragAndDropEvent) {
        dragOffset = event.toOffset()
        scrollChannel.trySend(computeAutoscroll(dragOffset))
    }

    override fun onDrop(event: DragAndDropEvent): Boolean {
        return placeHolderIndex?.let { dropIndex ->
            val widgetExtra = event.maybeWidgetExtra() ?: return false
            val (componentName, user) = widgetExtra
            if (componentName != null && user != null) {
                // Placeholder isn't removed yet to allow the setting the right rank for items
                // before adding in the new item.
                contentListState.onSaveList(
                    newItemComponentName = componentName,
                    newItemUser = user,
                    newItemIndex = dropIndex,
                )
                return@let true
            }
            return false
        } ?: false
    }

    override fun onEnded() {
        placeHolderIndex = null
        previousTargetItemKey = null
        contentListState.list.remove(placeHolder)
    }

    override fun onExited() {
        onEnded()
    }

    private fun performDragAction() {
        val targetItem =
            state.layoutInfo.visibleItemsInfo
                .asSequence()
                .filter { item -> contentListState.isItemEditable(item.index) }
                .firstItemAtOffset(dragOffset - contentOffset)

        if (
            targetItem != null &&
                (!communalWidgetResizing() || targetItem.key != previousTargetItemKey)
        ) {
            if (communalWidgetResizing()) {
                // Keep track of the previous target item, to avoid rapidly oscillating between
                // items if the target item doesn't visually move as a result of the index change.
                // In this case, even after the index changes, we'd still be colliding with the
                // element, so it would be selected as the target item the next time this function
                // runs again, which would trigger us to revert the index change we recently made.
                previousTargetItemKey = targetItem.key
            }

            val scrollToIndex =
                if (targetItem.index == state.firstVisibleItemIndex) {
                    placeHolderIndex
                } else if (placeHolderIndex == state.firstVisibleItemIndex) {
                    targetItem.index
                } else {
                    null
                }

            if (scrollToIndex != null) {
                scope.launch {
                    state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
                    movePlaceholderTo(targetItem.index)
                }
            } else {
                movePlaceholderTo(targetItem.index)
            }

            placeHolderIndex = targetItem.index
        } else if (targetItem == null) {
            previousTargetItemKey = null
        }
    }

    private fun computeAutoscroll(dragOffset: Offset): Float {
        val orientation = state.layoutInfo.orientation
        val distanceFromStart =
            if (orientation == Orientation.Horizontal) {
                dragOffset.x
            } else {
                dragOffset.y
            }
        val distanceFromEnd =
            if (orientation == Orientation.Horizontal) {
                state.layoutInfo.viewportEndOffset - dragOffset.x
            } else {
                state.layoutInfo.viewportEndOffset - dragOffset.y
            }

        return when {
            distanceFromEnd < autoScrollThreshold -> {
                (columnWidth - state.layoutInfo.beforeContentPadding).toFloat()
            }
            distanceFromStart < autoScrollThreshold -> {
                -(columnWidth - state.layoutInfo.afterContentPadding).toFloat()
            }
            else -> 0f
        }
    }

    private fun movePlaceholderTo(index: Int) {
        val currentIndex = contentListState.list.indexOf(placeHolder)
        if (currentIndex != index) {
            contentListState.swapItems(currentIndex, index)
        }
    }
}

/**
 * Parses and returns the intent extra associated with the widget that is dropped into the grid.
@@ -269,4 +514,3 @@ internal class DragAndDropTargetState(
}

private fun DragAndDropEvent.toOffset() = this.toAndroidDragEvent().run { Offset(x, y) }
}
+366 −29

File changed.

Preview size limit exceeded, changes collapsed.