11package com.smarttoolfactory.tutorial1_1basics.chapter5_gesture
22
3+ import androidx.compose.foundation.Canvas
34import androidx.compose.foundation.gestures.awaitFirstDown
45import androidx.compose.foundation.gestures.forEachGesture
6+ import androidx.compose.runtime.rememberCoroutineScope
57import androidx.compose.ui.Modifier
8+ import androidx.compose.ui.composed
69import androidx.compose.ui.input.pointer.AwaitPointerEventScope
710import androidx.compose.ui.input.pointer.PointerEvent
811import androidx.compose.ui.input.pointer.PointerInputChange
912import androidx.compose.ui.input.pointer.pointerInput
13+ import kotlinx.coroutines.CoroutineScope
14+ import kotlinx.coroutines.delay
15+ import kotlinx.coroutines.launch
1016
1117enum class MotionEvent {
1218 Idle , Down , Move , Up
1319}
1420
21+
22+ /* *
23+ * Reads [awaitFirstDown], and [awaitPointerEvent] to
24+ * get [PointerInputChange] and motion event states
25+ * [onDown], [onMove], and [onUp].
26+ *
27+ * @param onDown is invoked when first pointer is down initially.
28+ * @param onMove one or multiple pointers are being moved on screen.
29+ * @param onUp last pointer is up
30+ * @param scope [CoroutineScope] is required to have a delay after first down
31+ * to give enough time for [onDown] to be processed before [onMove]
32+ */
1533suspend fun AwaitPointerEventScope.awaitPointerMotionEvent (
16- onTouchEvent : (MotionEvent , PointerInputChange ) -> Unit
34+ onDown : (PointerInputChange ) -> Unit = {},
35+ onMove : (PointerInputChange ) -> Unit = {},
36+ onUp : (PointerInputChange ) -> Unit = {},
37+ scope : CoroutineScope
1738) {
1839
1940 // Wait for at least one pointer to press down, and set first contact position
2041 val down: PointerInputChange = awaitFirstDown()
21- onTouchEvent( MotionEvent . Down , down)
42+ onDown( down)
2243
2344 var pointer = down
2445 // Main pointer is the one that is down initially
2546 var pointerId = down.id
2647
48+ // If a move event is followed fast enough down is skipped, especially by Canvas
49+ // to prevent it we add 20ms delay after first touch
50+ var waitedAfterDown = false
51+
52+ scope.launch {
53+ delay(20 )
54+ waitedAfterDown = true
55+ }
56+
2757 // This is preferred way in default Compose gesture codes
2858 // to loop gesture events and use consume or position changes to
2959 // break while loop
@@ -46,20 +76,38 @@ suspend fun AwaitPointerEventScope.awaitPointerMotionEvent(
4676 pointerId = pointerInputChange.id
4777 pointer = pointerInputChange
4878
49- onTouchEvent(MotionEvent .Move , pointer)
79+ if (waitedAfterDown) {
80+ onMove(pointer)
81+ }
5082 } else {
5183 // All of the pointers are up
52- onTouchEvent( MotionEvent . Up , pointer)
84+ onUp( pointer)
5385 break
5486 }
5587 }
5688}
5789
90+ /* *
91+ * Reads [awaitFirstDown], and [awaitPointerEvent] to
92+ * get [PointerInputChange] and motion event states
93+ * [onDown], [onMove], and [onUp]. Unlike overload of this function [onMove] returns
94+ * list of [PointerInputChange] to get data about all pointers that are on the screen.
95+ *
96+ * @param onDown is invoked when first pointer is down initially.
97+ * @param onMove one or multiple pointers are being moved on screen.
98+ * @param onUp last pointer is up
99+ *
100+ * ### Note
101+ * There is a 20ms delay after [onDown] to let composables process this event.
102+ * When pointer is moved fast [Canvas] is not able to react first down at each occurrence.
103+ *
104+ */
105+ suspend fun AwaitPointerEventScope.awaitPointerMotionEvents (
58106
59- suspend fun AwaitPointerEventScope.awaitPointerMotionEvent (
60107 onDown : (PointerInputChange ) -> Unit = {},
61- onMove : (PointerInputChange ) -> Unit = {},
62- onUp : (PointerInputChange ) -> Unit = {},
108+ onMove : ( List <PointerInputChange >) -> Unit = {},
109+ onUp : (PointerInputChange ) -> Unit = {},
110+ scope : CoroutineScope
63111) {
64112
65113 // Wait for at least one pointer to press down, and set first contact position
@@ -70,6 +118,15 @@ suspend fun AwaitPointerEventScope.awaitPointerMotionEvent(
70118 // Main pointer is the one that is down initially
71119 var pointerId = down.id
72120
121+ // If a move event is followed fast enough down is skipped, especially by Canvas
122+ // to prevent it we add 20ms delay after first touch
123+ var waitedAfterDown = false
124+
125+ scope.launch {
126+ delay(20 )
127+ waitedAfterDown = true
128+ }
129+
73130 // This is preferred way in default Compose gesture codes
74131 // to loop gesture events and use consume or position changes to
75132 // break while loop
@@ -92,81 +149,88 @@ suspend fun AwaitPointerEventScope.awaitPointerMotionEvent(
92149 pointerId = pointerInputChange.id
93150 pointer = pointerInputChange
94151
95- onMove(pointer)
152+ if (waitedAfterDown) {
153+ onMove( event.changes)
154+ }
155+
96156 } else {
97157 // All of the pointers are up
98- onUp(pointer)
158+ onUp( pointer)
99159 break
100160 }
101161 }
102162}
103163
164+
165+ /* *
166+ * Reads [awaitFirstDown], and [awaitPointerEvent] to
167+ * get [PointerInputChange] and motion event states
168+ * [onDown], [onMove], and [onUp].
169+ *
170+ * @param onDown is invoked when first pointer is down initially.
171+ * @param onMove one or multiple pointers are being moved on screen.
172+ * @param onUp last pointer is up
173+ *
174+ * ### Note
175+ * There is a 20ms delay after [onDown] to let composables process this event.
176+ * When pointer is moved fast [Canvas] is not able to react first down at each occurrance.
177+ *
178+ */
104179fun Modifier.awaitPointerMotionEvent (
105180 onDown : (PointerInputChange ) -> Unit = {},
106- onMove : (PointerInputChange ) -> Unit = {},
107- onUp : (PointerInputChange ) -> Unit = {},
108- ) =
109- this .then(
181+ onMove : (PointerInputChange ) -> Unit = {},
182+ onUp : (PointerInputChange ) -> Unit = {},
183+ ) = composed(
184+ inspectorInfo = {
185+ name = " awaitPointerMotionEvent"
186+ },
187+ factory = {
188+ val scope = rememberCoroutineScope()
189+
110190 Modifier .pointerInput(Unit ) {
111191 forEachGesture {
112192 awaitPointerEventScope {
113- awaitPointerMotionEvent(onDown, onMove, onUp)
193+ awaitPointerMotionEvent(onDown, onMove, onUp, scope )
114194 }
115195 }
116196 }
117- )
197+ }
198+ )
199+
200+ /* *
201+ * Reads [awaitFirstDown], and [awaitPointerEvent] to
202+ * get [PointerInputChange] and motion event states
203+ * [onDown], [onMove], and [onUp].
204+ *
205+ * [onMove] returns list of [PointerEvent] that are on screen
206+ *
207+ * @param onDown is invoked when first pointer is down initially.
208+ * @param onMove one or multiple pointers are being moved on screen.
209+ * @param onUp last pointer is up
210+ *
211+ * ### Note
212+ * There is a 20ms delay after [onDown] to let composables process this event.
213+ * When pointer is moved fast [Canvas] is not able to react first down at each occurrance.
214+ *
215+ */
216+ fun Modifier.awaitPointerMotionEvents (
217+ onDown : (PointerInputChange ) -> Unit = {},
218+ onMove : (List <PointerInputChange >) -> Unit = {},
219+ onUp : (PointerInputChange ) -> Unit = {},
220+ ) = composed(
221+ inspectorInfo = {
222+ name = " awaitPointerMotionEvent"
223+ },
224+ factory = {
225+ val scope = rememberCoroutineScope()
118226
119- fun Modifier.awaitPointerMotionEvent (onTouchEvent : (MotionEvent , PointerInputChange ) -> Unit ) =
120- this .then(
121227 Modifier .pointerInput(Unit ) {
122228 forEachGesture {
123229 awaitPointerEventScope {
124- awaitPointerMotionEvent(onTouchEvent )
230+ awaitPointerMotionEvents(onDown, onMove, onUp, scope )
125231 }
126232 }
127233 }
128- )
129-
130- suspend fun AwaitPointerEventScope.awaitMotionEvent (
131- onTouchEvent : (MotionEvent , List <PointerInputChange >) -> Unit
132- ) {
133-
134- // Wait for at least one pointer to press down, and set first contact position
135- val down: PointerInputChange = awaitFirstDown()
136- onTouchEvent(MotionEvent .Down , listOf (down))
137-
138- var pointer = down
139- // Main pointer is the one that is down initially
140- var pointerId = down.id
141-
142- // This is preferred way in default Compose gesture codes
143- // to loop gesture events and use consume or position changes to
144- // break while loop
145- while (true ) {
146-
147- val event: PointerEvent = awaitPointerEvent()
148-
149- val anyPressed = event.changes.any { it.pressed }
150-
151- // There are at least one pointer pressed
152- if (anyPressed) {
153- // Get pointer that is down, if first pointer is up
154- // get another and use it if other pointers are also down
155- // event.changes.first() doesn't return same order
156- val pointerInputChange =
157- event.changes.firstOrNull { it.id == pointerId }
158- ? : event.changes.first()
159-
160- // Next time will check same pointer with this id
161- pointerId = pointerInputChange.id
162- pointer = pointerInputChange
163-
164- onTouchEvent(MotionEvent .Move , event.changes)
165-
166- } else {
167- // All of the pointers are up
168- onTouchEvent(MotionEvent .Up , listOf (pointer))
169- break
170- }
171234 }
172- }
235+ )
236+
0 commit comments