/*
* Copyright 2020 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.
*/
@file:Suppress("DEPRECATION")
package androidx.compose.foundation.gestures
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.gesture.ScrollCallback
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.gesture.util.VelocityTracker
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 java.util.concurrent.CancellationException
import kotlin.math.sign
@Suppress("ModifierInspectorInfo")
internal actual fun Modifier.touchScrollable(
scrollCallback: ScrollCallback,
orientation: Orientation,
enabled: Boolean,
startScrollImmediately: Boolean
): Modifier = composed(
factory = {
val orientationState = rememberUpdatedState(orientation)
val startScrollImmediatelyState = rememberUpdatedState(startScrollImmediately)
val enabledState = rememberUpdatedState(enabled)
val scrollCallbackState = rememberUpdatedState(scrollCallback)
val scrollLambda: suspend PointerInputScope.() -> Unit = remember {
{
forEachGesture {
dragForEachGesture(
orientation = orientationState,
enabled = enabledState,
startScrollImmediately = startScrollImmediatelyState,
callback = scrollCallbackState
)
}
}
}
Modifier.pointerInput(Unit, scrollLambda)
}
)
private suspend fun PointerInputScope.dragForEachGesture(
orientation: State<Orientation>,
enabled: State<Boolean>,
startScrollImmediately: State<Boolean>,
callback: State<ScrollCallback>
) {
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
)
var initialDelta = 0f
val startEvent = awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
if (!enabled.value) {
null
} else if (startScrollImmediately.value) {
// since we start immediately we don't wait for slop and set initial delta to 0
initialDelta = 0f
down
} else {
val onSlopPassed = { event: PointerInputChange, overSlop: Float ->
event.consume(event.position.run { if (isVertical()) y else x })
initialDelta = overSlop
}
val result = if (isVertical()) {
awaitVerticalTouchSlopOrCancellation(down.id, onSlopPassed)
} else {
awaitHorizontalTouchSlopOrCancellation(down.id, onSlopPassed)
}
if (enabled.value) result else null
}
}
startEvent?.let { drag ->
try {
awaitPointerEventScope {
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 })
callback.value.onStart(adjustedStart)
callback.value.onScroll(initialDelta)
val velocityTracker = VelocityTracker()
velocityTracker.addPosition(drag.uptimeMillis, drag.position)
val dragTick = { event: PointerInputChange ->
velocityTracker.addPosition(event.uptimeMillis, event.position)
val delta = event.positionChange().run { if (isVertical()) y else x }
callback.value.onScroll(delta)
event.consume(delta)
}
val isDragSuccessful = if (isVertical()) {
verticalDrag(drag.id, dragTick)
} else {
horizontalDrag(drag.id, dragTick)
}
if (isDragSuccessful) {
callback.value.onStop(
velocityTracker.calculateVelocity().run { if (isVertical()) y else x }
)
} else {
callback.value.onCancel()
}
}
} catch (cancellation: CancellationException) {
callback.value.onCancel()
throw cancellation
}
}
}
@Suppress("DEPRECATION")
internal actual fun Modifier.mouseScrollable(
scrollCallback: ScrollCallback,
orientation: Orientation
): Modifier = this