Skip to content

Commit

Permalink
[Android] Stylus support (#3111)
Browse files Browse the repository at this point in the history
## Description

This PR adds stylus support on Android.

>[!NOTE]
> You can read more about this feature in #3107

## Test plan

Tested on **_StylusData_** example
  • Loading branch information
m-bert authored Sep 24, 2024
1 parent 214663f commit efd5da9
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ object GestureUtils {
event.getY(lastPointerIdx)
}
}

fun coneToDeviation(angle: Double): Double =
cos(Math.toRadians(angle / 2.0))
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import com.swmansion.gesturehandler.react.RNViewConfigurationHelper
class HoverGestureHandler : GestureHandler<HoverGestureHandler>() {
private var handler: Handler? = null
private var finishRunnable = Runnable { finish() }
var stylusData: StylusData = StylusData()
private set

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

this.state == STATE_ACTIVE && event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS -> {
stylusData = StylusData.fromEvent(event)
}

this.state == STATE_UNDETERMINED &&
(event.action == MotionEvent.ACTION_HOVER_MOVE || event.action == MotionEvent.ACTION_HOVER_ENTER) -> {
begin()
Expand All @@ -111,6 +117,11 @@ class HoverGestureHandler : GestureHandler<HoverGestureHandler>() {
}
}

override fun onReset() {
super.onReset()
stylusData = StylusData()
}

private fun finish() {
when (this.state) {
STATE_UNDETERMINED -> cancel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class PanGestureHandler(context: Context?) : GestureHandler<PanGestureHandler>()
private var activateAfterLongPress = DEFAULT_ACTIVATE_AFTER_LONG_PRESS
private val activateDelayed = Runnable { activate() }
private var handler: Handler? = null
var stylusData: StylusData = StylusData()
private set

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

if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
stylusData = StylusData.fromEvent(event)
}

val state = state
val action = sourceEvent.actionMasked
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN) {
Expand Down Expand Up @@ -295,6 +301,8 @@ class PanGestureHandler(context: Context?) : GestureHandler<PanGestureHandler>()
it.recycle()
velocityTracker = null
}

stylusData = StylusData()
}

override fun resetProgress() {
Expand Down
103 changes: 103 additions & 0 deletions android/src/main/java/com/swmansion/gesturehandler/core/StylusData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.swmansion.gesturehandler.core

import android.view.MotionEvent
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableMap
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.atan
import kotlin.math.cos
import kotlin.math.round
import kotlin.math.sin
import kotlin.math.tan

data class StylusData(
val tiltX: Double = 0.0,
val tiltY: Double = 0.0,
val altitudeAngle: Double = 0.0,
val azimuthAngle: Double = 0.0,
val pressure: Double = -1.0
) {
fun toReadableMap(): ReadableMap {
val stylusDataObject = Arguments.createMap().apply {
putDouble("tiltX", tiltX)
putDouble("tiltY", tiltY)
putDouble("altitudeAngle", altitudeAngle)
putDouble("azimuthAngle", azimuthAngle)
putDouble("pressure", pressure)
}

val readableStylusData: ReadableMap = stylusDataObject

return readableStylusData
}

companion object {
// Source: https://w3c.github.io/pointerevents/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle
private fun spherical2tilt(altitudeAngle: Double, azimuthAngle: Double): Pair<Double, Double> {
val eps = 0.000000001
val radToDeg = 180 / PI

var tiltXrad = 0.0
var tiltYrad = 0.0

if (altitudeAngle < eps) {
// the pen is in the X-Y plane
if (azimuthAngle < eps || abs(azimuthAngle - 2 * PI) < eps) {
// pen is on positive X axis
tiltXrad = PI / 2
}
if (abs(azimuthAngle - PI / 2) < eps) {
// pen is on positive Y axis
tiltYrad = PI / 2
}
if (abs(azimuthAngle - PI) < eps) {
// pen is on negative X axis
tiltXrad = -PI / 2
}
if (abs(azimuthAngle - (3 * PI) / 2) < eps) {
// pen is on negative Y axis
tiltYrad = -PI / 2
}
if (azimuthAngle > eps && abs(azimuthAngle - PI / 2) < eps) {
tiltXrad = PI / 2
tiltYrad = PI / 2
}
if (abs(azimuthAngle - PI / 2) > eps && abs(azimuthAngle - PI) < eps) {
tiltXrad = -PI / 2
tiltYrad = PI / 2
}
if (abs(azimuthAngle - PI) > eps && abs(azimuthAngle - (3 * PI) / 2) < eps) {
tiltXrad = -PI / 2
tiltYrad = -PI / 2
}
if (abs(azimuthAngle - (3 * PI) / 2) > eps && abs(azimuthAngle - 2 * PI) < eps) {
tiltXrad = PI / 2
tiltYrad = -PI / 2
}
} else {
val tanAlt = tan(altitudeAngle)

tiltXrad = atan(cos(azimuthAngle) / tanAlt)
tiltYrad = atan(sin(azimuthAngle) / tanAlt)
}

val tiltX = round(tiltXrad * radToDeg)
val tiltY = round(tiltYrad * radToDeg)

return Pair(tiltX, tiltY)
}

fun fromEvent(event: MotionEvent): StylusData {
// On web and iOS 0 degrees means that stylus is parallel to the surface. On android this value will be PI / 2.
val altitudeAngle = (PI / 2) - event.getAxisValue(MotionEvent.AXIS_TILT).toDouble()
val pressure = event.getPressure(0).toDouble()
val orientation = event.getOrientation(0).toDouble()
// 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).
val azimuthAngle = (orientation + PI / 2).mod(2 * PI)
val tilts = spherical2tilt(altitudeAngle, azimuthAngle)

return StylusData(tilts.first, tilts.second, altitudeAngle, azimuthAngle, pressure)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ package com.swmansion.gesturehandler.react.eventbuilders
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.PixelUtil
import com.swmansion.gesturehandler.core.HoverGestureHandler
import com.swmansion.gesturehandler.core.StylusData

class HoverGestureHandlerEventDataBuilder(handler: HoverGestureHandler) : GestureHandlerEventDataBuilder<HoverGestureHandler>(handler) {
private val x: Float
private val y: Float
private val absoluteX: Float
private val absoluteY: Float
private val stylusData: StylusData

init {
x = handler.lastRelativePositionX
y = handler.lastRelativePositionY
absoluteX = handler.lastPositionInWindowX
absoluteY = handler.lastPositionInWindowY
stylusData = handler.stylusData
}

override fun buildEventData(eventData: WritableMap) {
Expand All @@ -25,6 +28,10 @@ class HoverGestureHandlerEventDataBuilder(handler: HoverGestureHandler) : Gestur
putDouble("y", PixelUtil.toDIPFromPixel(y).toDouble())
putDouble("absoluteX", PixelUtil.toDIPFromPixel(absoluteX).toDouble())
putDouble("absoluteY", PixelUtil.toDIPFromPixel(absoluteY).toDouble())

if (stylusData.pressure != -1.0) {
putMap("stylusData", stylusData.toReadableMap())
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.swmansion.gesturehandler.react.eventbuilders
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.PixelUtil
import com.swmansion.gesturehandler.core.PanGestureHandler
import com.swmansion.gesturehandler.core.StylusData

class PanGestureHandlerEventDataBuilder(handler: PanGestureHandler) : GestureHandlerEventDataBuilder<PanGestureHandler>(handler) {
private val x: Float
Expand All @@ -13,6 +14,7 @@ class PanGestureHandlerEventDataBuilder(handler: PanGestureHandler) : GestureHan
private val translationY: Float
private val velocityX: Float
private val velocityY: Float
private val stylusData: StylusData

init {
x = handler.lastRelativePositionX
Expand All @@ -23,6 +25,7 @@ class PanGestureHandlerEventDataBuilder(handler: PanGestureHandler) : GestureHan
translationY = handler.translationY
velocityX = handler.velocityX
velocityY = handler.velocityY
stylusData = handler.stylusData
}

override fun buildEventData(eventData: WritableMap) {
Expand All @@ -37,6 +40,10 @@ class PanGestureHandlerEventDataBuilder(handler: PanGestureHandler) : GestureHan
putDouble("translationY", PixelUtil.toDIPFromPixel(translationY).toDouble())
putDouble("velocityX", PixelUtil.toDIPFromPixel(velocityX).toDouble())
putDouble("velocityY", PixelUtil.toDIPFromPixel(velocityY).toDouble())

if (stylusData.pressure != -1.0) {
putMap("stylusData", stylusData.toReadableMap())
}
}
}
}

0 comments on commit efd5da9

Please sign in to comment.