diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/GestureUtils.kt b/android/src/main/java/com/swmansion/gesturehandler/core/GestureUtils.kt index 46f509f039..37d16841ef 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/core/GestureUtils.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/core/GestureUtils.kt @@ -45,6 +45,7 @@ object GestureUtils { event.getY(lastPointerIdx) } } + fun coneToDeviation(angle: Double): Double = cos(Math.toRadians(angle / 2.0)) } diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/HoverGestureHandler.kt b/android/src/main/java/com/swmansion/gesturehandler/core/HoverGestureHandler.kt index c90bc878c4..9a4b1fe14d 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/core/HoverGestureHandler.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/core/HoverGestureHandler.kt @@ -11,6 +11,8 @@ import com.swmansion.gesturehandler.react.RNViewConfigurationHelper class HoverGestureHandler : GestureHandler() { 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 @@ -103,6 +105,10 @@ class HoverGestureHandler : GestureHandler() { 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() @@ -111,6 +117,11 @@ class HoverGestureHandler : GestureHandler() { } } + override fun onReset() { + super.onReset() + stylusData = StylusData() + } + private fun finish() { when (this.state) { STATE_UNDETERMINED -> cancel() diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/PanGestureHandler.kt b/android/src/main/java/com/swmansion/gesturehandler/core/PanGestureHandler.kt index c6a27998e7..6b46c7cb2a 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/core/PanGestureHandler.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/core/PanGestureHandler.kt @@ -45,6 +45,8 @@ class PanGestureHandler(context: Context?) : GestureHandler() 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 @@ -212,6 +214,10 @@ class PanGestureHandler(context: Context?) : GestureHandler() 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) { @@ -295,6 +301,8 @@ class PanGestureHandler(context: Context?) : GestureHandler() it.recycle() velocityTracker = null } + + stylusData = StylusData() } override fun resetProgress() { diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/StylusData.kt b/android/src/main/java/com/swmansion/gesturehandler/core/StylusData.kt new file mode 100644 index 0000000000..13ede0a1b7 --- /dev/null +++ b/android/src/main/java/com/swmansion/gesturehandler/core/StylusData.kt @@ -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 { + 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) + } + } +} diff --git a/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/HoverGestureHandlerEventDataBuilder.kt b/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/HoverGestureHandlerEventDataBuilder.kt index e4103c9a6e..9700d140e1 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/HoverGestureHandlerEventDataBuilder.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/HoverGestureHandlerEventDataBuilder.kt @@ -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(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) { @@ -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()) + } } } } diff --git a/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PanGestureHandlerEventDataBuilder.kt b/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PanGestureHandlerEventDataBuilder.kt index 911bf4efec..b7b3bec317 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PanGestureHandlerEventDataBuilder.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PanGestureHandlerEventDataBuilder.kt @@ -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(handler) { private val x: Float @@ -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 @@ -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) { @@ -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()) + } } } }