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

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

Merge "Fix several issues with hub scrolling during widget drag-and-drop." into main

parents 86d676ea c2d8a870
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -517,7 +517,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) }
}