MotionDragHandler.kt
/*
* Copyright (C) 2022 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.constraintlayout.compose
import android.annotation.SuppressLint
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Velocity
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive
/**
* Helper modifier for MotionLayout to support OnSwipe in Transitions.
*
* @see Modifier.pointerInput
* @see TransitionHandler
*/
@SuppressLint("UnnecessaryComposedModifier")
@Suppress("NOTHING_TO_INLINE")
@PublishedApi
internal inline fun Modifier.motionPointerInput(
key: Any = Unit,
progressState: MutableState<Float>,
measurer: MotionMeasurer
): Modifier {
val motionProgress: MotionProgress =
object : MotionProgress {
override val progress: Float
get() = progressState.value
override suspend fun updateProgress(newProgress: Float) {
progressState.value = newProgress
}
}
return this.motionPointerInput(key, motionProgress, measurer)
}
/**
* Helper modifier for MotionLayout to support OnSwipe in Transitions.
*
* @see Modifier.pointerInput
* @see TransitionHandler
*/
@SuppressLint("UnnecessaryComposedModifier")
@Suppress("NOTHING_TO_INLINE")
@PublishedApi
internal inline fun Modifier.motionPointerInput(
key: Any = Unit,
motionProgress: MotionProgress,
measurer: MotionMeasurer
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "motionPointerInput"
properties["key"] = key
properties["measurer"] = measurer
}
) {
if (!measurer.transition.hasOnSwipe()) {
return@composed this
}
val swipeHandler = remember(key) {
TransitionHandler(motionMeasurer = measurer, motionProgress = motionProgress)
}
val dragChannel = remember(key) { Channel<MotionDragState>(Channel.CONFLATED) }
LaunchedEffect(key1 = key) effectScope@{
var isTouchUp = false
var dragState: MotionDragState? = null
while (coroutineContext.isActive) {
if (isTouchUp && swipeHandler.pendingProgressWhileTouchUp()) {
// Loop until there's no need to update the progress or the there's a touch down
swipeHandler.updateProgressWhileTouchUp()
// TODO: Once the progress while Up ends, snap the progress to target (0 or 1)
} else {
if (dragState == null) {
// TODO: Investigate if it's worth skipping some drag events
dragState = dragChannel.receive()
}
coroutineContext.ensureActive()
isTouchUp = !dragState.isDragging
if (isTouchUp) {
swipeHandler.onTouchUp(velocity = dragState.velocity)
} else {
swipeHandler.updateProgressOnDrag(dragAmount = dragState.dragAmount)
}
dragState = null
}
// To be able to interrupt the free-form progress of 'isUp', check if there's another
// dragState that initiated a new drag
val channelResult = dragChannel.tryReceive()
if (channelResult.isSuccess) {
val receivedState = channelResult.getOrThrow()
if (receivedState.isDragging) {
// If another drag is initiated, switching 'isUp' interrupts the
// 'getTouchUpProgress' loop
isTouchUp = false
}
// Just save the received state, don't 'consume' it
dragState = receivedState
}
}
}
return@composed this.pointerInput(key) {
val velocityTracker = VelocityTracker()
detectDragGestures(
onDragStart = {
velocityTracker.resetTracking()
},
onDragEnd = {
dragChannel.trySend(
// Indicate that the swipe has ended, MotionLayout should animate the rest.
MotionDragState.onDragEnd(velocityTracker.calculateVelocity())
)
}
) { change, dragAmount ->
velocityTracker.addPosition(change.uptimeMillis, change.position)
// As dragging is done, pass the dragAmount to update the MotionLayout progress.
dragChannel.trySend(MotionDragState.onDrag(dragAmount))
}
}
}
/**
* Data class with the relevant values of a touch input event used for OnSwipe support.
*/
@PublishedApi
internal data class MotionDragState(
val isDragging: Boolean,
val dragAmount: Offset,
val velocity: Velocity
) {
companion object {
fun onDrag(dragAmount: Offset) =
MotionDragState(
isDragging = true,
dragAmount = dragAmount,
velocity = Velocity.Zero
)
fun onDragEnd(velocity: Velocity) =
MotionDragState(
isDragging = false,
dragAmount = Offset.Unspecified,
velocity = velocity
)
}
}