Skip to content

Commit efd5da9

Browse files
authored
[Android] Stylus support (#3111)
## Description This PR adds stylus support on Android. >[!NOTE] > You can read more about this feature in #3107 ## Test plan Tested on **_StylusData_** example
1 parent 214663f commit efd5da9

File tree

6 files changed

+137
-0
lines changed

6 files changed

+137
-0
lines changed

android/src/main/java/com/swmansion/gesturehandler/core/GestureUtils.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ object GestureUtils {
4545
event.getY(lastPointerIdx)
4646
}
4747
}
48+
4849
fun coneToDeviation(angle: Double): Double =
4950
cos(Math.toRadians(angle / 2.0))
5051
}

android/src/main/java/com/swmansion/gesturehandler/core/HoverGestureHandler.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import com.swmansion.gesturehandler.react.RNViewConfigurationHelper
1111
class HoverGestureHandler : GestureHandler<HoverGestureHandler>() {
1212
private var handler: Handler? = null
1313
private var finishRunnable = Runnable { finish() }
14+
var stylusData: StylusData = StylusData()
15+
private set
1416

1517
private infix fun isAncestorOf(other: GestureHandler<*>): Boolean {
1618
var current: View? = other.view
@@ -103,6 +105,10 @@ class HoverGestureHandler : GestureHandler<HoverGestureHandler>() {
103105
finish()
104106
}
105107

108+
this.state == STATE_ACTIVE && event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS -> {
109+
stylusData = StylusData.fromEvent(event)
110+
}
111+
106112
this.state == STATE_UNDETERMINED &&
107113
(event.action == MotionEvent.ACTION_HOVER_MOVE || event.action == MotionEvent.ACTION_HOVER_ENTER) -> {
108114
begin()
@@ -111,6 +117,11 @@ class HoverGestureHandler : GestureHandler<HoverGestureHandler>() {
111117
}
112118
}
113119

120+
override fun onReset() {
121+
super.onReset()
122+
stylusData = StylusData()
123+
}
124+
114125
private fun finish() {
115126
when (this.state) {
116127
STATE_UNDETERMINED -> cancel()

android/src/main/java/com/swmansion/gesturehandler/core/PanGestureHandler.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class PanGestureHandler(context: Context?) : GestureHandler<PanGestureHandler>()
4545
private var activateAfterLongPress = DEFAULT_ACTIVATE_AFTER_LONG_PRESS
4646
private val activateDelayed = Runnable { activate() }
4747
private var handler: Handler? = null
48+
var stylusData: StylusData = StylusData()
49+
private set
4850

4951
/**
5052
* On Android when there are multiple pointers on the screen pan gestures most often just consider
@@ -212,6 +214,10 @@ class PanGestureHandler(context: Context?) : GestureHandler<PanGestureHandler>()
212214
return
213215
}
214216

217+
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
218+
stylusData = StylusData.fromEvent(event)
219+
}
220+
215221
val state = state
216222
val action = sourceEvent.actionMasked
217223
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN) {
@@ -295,6 +301,8 @@ class PanGestureHandler(context: Context?) : GestureHandler<PanGestureHandler>()
295301
it.recycle()
296302
velocityTracker = null
297303
}
304+
305+
stylusData = StylusData()
298306
}
299307

300308
override fun resetProgress() {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.swmansion.gesturehandler.core
2+
3+
import android.view.MotionEvent
4+
import com.facebook.react.bridge.Arguments
5+
import com.facebook.react.bridge.ReadableMap
6+
import kotlin.math.PI
7+
import kotlin.math.abs
8+
import kotlin.math.atan
9+
import kotlin.math.cos
10+
import kotlin.math.round
11+
import kotlin.math.sin
12+
import kotlin.math.tan
13+
14+
data class StylusData(
15+
val tiltX: Double = 0.0,
16+
val tiltY: Double = 0.0,
17+
val altitudeAngle: Double = 0.0,
18+
val azimuthAngle: Double = 0.0,
19+
val pressure: Double = -1.0
20+
) {
21+
fun toReadableMap(): ReadableMap {
22+
val stylusDataObject = Arguments.createMap().apply {
23+
putDouble("tiltX", tiltX)
24+
putDouble("tiltY", tiltY)
25+
putDouble("altitudeAngle", altitudeAngle)
26+
putDouble("azimuthAngle", azimuthAngle)
27+
putDouble("pressure", pressure)
28+
}
29+
30+
val readableStylusData: ReadableMap = stylusDataObject
31+
32+
return readableStylusData
33+
}
34+
35+
companion object {
36+
// Source: https://w3c.github.io/pointerevents/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle
37+
private fun spherical2tilt(altitudeAngle: Double, azimuthAngle: Double): Pair<Double, Double> {
38+
val eps = 0.000000001
39+
val radToDeg = 180 / PI
40+
41+
var tiltXrad = 0.0
42+
var tiltYrad = 0.0
43+
44+
if (altitudeAngle < eps) {
45+
// the pen is in the X-Y plane
46+
if (azimuthAngle < eps || abs(azimuthAngle - 2 * PI) < eps) {
47+
// pen is on positive X axis
48+
tiltXrad = PI / 2
49+
}
50+
if (abs(azimuthAngle - PI / 2) < eps) {
51+
// pen is on positive Y axis
52+
tiltYrad = PI / 2
53+
}
54+
if (abs(azimuthAngle - PI) < eps) {
55+
// pen is on negative X axis
56+
tiltXrad = -PI / 2
57+
}
58+
if (abs(azimuthAngle - (3 * PI) / 2) < eps) {
59+
// pen is on negative Y axis
60+
tiltYrad = -PI / 2
61+
}
62+
if (azimuthAngle > eps && abs(azimuthAngle - PI / 2) < eps) {
63+
tiltXrad = PI / 2
64+
tiltYrad = PI / 2
65+
}
66+
if (abs(azimuthAngle - PI / 2) > eps && abs(azimuthAngle - PI) < eps) {
67+
tiltXrad = -PI / 2
68+
tiltYrad = PI / 2
69+
}
70+
if (abs(azimuthAngle - PI) > eps && abs(azimuthAngle - (3 * PI) / 2) < eps) {
71+
tiltXrad = -PI / 2
72+
tiltYrad = -PI / 2
73+
}
74+
if (abs(azimuthAngle - (3 * PI) / 2) > eps && abs(azimuthAngle - 2 * PI) < eps) {
75+
tiltXrad = PI / 2
76+
tiltYrad = -PI / 2
77+
}
78+
} else {
79+
val tanAlt = tan(altitudeAngle)
80+
81+
tiltXrad = atan(cos(azimuthAngle) / tanAlt)
82+
tiltYrad = atan(sin(azimuthAngle) / tanAlt)
83+
}
84+
85+
val tiltX = round(tiltXrad * radToDeg)
86+
val tiltY = round(tiltYrad * radToDeg)
87+
88+
return Pair(tiltX, tiltY)
89+
}
90+
91+
fun fromEvent(event: MotionEvent): StylusData {
92+
// On web and iOS 0 degrees means that stylus is parallel to the surface. On android this value will be PI / 2.
93+
val altitudeAngle = (PI / 2) - event.getAxisValue(MotionEvent.AXIS_TILT).toDouble()
94+
val pressure = event.getPressure(0).toDouble()
95+
val orientation = event.getOrientation(0).toDouble()
96+
// To get azimuth angle, we need to use orientation property (https://developer.android.com/develop/ui/compose/touch-input/stylus-input/advanced-stylus-features#orientation).
97+
val azimuthAngle = (orientation + PI / 2).mod(2 * PI)
98+
val tilts = spherical2tilt(altitudeAngle, azimuthAngle)
99+
100+
return StylusData(tilts.first, tilts.second, altitudeAngle, azimuthAngle, pressure)
101+
}
102+
}
103+
}

android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/HoverGestureHandlerEventDataBuilder.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ package com.swmansion.gesturehandler.react.eventbuilders
33
import com.facebook.react.bridge.WritableMap
44
import com.facebook.react.uimanager.PixelUtil
55
import com.swmansion.gesturehandler.core.HoverGestureHandler
6+
import com.swmansion.gesturehandler.core.StylusData
67

78
class HoverGestureHandlerEventDataBuilder(handler: HoverGestureHandler) : GestureHandlerEventDataBuilder<HoverGestureHandler>(handler) {
89
private val x: Float
910
private val y: Float
1011
private val absoluteX: Float
1112
private val absoluteY: Float
13+
private val stylusData: StylusData
1214

1315
init {
1416
x = handler.lastRelativePositionX
1517
y = handler.lastRelativePositionY
1618
absoluteX = handler.lastPositionInWindowX
1719
absoluteY = handler.lastPositionInWindowY
20+
stylusData = handler.stylusData
1821
}
1922

2023
override fun buildEventData(eventData: WritableMap) {
@@ -25,6 +28,10 @@ class HoverGestureHandlerEventDataBuilder(handler: HoverGestureHandler) : Gestur
2528
putDouble("y", PixelUtil.toDIPFromPixel(y).toDouble())
2629
putDouble("absoluteX", PixelUtil.toDIPFromPixel(absoluteX).toDouble())
2730
putDouble("absoluteY", PixelUtil.toDIPFromPixel(absoluteY).toDouble())
31+
32+
if (stylusData.pressure != -1.0) {
33+
putMap("stylusData", stylusData.toReadableMap())
34+
}
2835
}
2936
}
3037
}

android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PanGestureHandlerEventDataBuilder.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.swmansion.gesturehandler.react.eventbuilders
33
import com.facebook.react.bridge.WritableMap
44
import com.facebook.react.uimanager.PixelUtil
55
import com.swmansion.gesturehandler.core.PanGestureHandler
6+
import com.swmansion.gesturehandler.core.StylusData
67

78
class PanGestureHandlerEventDataBuilder(handler: PanGestureHandler) : GestureHandlerEventDataBuilder<PanGestureHandler>(handler) {
89
private val x: Float
@@ -13,6 +14,7 @@ class PanGestureHandlerEventDataBuilder(handler: PanGestureHandler) : GestureHan
1314
private val translationY: Float
1415
private val velocityX: Float
1516
private val velocityY: Float
17+
private val stylusData: StylusData
1618

1719
init {
1820
x = handler.lastRelativePositionX
@@ -23,6 +25,7 @@ class PanGestureHandlerEventDataBuilder(handler: PanGestureHandler) : GestureHan
2325
translationY = handler.translationY
2426
velocityX = handler.velocityX
2527
velocityY = handler.velocityY
28+
stylusData = handler.stylusData
2629
}
2730

2831
override fun buildEventData(eventData: WritableMap) {
@@ -37,6 +40,10 @@ class PanGestureHandlerEventDataBuilder(handler: PanGestureHandler) : GestureHan
3740
putDouble("translationY", PixelUtil.toDIPFromPixel(translationY).toDouble())
3841
putDouble("velocityX", PixelUtil.toDIPFromPixel(velocityX).toDouble())
3942
putDouble("velocityY", PixelUtil.toDIPFromPixel(velocityY).toDouble())
43+
44+
if (stylusData.pressure != -1.0) {
45+
putMap("stylusData", stylusData.toReadableMap())
46+
}
4047
}
4148
}
4249
}

0 commit comments

Comments
 (0)