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
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index bd4710b..9f35fcc 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -509,7 +509,6 @@
gridState = gridState,
contentListState = contentListState,
contentOffset = contentOffset,
- updateDragPositionForRemove = updateDragPositionForRemove
)
// A full size box in background that listens to widget drops from the picker.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
index 9e6f22a..0c29394 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
@@ -18,17 +18,13 @@
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.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 @@
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 @@
* @see androidx.compose.foundation.draganddrop.dragAndDropTarget
* @see DragEvent
*/
-@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun Modifier.dragAndDropTarget(
dragDropTargetState: DragAndDropTargetState,
@@ -122,6 +106,10 @@
return state.onDrop(event)
}
+ override fun onExited(event: DragAndDropEvent) {
+ state.onExited()
+ }
+
override fun onEnded(event: DragAndDropEvent) {
state.onEnded()
}
@@ -149,19 +137,17 @@
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,39 +156,39 @@
}
fun onMoved(event: DragAndDropEvent) {
- val dragEvent = event.toAndroidDragEvent()
- isOnRemoveButton = updateDragPositionForRemove(Offset(dragEvent.x, dragEvent.y))
- if (!isOnRemoveButton) {
- findTargetItem(dragEvent)?.apply {
- var scrollIndex: Int? = null
- var scrollOffset: Int? = null
- if (placeHolderIndex == state.firstVisibleItemIndex) {
- // Save info about the first item before the move, to neutralize the automatic
- // keeping first item first.
- scrollIndex = placeHolderIndex
- scrollOffset = state.firstVisibleItemScrollOffset
- }
+ val dragOffset = event.toOffset()
- autoScrollIfNearEdges(dragEvent)
+ val targetItem =
+ state.layoutInfo.visibleItemsInfo
+ .asSequence()
+ .filter { item -> contentListState.isItemEditable(item.index) }
+ .firstItemAtOffset(dragOffset - contentOffset)
- if (contentListState.isItemEditable(this.index)) {
- movePlaceholderTo(this.index)
- placeHolderIndex = this.index
- }
-
- if (scrollIndex != null && scrollOffset != null) {
- // this is needed to neutralize automatic keeping the first item first.
- scope.launch { state.scrollToItem(scrollIndex, scrollOffset) }
- }
+ if (targetItem != null) {
+ var scrollIndex: Int? = null
+ var scrollOffset: Int? = null
+ if (placeHolderIndex == state.firstVisibleItemIndex) {
+ // Save info about the first item before the move, to neutralize the automatic
+ // keeping first item first.
+ scrollIndex = placeHolderIndex
+ scrollOffset = state.firstVisibleItemScrollOffset
}
+
+ 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,39 +207,35 @@
}
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 {
- distanceFromEnd < autoScrollThreshold -> autoScrollThreshold - distanceFromEnd
- distanceFromStart < autoScrollThreshold ->
- -(autoScrollThreshold - distanceFromStart)
- else -> 0f
- }
- }
- private fun findTargetItem(dragEvent: DragEvent): LazyGridItemInfo? =
- state.layoutInfo.visibleItemsInfo.firstItemAtOffset(
- Offset(dragEvent.x, dragEvent.y) - contentOffset
- )
+ return when {
+ distanceFromEnd < autoScrollThreshold -> autoScrollThreshold - distanceFromEnd
+ distanceFromStart < autoScrollThreshold -> distanceFromStart - autoScrollThreshold
+ else -> 0f
+ }
+ }
private fun movePlaceholderTo(index: Int) {
val currentIndex = contentListState.list.indexOf(placeHolder)
@@ -271,4 +253,6 @@
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) }
}