Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
*/
package org.burnoutcrew.reorderable

import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
Expand All @@ -25,20 +25,18 @@ import androidx.compose.ui.input.pointer.pointerInput
fun Modifier.detectReorder(state: ReorderableState<*>) =
this.then(
Modifier.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
var overSlop = Offset.Zero
do {
drag = awaitPointerSlopOrCancellation(down.id, down.type) { change, over ->
change.consume()
overSlop = over
}
} while (drag != null && !drag.isConsumed)
if (drag != null) {
state.interactions.trySend(StartDrag(down.id, overSlop))
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
var overSlop = Offset.Zero
do {
drag = awaitPointerSlopOrCancellation(down.id, down.type) { change, over ->
change.consume()
overSlop = over
}
} while (drag != null && !drag.isConsumed)
if (drag != null) {
state.interactions.trySend(StartDrag(down.id, overSlop))
}
}
}
Expand All @@ -48,13 +46,11 @@ fun Modifier.detectReorder(state: ReorderableState<*>) =
fun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) =
this.then(
Modifier.pointerInput(Unit) {
forEachGesture {
val down = awaitPointerEventScope {
awaitFirstDown(requireUnconsumed = false)
}
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
awaitLongPressOrCancellation(down)?.also {
state.interactions.trySend(StartDrag(down.id))
}
}
}
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -88,50 +88,42 @@ internal suspend fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
}
}

internal suspend fun PointerInputScope.awaitLongPressOrCancellation(
internal suspend fun AwaitPointerEventScope.awaitLongPressOrCancellation(
initialDown: PointerInputChange
): PointerInputChange? {
var longPress: PointerInputChange? = null
var currentDown = initialDown
val longPressTimeout = viewConfiguration.longPressTimeoutMillis
return try {
// wait for first tap up or long press
withTimeout(longPressTimeout) {
awaitPointerEventScope {
var finished = false
while (!finished) {
val event = awaitPointerEvent(PointerEventPass.Main)
if (event.changes.fastAll { it.changedToUpIgnoreConsumed() }) {
// All pointers are up
finished = true
}
var finished = false
while (!finished) {
val event = awaitPointerEvent(PointerEventPass.Main)
if (event.changes.fastAll { it.changedToUpIgnoreConsumed() }) {
finished = true
}

if (
event.changes.fastAny {
it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)
}
) {
finished = true // Canceled
if (
event.changes.fastAny {
it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)
}
) {
finished = true
}

// Check for cancel by position consumption. We can look on the Final pass of
// the existing pointer event because it comes after the Main pass we checked
// above.
val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
if (consumeCheck.changes.fastAny { it.isConsumed }) {
finished = true
}
if (!event.isPointerUp(currentDown.id)) {
longPress = event.changes.fastFirstOrNull { it.id == currentDown.id }
val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
if (consumeCheck.changes.fastAny { it.isConsumed }) {
finished = true
}
if (!event.isPointerUp(currentDown.id)) {
longPress = event.changes.fastFirstOrNull { it.id == currentDown.id }
} else {
val newPressed = event.changes.fastFirstOrNull { it.pressed }
if (newPressed != null) {
currentDown = newPressed
longPress = currentDown
} else {
val newPressed = event.changes.fastFirstOrNull { it.pressed }
if (newPressed != null) {
currentDown = newPressed
longPress = currentDown
} else {
// should technically never happen as we checked it above
finished = true
}
finished = true
}
}
}
Expand All @@ -142,6 +134,12 @@ internal suspend fun PointerInputScope.awaitLongPressOrCancellation(
}
}

internal suspend fun PointerInputScope.awaitLongPressOrCancellation(
initialDown: PointerInputChange
): PointerInputChange? = awaitPointerEventScope {
awaitLongPressOrCancellation(initialDown)
}

private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean =
changes.fastFirstOrNull { it.id == pointerId }?.pressed != true

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2022 André Claßen
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.burnoutcrew.reorderable

import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive

/**
* Replacement for the deprecated [androidx.compose.foundation.gestures.forEachGesture].
* Use when the gesture loop needs arbitrary suspending calls (e.g. channel receive)
* that [androidx.compose.foundation.gestures.awaitEachGesture] does not support.
*/
internal suspend fun PointerInputScope.eachGestureWhileActive(
block: suspend PointerInputScope.() -> Unit
) {
val currentContext = currentCoroutineContext()
while (currentContext.isActive) {
try {
block()
} catch (e: CancellationException) {
if (!currentContext.isActive) throw e
awaitPointerEventScope {
awaitAllPointersUp()
}
}
}
}

private suspend fun AwaitPointerEventScope.awaitAllPointersUp() {
if (currentEvent.changes.fastAll { !it.pressed }) return
do {
awaitPointerEvent(PointerEventPass.Initial)
} while (currentEvent.changes.fastAny { it.pressed })
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package org.burnoutcrew.reorderable

import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerId
Expand All @@ -31,7 +30,7 @@ fun Modifier.reorderable(
state: ReorderableState<*>
) = then(
Modifier.pointerInput(Unit) {
forEachGesture {
eachGestureWhileActive {
val dragStart = state.interactions.receive()
val down = awaitPointerEventScope {
currentEvent.changes.fastFirstOrNull { it.id == dragStart.id }
Expand Down