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

Commit c2d8a870 authored by Will Leshner's avatar Will Leshner
Browse files

Fix several issues with hub scrolling during widget drag-and-drop.

Bug: 347293340
Bug: 346328875
Test: Maunally by dragging widgets to the hub from the widget picker.
Flag: com.android.systemui.communal_hub

Change-Id: Ic2d9736e23d5b2ad9a1c316a9a3ee204320ca320
parent 266b176e
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -509,7 +509,6 @@ private fun BoxScope.CommunalHubLazyGrid(
                gridState = gridState,
                contentListState = contentListState,
                contentOffset = contentOffset,
                updateDragPositionForRemove = updateDragPositionForRemove
            )

        // A full size box in background that listens to widget drops from the picker.
+55 −71
Original line number Diff line number Diff line
@@ -18,17 +18,13 @@ package com.android.systemui.communal.ui.compose

import android.content.ClipDescription
import android.view.DragEvent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.draganddrop.dragAndDropTarget
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
@@ -45,8 +41,7 @@ import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
import com.android.systemui.communal.util.WidgetPickerIntentUtils
import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtraFromIntent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch

/**
@@ -59,32 +54,22 @@ internal fun rememberDragAndDropTargetState(
    gridState: LazyGridState,
    contentOffset: Offset,
    contentListState: ContentListState,
    updateDragPositionForRemove: (offset: Offset) -> Boolean,
): DragAndDropTargetState {
    val scope = rememberCoroutineScope()
    val autoScrollSpeed = remember { mutableFloatStateOf(0f) }
    // Threshold of distance from edges that should start auto-scroll - chosen to be a narrow value
    // that allows differentiating intention of scrolling from intention of dragging over the first
    // visible item.
    val autoScrollThreshold = with(LocalDensity.current) { 60.dp.toPx() }
    val state =
        remember(gridState, contentListState) {
        remember(gridState, contentOffset, contentListState, autoScrollThreshold, scope) {
            DragAndDropTargetState(
                state = gridState,
                contentOffset = contentOffset,
                contentListState = contentListState,
                scope = scope,
                autoScrollSpeed = autoScrollSpeed,
                autoScrollThreshold = autoScrollThreshold,
                updateDragPositionForRemove = updateDragPositionForRemove,
                scope = scope,
            )
        }
    LaunchedEffect(autoScrollSpeed.floatValue) {
        if (autoScrollSpeed.floatValue != 0f) {
            while (isActive) {
                gridState.scrollBy(autoScrollSpeed.floatValue)
                delay(10)
            }
    LaunchedEffect(state) {
        for (diff in state.scrollChannel) {
            gridState.scrollBy(diff)
        }
    }
    return state
@@ -96,7 +81,6 @@ internal fun rememberDragAndDropTargetState(
 * @see androidx.compose.foundation.draganddrop.dragAndDropTarget
 * @see DragEvent
 */
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun Modifier.dragAndDropTarget(
    dragDropTargetState: DragAndDropTargetState,
@@ -122,6 +106,10 @@ internal fun Modifier.dragAndDropTarget(
                        return state.onDrop(event)
                    }

                    override fun onExited(event: DragAndDropEvent) {
                        state.onExited()
                    }

                    override fun onEnded(event: DragAndDropEvent) {
                        state.onEnded()
                    }
@@ -149,19 +137,17 @@ internal class DragAndDropTargetState(
    private val state: LazyGridState,
    private val contentOffset: Offset,
    private val contentListState: ContentListState,
    private val scope: CoroutineScope,
    private val autoScrollSpeed: MutableState<Float>,
    private val autoScrollThreshold: Float,
    private val updateDragPositionForRemove: (offset: Offset) -> Boolean,
    private val scope: CoroutineScope,
) {
    /**
     * 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 isOnRemoveButton = false

    internal val scrollChannel = Channel<Float>()

    fun onStarted() {
        // assume item will be added to the end.
@@ -170,10 +156,15 @@ internal class DragAndDropTargetState(
    }

    fun onMoved(event: DragAndDropEvent) {
        val dragEvent = event.toAndroidDragEvent()
        isOnRemoveButton = updateDragPositionForRemove(Offset(dragEvent.x, dragEvent.y))
        if (!isOnRemoveButton) {
            findTargetItem(dragEvent)?.apply {
        val dragOffset = event.toOffset()

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

        if (targetItem != null) {
            var scrollIndex: Int? = null
            var scrollOffset: Int? = null
            if (placeHolderIndex == state.firstVisibleItemIndex) {
@@ -183,26 +174,21 @@ internal class DragAndDropTargetState(
                scrollOffset = state.firstVisibleItemScrollOffset
            }

                autoScrollIfNearEdges(dragEvent)

                if (contentListState.isItemEditable(this.index)) {
                    movePlaceholderTo(this.index)
                    placeHolderIndex = this.index
            if (contentListState.isItemEditable(targetItem.index)) {
                movePlaceholderTo(targetItem.index)
                placeHolderIndex = targetItem.index
            }

            if (scrollIndex != null && scrollOffset != null) {
                // this is needed to neutralize automatic keeping the first item first.
                scope.launch { state.scrollToItem(scrollIndex, scrollOffset) }
            }
            }
        } else {
            computeAutoscroll(dragOffset).takeIf { it != 0f }?.let { scrollChannel.trySend(it) }
        }
    }

    fun onDrop(event: DragAndDropEvent): Boolean {
        autoScrollSpeed.value = 0f
        if (isOnRemoveButton) {
            return false
        }
        return placeHolderIndex?.let { dropIndex ->
            val widgetExtra = event.maybeWidgetExtra() ?: return false
            val (componentName, user) = widgetExtra
@@ -221,40 +207,36 @@ internal class DragAndDropTargetState(
    }

    fun onEnded() {
        autoScrollSpeed.value = 0f
        placeHolderIndex = null
        contentListState.list.remove(placeHolder)
        isOnRemoveButton = updateDragPositionForRemove(Offset.Zero)
    }

    private fun autoScrollIfNearEdges(dragEvent: DragEvent) {
    fun onExited() {
        onEnded()
    }

    private fun computeAutoscroll(dragOffset: Offset): Float {
        val orientation = state.layoutInfo.orientation
        val distanceFromStart =
            if (orientation == Orientation.Horizontal) {
                dragEvent.x
                dragOffset.x
            } else {
                dragEvent.y
                dragOffset.y
            }
        val distanceFromEnd =
            if (orientation == Orientation.Horizontal) {
                state.layoutInfo.viewportSize.width - dragEvent.x
                state.layoutInfo.viewportEndOffset - dragOffset.x
            } else {
                state.layoutInfo.viewportSize.height - dragEvent.y
                state.layoutInfo.viewportEndOffset - dragOffset.y
            }
        autoScrollSpeed.value =
            when {

        return when {
            distanceFromEnd < autoScrollThreshold -> autoScrollThreshold - distanceFromEnd
                distanceFromStart < autoScrollThreshold ->
                    -(autoScrollThreshold - distanceFromStart)
            distanceFromStart < autoScrollThreshold -> distanceFromStart - autoScrollThreshold
            else -> 0f
        }
    }

    private fun findTargetItem(dragEvent: DragEvent): LazyGridItemInfo? =
        state.layoutInfo.visibleItemsInfo.firstItemAtOffset(
            Offset(dragEvent.x, dragEvent.y) - contentOffset
        )

    private fun movePlaceholderTo(index: Int) {
        val currentIndex = contentListState.list.indexOf(placeHolder)
        if (currentIndex != index) {
@@ -271,4 +253,6 @@ internal class DragAndDropTargetState(
        val clipData = this.toAndroidDragEvent().clipData.takeIf { it.itemCount != 0 }
        return clipData?.getItemAt(0)?.intent?.let { intent -> getWidgetExtraFromIntent(intent) }
    }

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