Skip to content

Commit 2a59b03

Browse files
add gestures
1 parent 245836c commit 2a59b03

File tree

4 files changed

+448
-0
lines changed

4 files changed

+448
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.smarttoolfactory.slider
2+
3+
import androidx.compose.material.ExperimentalMaterialApi
4+
import androidx.compose.material.LocalMinimumTouchTargetEnforcement
5+
import androidx.compose.ui.Modifier
6+
import androidx.compose.ui.composed
7+
import androidx.compose.ui.layout.LayoutModifier
8+
import androidx.compose.ui.layout.Measurable
9+
import androidx.compose.ui.layout.MeasureResult
10+
import androidx.compose.ui.layout.MeasureScope
11+
import androidx.compose.ui.platform.debugInspectorInfo
12+
import androidx.compose.ui.unit.Constraints
13+
import androidx.compose.ui.unit.DpSize
14+
import androidx.compose.ui.unit.dp
15+
import kotlin.math.roundToInt
16+
17+
@OptIn(ExperimentalMaterialApi::class)
18+
@Suppress("ModifierInspectorInfo")
19+
fun Modifier.minimumTouchTargetSize(): Modifier = composed(
20+
inspectorInfo = debugInspectorInfo {
21+
name = "minimumTouchTargetSize"
22+
23+
properties["README"] = "Adds outer padding to measure at least 48.dp (default) in " +
24+
"size to disambiguate touch interactions if the element would measure smaller"
25+
}
26+
) {
27+
if (LocalMinimumTouchTargetEnforcement.current) {
28+
MinimumTouchTargetModifier(DpSize(48.dp, 48.dp))
29+
} else {
30+
Modifier
31+
}
32+
}
33+
34+
private class MinimumTouchTargetModifier(val size: DpSize) : LayoutModifier {
35+
override fun MeasureScope.measure(
36+
measurable: Measurable,
37+
constraints: Constraints
38+
): MeasureResult {
39+
40+
val placeable = measurable.measure(constraints)
41+
42+
// Be at least as big as the minimum dimension in both dimensions
43+
val width = maxOf(placeable.width, size.width.roundToPx())
44+
val height = maxOf(placeable.height, size.height.roundToPx())
45+
46+
return layout(width, height) {
47+
val centerX = ((width - placeable.width) / 2f).roundToInt()
48+
val centerY = ((height - placeable.height) / 2f).roundToInt()
49+
placeable.place(centerX, centerY)
50+
}
51+
}
52+
53+
override fun equals(other: Any?): Boolean {
54+
val otherModifier = other as? MinimumTouchTargetModifier ?: return false
55+
return size == otherModifier.size
56+
}
57+
58+
override fun hashCode(): Int {
59+
return size.hashCode()
60+
}
61+
}
62+
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package com.smarttoolfactory.slider.gesture
2+
3+
import androidx.compose.foundation.gestures.*
4+
import androidx.compose.ui.input.pointer.*
5+
import kotlinx.coroutines.coroutineScope
6+
import kotlinx.coroutines.delay
7+
import kotlinx.coroutines.launch
8+
9+
/**
10+
* Reads [awaitFirstDown], and [awaitPointerEvent] to
11+
* get [PointerInputChange] and motion event states
12+
* [onDown], [onMove], and [onUp].
13+
*
14+
* To prevent other pointer functions that call [awaitFirstDown] or [awaitPointerEvent]
15+
* (scroll, swipe, detect functions)
16+
* receiving changes call [PointerInputChange.consumeDownChange] in [onDown],
17+
* and call [PointerInputChange.consumePositionChange]
18+
* in [onMove] block.
19+
*
20+
* @param onDown is invoked when first pointer is down initially.
21+
* @param onMove one or multiple pointers are being moved on screen.
22+
* @param onUp last pointer is up
23+
* @param delayAfterDownInMillis is optional delay after [onDown] This delay might be
24+
* required Composables like **Canvas** to process [onDown] before [onMove]
25+
*
26+
27+
*/
28+
suspend fun PointerInputScope.detectMotionEvents(
29+
onDown: (PointerInputChange) -> Unit = {},
30+
onMove: (PointerInputChange) -> Unit = {},
31+
onUp: (PointerInputChange) -> Unit = {},
32+
delayAfterDownInMillis: Long = 0L
33+
) {
34+
coroutineScope {
35+
forEachGesture {
36+
awaitPointerEventScope {
37+
// Wait for at least one pointer to press down, and set first contact position
38+
val down: PointerInputChange = awaitFirstDown()
39+
onDown(down)
40+
41+
var pointer = down
42+
// Main pointer is the one that is down initially
43+
var pointerId = down.id
44+
45+
// If a move event is followed fast enough down is skipped, especially by Canvas
46+
// to prevent it we add delay after first touch
47+
var waitedAfterDown = false
48+
49+
launch {
50+
delay(delayAfterDownInMillis)
51+
waitedAfterDown = true
52+
}
53+
54+
while (true) {
55+
56+
val event: PointerEvent = awaitPointerEvent()
57+
58+
val anyPressed = event.changes.any { it.pressed }
59+
60+
// There are at least one pointer pressed
61+
if (anyPressed) {
62+
// Get pointer that is down, if first pointer is up
63+
// get another and use it if other pointers are also down
64+
// event.changes.first() doesn't return same order
65+
val pointerInputChange =
66+
event.changes.firstOrNull { it.id == pointerId }
67+
?: event.changes.first()
68+
69+
// Next time will check same pointer with this id
70+
pointerId = pointerInputChange.id
71+
pointer = pointerInputChange
72+
73+
if (waitedAfterDown) {
74+
onMove(pointer)
75+
}
76+
} else {
77+
// All of the pointers are up
78+
onUp(pointer)
79+
break
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}
86+
87+
/**
88+
* Reads [awaitFirstDown], and [awaitPointerEvent] to
89+
* get [PointerInputChange] and motion event states
90+
* [onDown], [onMove], and [onUp]. Unlike overload of this function [onMove] returns
91+
* list of [PointerInputChange] to get data about all pointers that are on the screen.
92+
*
93+
* To prevent other pointer functions that call [awaitFirstDown] or [awaitPointerEvent]
94+
* (scroll, swipe, detect functions)
95+
* receiving changes call [PointerInputChange.consumeDownChange] in [onDown],
96+
* and call [PointerInputChange.consumePositionChange]
97+
* in [onMove] block.
98+
*
99+
* @param onDown is invoked when first pointer is down initially.
100+
* @param onMove one or multiple pointers are being moved on screen.
101+
* @param onUp last pointer is up
102+
* @param delayAfterDownInMillis is optional delay after [onDown] This delay might be
103+
* required Composables like **Canvas** to process [onDown] before [onMove]
104+
*
105+
*/
106+
suspend fun PointerInputScope.detectMotionEventsAsList(
107+
onDown: (PointerInputChange) -> Unit = {},
108+
onMove: (List<PointerInputChange>) -> Unit = {},
109+
onUp: (PointerInputChange) -> Unit = {},
110+
delayAfterDownInMillis: Long = 0L
111+
) {
112+
113+
coroutineScope {
114+
forEachGesture {
115+
awaitPointerEventScope {
116+
// Wait for at least one pointer to press down, and set first contact position
117+
val down: PointerInputChange = awaitFirstDown()
118+
onDown(down)
119+
120+
var pointer = down
121+
// Main pointer is the one that is down initially
122+
var pointerId = down.id
123+
124+
// If a move event is followed fast enough down is skipped, especially by Canvas
125+
// to prevent it we add delay after first touch
126+
var waitedAfterDown = false
127+
128+
launch {
129+
delay(delayAfterDownInMillis)
130+
waitedAfterDown = true
131+
}
132+
133+
while (true) {
134+
135+
val event: PointerEvent = awaitPointerEvent()
136+
137+
val anyPressed = event.changes.any { it.pressed }
138+
139+
// There are at least one pointer pressed
140+
if (anyPressed) {
141+
// Get pointer that is down, if first pointer is up
142+
// get another and use it if other pointers are also down
143+
// event.changes.first() doesn't return same order
144+
val pointerInputChange =
145+
event.changes.firstOrNull { it.id == pointerId }
146+
?: event.changes.first()
147+
148+
// Next time will check same pointer with this id
149+
pointerId = pointerInputChange.id
150+
pointer = pointerInputChange
151+
152+
if (waitedAfterDown) {
153+
onMove(event.changes)
154+
}
155+
156+
} else {
157+
// All of the pointers are up
158+
onUp(pointer)
159+
break
160+
}
161+
}
162+
}
163+
}
164+
}
165+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.smarttoolfactory.slider.gesture
2+
3+
enum class MotionEvent {
4+
Idle, Down, Move, Up
5+
}

0 commit comments

Comments
 (0)