1 /* <lambda>null2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.constraintlayout.compose 18 19 import androidx.compose.foundation.gestures.awaitEachGesture 20 import androidx.compose.foundation.gestures.awaitFirstDown 21 import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation 22 import androidx.compose.foundation.gestures.drag 23 import androidx.compose.runtime.LaunchedEffect 24 import androidx.compose.runtime.MutableFloatState 25 import androidx.compose.runtime.remember 26 import androidx.compose.ui.Modifier 27 import androidx.compose.ui.composed 28 import androidx.compose.ui.geometry.Offset 29 import androidx.compose.ui.input.pointer.PointerInputChange 30 import androidx.compose.ui.input.pointer.PointerInputScope 31 import androidx.compose.ui.input.pointer.pointerInput 32 import androidx.compose.ui.input.pointer.positionChange 33 import androidx.compose.ui.input.pointer.util.VelocityTracker 34 import androidx.compose.ui.input.pointer.util.addPointerInputChange 35 import androidx.compose.ui.platform.debugInspectorInfo 36 import androidx.compose.ui.unit.Velocity 37 import kotlinx.coroutines.channels.Channel 38 import kotlinx.coroutines.ensureActive 39 import kotlinx.coroutines.isActive 40 41 /** 42 * Helper modifier for MotionLayout to support OnSwipe in Transitions. 43 * 44 * @see Modifier.pointerInput 45 * @see TransitionHandler 46 */ 47 @ExperimentalMotionApi 48 internal fun Modifier.motionPointerInput( 49 key: Any, 50 motionProgress: MutableFloatState, 51 measurer: MotionMeasurer 52 ): Modifier = 53 composed( 54 inspectorInfo = 55 debugInspectorInfo { 56 name = "motionPointerInput" 57 properties["key"] = key 58 properties["motionProgress"] = motionProgress 59 properties["measurer"] = measurer 60 } <lambda>null61 ) { 62 if (!measurer.transition.hasOnSwipe()) { 63 return@composed this 64 } 65 val swipeHandler = 66 remember(key) { 67 TransitionHandler(motionMeasurer = measurer, motionProgress = motionProgress) 68 } 69 val dragChannel = remember(key) { Channel<MotionDragState>(Channel.CONFLATED) } 70 71 LaunchedEffect(key1 = key) effectScope@{ 72 var isTouchUp = false 73 var dragState: MotionDragState? = null 74 while (coroutineContext.isActive) { 75 if (isTouchUp && swipeHandler.pendingProgressWhileTouchUp()) { 76 // Loop until there's no need to update the progress or the there's a touch down 77 swipeHandler.updateProgressWhileTouchUp() 78 } else { 79 if (dragState == null) { 80 dragState = dragChannel.receive() 81 } 82 coroutineContext.ensureActive() 83 isTouchUp = !dragState.isDragging 84 if (isTouchUp) { 85 swipeHandler.onTouchUp(velocity = dragState.velocity) 86 } else { 87 swipeHandler.updateProgressOnDrag(dragAmount = dragState.dragAmount) 88 } 89 dragState = null 90 } 91 92 // To be able to interrupt the free-form progress of 'isUp', check if there's 93 // another 94 // dragState that initiated a new drag 95 val channelResult = dragChannel.tryReceive() 96 if (channelResult.isSuccess) { 97 val receivedState = channelResult.getOrThrow() 98 if (receivedState.isDragging) { 99 // If another drag is initiated, switching 'isUp' interrupts the 100 // 'getTouchUpProgress' loop 101 isTouchUp = false 102 } 103 // Just save the received state, don't 'consume' it 104 dragState = receivedState 105 } 106 } 107 } 108 return@composed this.pointerInput(key) { 109 val velocityTracker = VelocityTracker() 110 detectDragGesturesWhenNeeded( 111 onAcceptFirstDown = { offset -> swipeHandler.onAcceptFirstDownForOnSwipe(offset) }, 112 onDragStart = { _ -> velocityTracker.resetTracking() }, 113 onDragEnd = { 114 dragChannel.trySend( 115 // Indicate that the swipe has ended, MotionLayout should animate the rest. 116 MotionDragState.onDragEnd(velocityTracker.calculateVelocity()) 117 ) 118 }, 119 onDragCancel = { 120 dragChannel.trySend( 121 // Indicate that the swipe has ended, MotionLayout should animate the rest. 122 MotionDragState.onDragEnd(velocityTracker.calculateVelocity()) 123 ) 124 }, 125 onDrag = { change, dragAmount -> 126 velocityTracker.addPointerInputChange(change) 127 // As dragging is done, pass the dragAmount to update the MotionLayout progress. 128 dragChannel.trySend(MotionDragState.onDrag(dragAmount)) 129 } 130 ) 131 } 132 } 133 134 /** Data class with the relevant values of a touch input event used for OnSwipe support. */ 135 internal data class MotionDragState( 136 val isDragging: Boolean, 137 val dragAmount: Offset, 138 val velocity: Velocity 139 ) { 140 companion object { 141 onDragnull142 fun onDrag(dragAmount: Offset) = 143 MotionDragState(isDragging = true, dragAmount = dragAmount, velocity = Velocity.Zero) 144 145 fun onDragEnd(velocity: Velocity) = 146 MotionDragState( 147 isDragging = false, 148 dragAmount = Offset.Unspecified, 149 velocity = velocity 150 ) 151 } 152 } 153 154 /** 155 * Copy of [androidx.compose.foundation.gestures.detectDragGestures] with the opportunity to decide 156 * whether we consume the rest of the drag with [onAcceptFirstDown]. 157 */ 158 private suspend fun PointerInputScope.detectDragGesturesWhenNeeded( 159 onAcceptFirstDown: (Offset) -> Boolean, 160 onDragStart: (Offset) -> Unit, 161 onDragEnd: () -> Unit, 162 onDragCancel: () -> Unit, 163 onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit 164 ) { 165 awaitEachGesture { 166 val down = awaitFirstDown(requireUnconsumed = true) 167 if (!onAcceptFirstDown(down.position)) { 168 return@awaitEachGesture 169 } 170 var drag: PointerInputChange? 171 var overSlop = Offset.Zero 172 do { 173 drag = 174 awaitTouchSlopOrCancellation(pointerId = down.id) { change, over -> 175 change.consume() 176 overSlop = over 177 } 178 } while (drag != null && !drag.isConsumed) 179 if (drag != null) { 180 onDragStart.invoke(drag.position) 181 onDrag(drag, overSlop) 182 if ( 183 !drag(drag.id) { 184 onDrag(it, it.positionChange()) 185 it.consume() 186 } 187 ) { 188 onDragCancel() 189 } else { 190 onDragEnd() 191 } 192 } 193 } 194 } 195