[go: nahoru, domu]

blob: c4a4307cb1d692ed9fb30445292beade3e05e5f1 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* 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
*
* http://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 androidx.compose.foundation.gestures
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.MutatorMutex
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.consumePositionChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.platform.debugInspectorInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlin.coroutines.cancellation.CancellationException
import kotlin.math.sign
/**
* State of [draggable]. Allows for a granular control of how deltas are consumed by the user as
* well as to write custom drag methods using [drag] suspend function.
*/
interface DraggableState {
/**
* Call this function to take control of drag logic.
*
* All actions that change the logical drag position must be performed within a [drag]
* block (even if they don't call any other methods on this object) in order to guarantee
* that mutual exclusion is enforced.
*
* If [drag] is called from elsewhere with the [dragPriority] higher or equal to ongoing
* drag, ongoing drag will be canceled.
*
* @param dragPriority of the drag operation
* @param block to perform drag in
*/
suspend fun drag(
dragPriority: MutatePriority = MutatePriority.Default,
block: suspend DragScope.() -> Unit
)
/**
* Dispatch drag delta in pixels avoiding all drag related priority mechanisms.
*
* **NOTE:** unlike [drag], dispatching any delta with this method will bypass scrolling of
* any priority. This method will also ignore `reverseDirection` and other parameters set in
* [draggable].
*
* This method is used internally for low level operations, allowing implementers of
* [DraggableState] influence the consumption as suits them, e.g introduce nested scrolling.
* Manually dispatching delta via this method will likely result in a bad user experience,
* you must prefer [drag] method over this one.
*
* @param delta amount of scroll dispatched in the nested drag process
*/
fun dispatchRawDelta(delta: Float)
}
/**
* Scope used for suspending drag blocks
*/
interface DragScope {
/**
* Attempts to drag by [pixels] px.
*/
fun dragBy(pixels: Float)
}
/**
* Default implementation of [DraggableState] interface that allows to pass a simple action that
* will be invoked when the drag occurs.
*
* This is the simplest way to set up a [draggable] modifier. When constructing this
* [DraggableState], you must provide a [onDelta] lambda, which will be invoked whenever
* drag happens (by gesture input or a custom [DraggableState.drag] call) with the delta in
* pixels.
*
* If you are creating [DraggableState] in composition, consider using [rememberDraggableState].
*
* @param onDelta callback invoked when drag occurs. The callback receives the delta in pixels.
*/
fun DraggableState(onDelta: (Float) -> Unit): DraggableState =
DefaultDraggableState(onDelta)
/**
* Create and remember default implementation of [DraggableState] interface that allows to pass a
* simple action that will be invoked when the drag occurs.
*
* This is the simplest way to set up a [draggable] modifier. When constructing this
* [DraggableState], you must provide a [onDelta] lambda, which will be invoked whenever
* drag happens (by gesture input or a custom [DraggableState.drag] call) with the delta in
* pixels.
*
* @param onDelta callback invoked when drag occurs. The callback receives the delta in pixels.
*/
@Composable
fun rememberDraggableState(onDelta: (Float) -> Unit): DraggableState {
val onDeltaState = rememberUpdatedState(onDelta)
return remember { DraggableState { onDeltaState.value.invoke(it) } }
}
/**
* Configure touch dragging for the UI element in a single [Orientation]. The drag distance
* reported to [DraggableState], allowing users to react on the drag delta and update their state.
*
* The common usecase for this component is when you need to be able to drag something
* inside the component on the screen and represent this state via one float value
*
* If you need to control the whole dragging flow, consider using [pointerInput] instead with the
* helper functions like [detectDragGestures].
*
* If you are implementing scroll/fling behavior, consider using [scrollable].
*
* @sample androidx.compose.foundation.samples.DraggableSample
*
* @param state [DraggableState] state of the draggable. Defines how drag events will be
* interpreted by the user land logic.
* @param orientation orientation of the drag
* @param enabled whether or not drag is enabled
* @param interactionState [InteractionState] that will be updated when this draggable is
* being dragged, using [Interaction.Dragged].
* @param startDragImmediately when set to true, draggable will start dragging immediately and
* prevent other gesture detectors from reacting to "down" events (in order to block composed
* press-based gestures). This is intended to allow end users to "catch" an animating widget by
* pressing on it. It's useful to set it when value you're dragging is settling / animating.
* @param onDragStarted callback that will be invoked when drag is about to start at the starting
* position, allowing user to suspend and perform preparation for drag, if desired. This suspend
* function is invoked with the draggable scope, allowing for async processing, if desired
* @param onDragStopped callback that will be invoked when drag is finished, allowing the
* user to react on velocity and process it. This suspend function is invoked with the draggable
* scope, allowing for async processing, if desired
* @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will
* behave like bottom to top and left to right will behave like right to left.
*/
fun Modifier.draggable(
state: DraggableState,
orientation: Orientation,
enabled: Boolean = true,
interactionState: InteractionState? = null,
startDragImmediately: Boolean = false,
onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
reverseDirection: Boolean = false
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "draggable"
properties["orientation"] = orientation
properties["enabled"] = enabled
properties["reverseDirection"] = reverseDirection
properties["interactionState"] = interactionState
properties["startDragImmediately"] = startDragImmediately
properties["onDragStarted"] = onDragStarted
properties["onDragStopped"] = onDragStopped
properties["state"] = state
}
) {
DisposableEffect(interactionState) {
onDispose {
interactionState?.removeInteraction(Interaction.Dragged)
}
}
val orientationState = rememberUpdatedState(orientation)
val enabledState = rememberUpdatedState(enabled)
val reverseDirectionState = rememberUpdatedState(reverseDirection)
val startImmediatelyState = rememberUpdatedState(startDragImmediately)
val interactionStateState = rememberUpdatedState(interactionState)
val onDragStartedState = rememberUpdatedState(onDragStarted)
val updatedDraggableState = rememberUpdatedState(state)
val onDragStoppedState = rememberUpdatedState(onDragStopped)
val dragBlock: suspend PointerInputScope.() -> Unit = remember {
{
dragForEachGesture(
orientation = orientationState,
enabled = enabledState,
interactionState = interactionStateState,
reverseDirection = reverseDirectionState,
startDragImmediately = startImmediatelyState,
onDragStarted = onDragStartedState,
onDragStopped = onDragStoppedState,
dragState = updatedDraggableState
)
}
}
Modifier.pointerInput(Unit, dragBlock)
}
private suspend fun PointerInputScope.dragForEachGesture(
orientation: State<Orientation>,
enabled: State<Boolean>,
reverseDirection: State<Boolean>,
interactionState: State<InteractionState?>,
startDragImmediately: State<Boolean>,
onDragStarted: State<suspend CoroutineScope.(startedPosition: Offset) -> Unit>,
onDragStopped: State<suspend CoroutineScope.(velocity: Float) -> Unit>,
dragState: State<DraggableState>
) {
coroutineScope {
forEachGesture {
fun isVertical() = orientation.value == Orientation.Vertical
fun PointerInputChange.consume(amount: Float) = this.consumePositionChange(
consumedDx = if (isVertical()) 0f else amount,
consumedDy = if (isVertical()) amount else 0f
)
suspend fun DragScope.performDrag(
initialDelta: Float,
dragStart: PointerInputChange,
velocityTracker: VelocityTracker
): Boolean {
return awaitPointerEventScope {
dragBy(if (reverseDirection.value) initialDelta * -1 else initialDelta)
velocityTracker.addPosition(dragStart.uptimeMillis, dragStart.position)
val dragTick = { event: PointerInputChange ->
velocityTracker.addPosition(event.uptimeMillis, event.position)
val delta =
event.positionChange().run { if (isVertical()) y else x }
event.consume(delta)
if (enabled.value) {
dragBy(if (reverseDirection.value) delta * -1 else delta)
}
}
if (isVertical()) {
verticalDrag(dragStart.id, dragTick)
} else {
horizontalDrag(dragStart.id, dragTick)
}
}
}
var initialDelta = 0f
val startEvent = awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
if (!enabled.value) {
null
} else if (startDragImmediately.value) {
// since we start immediately we don't wait for slop and set initial delta to 0
initialDelta = 0f
down
} else {
val postTouchSlop = { event: PointerInputChange, offset: Float ->
event.consume(event.position.run { if (isVertical()) y else x })
initialDelta = offset
}
val afterSlopResult = if (isVertical()) {
awaitVerticalTouchSlopOrCancellation(down.id, postTouchSlop)
} else {
awaitHorizontalTouchSlopOrCancellation(down.id, postTouchSlop)
}
if (enabled.value) afterSlopResult else null
}
}
startEvent?.let { drag ->
var isDragSuccessful = false
val velocityTracker = VelocityTracker()
var enabledWhenInteractionAdded = false
try {
// remember enabled state when we add interaction to remove later if needed
enabledWhenInteractionAdded = enabled.value
val overSlopOffset =
if (isVertical()) Offset(0f, initialDelta)
else Offset(initialDelta, 0f)
val adjustedStart = drag.position -
overSlopOffset * sign(drag.position.run { if (isVertical()) y else x })
if (enabledWhenInteractionAdded) {
onDragStarted.value.invoke(this@coroutineScope, adjustedStart)
interactionState.value?.addInteraction(Interaction.Dragged)
}
dragState.value.drag(dragPriority = MutatePriority.UserInput) {
isDragSuccessful = performDrag(initialDelta, drag, velocityTracker)
}
} catch (cancellation: CancellationException) {
isDragSuccessful = false
} finally {
if (enabledWhenInteractionAdded) {
interactionState.value?.removeInteraction(Interaction.Dragged)
}
val velocity =
if (isDragSuccessful) {
velocityTracker.calculateVelocity().run { if (isVertical()) y else x }
} else {
0f
}
onDragStopped.value.invoke(
this@coroutineScope,
if (reverseDirection.value) velocity * -1 else velocity
)
}
}
}
}
}
private class DefaultDraggableState(val onDelta: (Float) -> Unit) : DraggableState {
private val dragScope: DragScope = object : DragScope {
override fun dragBy(pixels: Float): Unit = onDelta(pixels)
}
private val scrollMutex = MutatorMutex()
override suspend fun drag(
dragPriority: MutatePriority,
block: suspend DragScope.() -> Unit
): Unit = coroutineScope {
scrollMutex.mutateWith(dragScope, dragPriority, block)
}
override fun dispatchRawDelta(delta: Float) {
return onDelta(delta)
}
}