Unverified Commit e825dd01 authored by Igor Demin's avatar Igor Demin Committed by GitHub

Implement multitouch (#681)

1. Remove SkikoTouchEvent, we send all pointer events as SkikoPointerEvent now.

2. Add SkikoPointerEvent.Pointer that represents a separate touch/mouse

3. Implement multitouch for iOS/Android
parent ea760f80
...@@ -13,7 +13,7 @@ fun toSkikoKeyboardEvent( ...@@ -13,7 +13,7 @@ fun toSkikoKeyboardEvent(
key = SkikoKey.valueOf(keyCode), key = SkikoKey.valueOf(keyCode),
modifiers = toSkikoModifiers(event), modifiers = toSkikoModifiers(event),
kind = kind, kind = kind,
timestamp = event.getEventTime(), timestamp = event.eventTime,
platform = event platform = event
) )
} }
...@@ -48,21 +48,33 @@ private fun toSkikoModifiers(event: KeyEvent): SkikoInputModifiers { ...@@ -48,21 +48,33 @@ private fun toSkikoModifiers(event: KeyEvent): SkikoInputModifiers {
return SkikoInputModifiers(result) return SkikoInputModifiers(result)
} }
fun toSkikoTouchEvent(event: MotionEvent, index: Int, density: Float): SkikoTouchEvent { fun toSkikoPointerEvent(event: MotionEvent, density: Float): SkikoPointerEvent {
return SkikoTouchEvent( val upIndex = when (event.action) {
x = (event.getX(index) / density).toDouble(), MotionEvent.ACTION_UP -> 0
y = (event.getY(index) / density).toDouble(), MotionEvent.ACTION_POINTER_UP -> event.actionIndex
timestamp = event.getEventTime(), else -> -1
kind = toSkikoTouchEventKind(event), }
platform = event
)
}
fun toSkikoGestureEvent(event: MotionEvent, density: Float): SkikoGestureEvent { val pointers = (0 until event.pointerCount).map {
return SkikoGestureEvent( SkikoPointer(
x = (event.x / density).toDouble(), x = event.getX(it).toDouble() / density,
y = (event.y / density).toDouble(), y = event.getY(it).toDouble() / density,
kind = toSkikoGestureEventKind(event), // Same as in Jetpack Compose for Android
// https://github.com/androidx/androidx/blob/58597f0eba31b89f57b6605b7ed4977cd48ed38d/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt#L126
// (Mouse isn't supported for Android yet, so hover cannot be checked)
pressed = it != upIndex,
device = SkikoPointerDevice.TOUCH,
id = event.getPointerId(it).toLong(),
pressure = event.getPressure(it).toDouble()
)
}
return SkikoPointerEvent(
x = pointers.centroidX,
y = pointers.centroidY,
kind = toSkikoPointerEventKind(event),
timestamp = event.eventTime,
pointers = pointers,
platform = event platform = event
) )
} }
...@@ -114,14 +126,14 @@ internal fun toSkikoGestureDirection( ...@@ -114,14 +126,14 @@ internal fun toSkikoGestureDirection(
return SkikoGestureEventDirection.UNKNOWN return SkikoGestureEventDirection.UNKNOWN
} }
private fun toSkikoTouchEventKind(event: MotionEvent): SkikoTouchEventKind { internal fun toSkikoPointerEventKind(event: MotionEvent): SkikoPointerEventKind {
return when (event.action) { return when (event.action) {
MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_DOWN,
MotionEvent.ACTION_DOWN -> SkikoTouchEventKind.STARTED MotionEvent.ACTION_DOWN -> SkikoPointerEventKind.DOWN
MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_POINTER_UP,
MotionEvent.ACTION_UP -> SkikoTouchEventKind.ENDED MotionEvent.ACTION_UP -> SkikoPointerEventKind.UP
MotionEvent.ACTION_MOVE -> SkikoTouchEventKind.MOVED MotionEvent.ACTION_MOVE -> SkikoPointerEventKind.MOVE
else -> SkikoTouchEventKind.UNKNOWN else -> SkikoPointerEventKind.UNKNOWN
} }
} }
......
...@@ -47,12 +47,7 @@ class SkikoSurfaceView(context: Context, val layer: SkiaLayer) : GLSurfaceView(c ...@@ -47,12 +47,7 @@ class SkikoSurfaceView(context: Context, val layer: SkiaLayer) : GLSurfaceView(c
internal val gesturesDetector = SkikoGesturesDetector(context, layer) internal val gesturesDetector = SkikoGesturesDetector(context, layer)
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
val events: MutableList<SkikoTouchEvent> = mutableListOf() layer.skikoView?.onPointerEvent(toSkikoPointerEvent(event, layer.contentScale))
val count = event.pointerCount
for (index in 0 until count) {
events.add(toSkikoTouchEvent(event, index, layer.contentScale))
}
layer.skikoView?.onTouchEvent(events.toTypedArray())
return true return true
} }
......
...@@ -8,7 +8,6 @@ import org.jetbrains.skia.PixelGeometry ...@@ -8,7 +8,6 @@ import org.jetbrains.skia.PixelGeometry
actual typealias SkikoGesturePlatformEvent = MotionEvent actual typealias SkikoGesturePlatformEvent = MotionEvent
actual typealias SkikoPlatformPointerEvent = MotionEvent actual typealias SkikoPlatformPointerEvent = MotionEvent
actual typealias SkikoTouchPlatformEvent = MotionEvent
actual typealias SkikoPlatformInputEvent = KeyEvent actual typealias SkikoPlatformInputEvent = KeyEvent
actual typealias SkikoPlatformKeyboardEvent = KeyEvent actual typealias SkikoPlatformKeyboardEvent = KeyEvent
......
...@@ -676,7 +676,6 @@ internal fun defaultFPSCounter( ...@@ -676,7 +676,6 @@ internal fun defaultFPSCounter(
} }
// InputEvent is abstract, so we wrap to match modality. // InputEvent is abstract, so we wrap to match modality.
actual typealias SkikoTouchPlatformEvent = Any
actual typealias SkikoGesturePlatformEvent = Any actual typealias SkikoGesturePlatformEvent = Any
actual typealias SkikoPlatformInputEvent = Any actual typealias SkikoPlatformInputEvent = Any
actual typealias SkikoPlatformKeyboardEvent = KeyEvent actual typealias SkikoPlatformKeyboardEvent = KeyEvent
......
...@@ -95,13 +95,13 @@ value class SkikoInputModifiers(val value: Int) { ...@@ -95,13 +95,13 @@ value class SkikoInputModifiers(val value: Int) {
} }
enum class SkikoGestureEventKind { enum class SkikoGestureEventKind {
TAP, DOUBLETAP, LONGPRESS, PAN, PINCH, ROTATION, SWIPE, UNKNOWN UNKNOWN, TAP, DOUBLETAP, LONGPRESS, PAN, PINCH, ROTATION, SWIPE
} }
enum class SkikoGestureEventDirection { enum class SkikoGestureEventDirection {
UP, DOWN, LEFT, RIGHT, UNKNOWN UNKNOWN, UP, DOWN, LEFT, RIGHT
} }
enum class SkikoGestureEventState { enum class SkikoGestureEventState {
PRESSED, STARTED, CHANGED, ENDED, UNKNOWN UNKNOWN, PRESSED, STARTED, CHANGED, ENDED
} }
expect class SkikoGesturePlatformEvent expect class SkikoGesturePlatformEvent
data class SkikoGestureEvent( data class SkikoGestureEvent(
...@@ -116,18 +116,6 @@ data class SkikoGestureEvent( ...@@ -116,18 +116,6 @@ data class SkikoGestureEvent(
val platform: SkikoGesturePlatformEvent? = null val platform: SkikoGesturePlatformEvent? = null
) )
enum class SkikoTouchEventKind {
STARTED, ENDED, MOVED, CANCELLED, UNKNOWN
}
expect class SkikoTouchPlatformEvent
data class SkikoTouchEvent(
val x: Double,
val y: Double,
val kind: SkikoTouchEventKind = SkikoTouchEventKind.UNKNOWN,
val timestamp: Long = 0,
val platform: SkikoTouchPlatformEvent? = null
)
expect class SkikoPlatformInputEvent expect class SkikoPlatformInputEvent
data class SkikoInputEvent( data class SkikoInputEvent(
val input: String, val input: String,
...@@ -138,7 +126,7 @@ data class SkikoInputEvent( ...@@ -138,7 +126,7 @@ data class SkikoInputEvent(
) )
enum class SkikoKeyboardEventKind { enum class SkikoKeyboardEventKind {
UP, DOWN, TYPE, UNKNOWN UNKNOWN, UP, DOWN, TYPE
} }
expect class SkikoPlatformKeyboardEvent expect class SkikoPlatformKeyboardEvent
data class SkikoKeyboardEvent( data class SkikoKeyboardEvent(
...@@ -150,21 +138,45 @@ data class SkikoKeyboardEvent( ...@@ -150,21 +138,45 @@ data class SkikoKeyboardEvent(
) )
enum class SkikoPointerEventKind { enum class SkikoPointerEventKind {
UP, DOWN, MOVE, DRAG, SCROLL, ENTER, EXIT, UNKNOWN UNKNOWN, UP, DOWN, MOVE, DRAG, SCROLL, ENTER, EXIT
} }
expect class SkikoPlatformPointerEvent expect class SkikoPlatformPointerEvent
// TODO(https://github.com/JetBrains/skiko/issues/680) refactor API
data class SkikoPointerEvent( data class SkikoPointerEvent(
/**
* X position in points (scaled pixels that depend on the scale factor of the current display).
*
* If the event contains multiple pointers, it represents the center of all pointers.
*/
val x: Double, val x: Double,
/**
* Y position in points (scaled pixels that depend on the scale factor of the current display)
*
* If the event contains multiple pointers, it represents the center of all pointers.
*/
val y: Double, val y: Double,
val kind: SkikoPointerEventKind,
/**
* Scroll delta along the X axis
*/
val deltaX: Double = 0.0, val deltaX: Double = 0.0,
/**
* Scroll delta along the Y axis
*/
val deltaY: Double = 0.0, val deltaY: Double = 0.0,
val pressedButtons: SkikoMouseButtons = SkikoMouseButtons.NONE, val pressedButtons: SkikoMouseButtons = SkikoMouseButtons.NONE,
val button: SkikoMouseButtons = SkikoMouseButtons.NONE, val button: SkikoMouseButtons = SkikoMouseButtons.NONE,
val modifiers: SkikoInputModifiers = SkikoInputModifiers.EMPTY, val modifiers: SkikoInputModifiers = SkikoInputModifiers.EMPTY,
val kind: SkikoPointerEventKind, /**
* Timestamp in milliseconds
*/
val timestamp: Long = 0, val timestamp: Long = 0,
val platform: SkikoPlatformPointerEvent? val pointers: List<SkikoPointer> = listOf(
SkikoPointer(0, x, y, pressedButtons.has(SkikoMouseButtons.LEFT))
),
val platform: SkikoPlatformPointerEvent? = null
) )
val SkikoPointerEvent.isLeftClick: Boolean val SkikoPointerEvent.isLeftClick: Boolean
...@@ -176,3 +188,54 @@ val SkikoPointerEvent.isRightClick: Boolean ...@@ -176,3 +188,54 @@ val SkikoPointerEvent.isRightClick: Boolean
val SkikoPointerEvent.isMiddleClick: Boolean val SkikoPointerEvent.isMiddleClick: Boolean
get() = button.has(SkikoMouseButtons.MIDDLE) && (kind == SkikoPointerEventKind.UP) get() = button.has(SkikoMouseButtons.MIDDLE) && (kind == SkikoPointerEventKind.UP)
/**
* The device type that produces pointer events, such as a mouse or stylus.
*/
enum class SkikoPointerDevice {
UNKNOWN, MOUSE, TOUCH
}
/**
* Represents pointer such as mouse cursor, or touch/stylus press.
* There can be multiple pointers on the screen at the same time.
*/
data class SkikoPointer(
/**
* Unique id associated with the pointer. Used to distinguish between multiple pointers that can exist
* at the same time (i.e. multiple pressed touches).
*
* If there is only on pointer in the system (for example, one mouse), it should always
* have the same id across multiple events.
*/
val id: Long,
/**
* X position in points (scaled pixels that depend on the scale factor of the current display)
*/
val x: Double,
/**
* Y position in points (scaled pixels that depend on the scale factor of the current display)
*/
val y: Double,
/**
* `true` if the pointer event is considered "pressed." For example, finger
* touching the screen or a mouse button is pressed [pressed] would be `true`.
* During the up event, pointer is considered not pressed.
*/
val pressed: Boolean,
/**
* The device type associated with the pointer, such as [mouse][SkikoPointerDevice.MOUSE],
* or [touch][SkikoPointerDevice.TOUCH].
*/
val device: SkikoPointerDevice = SkikoPointerDevice.MOUSE,
/**
* Pressure of the pointer. 0.0 - no pressure, 1.0 - average pressure
*/
val pressure: Double = 1.0,
)
internal val Iterable<SkikoPointer>.centroidX get() = asSequence().map { it.x }.average()
internal val Iterable<SkikoPointer>.centroidY get() = asSequence().map { it.y }.average()
\ No newline at end of file
...@@ -10,7 +10,6 @@ interface SkikoView { ...@@ -10,7 +10,6 @@ interface SkikoView {
@Deprecated("This method will be removed. Use override val input: SkikoInput") @Deprecated("This method will be removed. Use override val input: SkikoInput")
fun onInputEvent(event: SkikoInputEvent) = Unit fun onInputEvent(event: SkikoInputEvent) = Unit
val input: SkikoInput get() = SkikoInput.Empty val input: SkikoInput get() = SkikoInput.Empty
fun onTouchEvent(events: Array<SkikoTouchEvent>) = Unit
fun onGestureEvent(event: SkikoGestureEvent) = Unit fun onGestureEvent(event: SkikoGestureEvent) = Unit
// Rendering // Rendering
...@@ -44,10 +43,6 @@ open class GenericSkikoView( ...@@ -44,10 +43,6 @@ open class GenericSkikoView(
app.onPointerEvent(event) app.onPointerEvent(event)
} }
override fun onTouchEvent(events: Array<SkikoTouchEvent>) {
app.onTouchEvent(events)
}
override fun onGestureEvent(event: SkikoGestureEvent) { override fun onGestureEvent(event: SkikoGestureEvent) {
app.onGestureEvent(event) app.onGestureEvent(event)
} }
......
...@@ -114,7 +114,6 @@ actual open class SkiaLayer { ...@@ -114,7 +114,6 @@ actual open class SkiaLayer {
} }
// TODO: do properly // TODO: do properly
actual typealias SkikoTouchPlatformEvent = UITouch
actual typealias SkikoGesturePlatformEvent = UIEvent actual typealias SkikoGesturePlatformEvent = UIEvent
actual typealias SkikoPlatformInputEvent = UIPress actual typealias SkikoPlatformInputEvent = UIPress
actual typealias SkikoPlatformKeyboardEvent = UIPress actual typealias SkikoPlatformKeyboardEvent = UIPress
......
...@@ -24,6 +24,10 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol, ...@@ -24,6 +24,10 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol,
@OverrideInit @OverrideInit
constructor(coder: NSCoder) : super(coder) constructor(coder: NSCoder) : super(coder)
init {
multipleTouchEnabled = true
}
private var skiaLayer: SkiaLayer? = null private var skiaLayer: SkiaLayer? = null
private lateinit var _pointInside: (Point, UIEvent?) -> Boolean private lateinit var _pointInside: (Point, UIEvent?) -> Boolean
private var _inputDelegate: UITextInputDelegateProtocol? = null private var _inputDelegate: UITextInputDelegateProtocol? = null
...@@ -179,34 +183,54 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol, ...@@ -179,34 +183,54 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol,
override fun touchesBegan(touches: Set<*>, withEvent: UIEvent?) { override fun touchesBegan(touches: Set<*>, withEvent: UIEvent?) {
super.touchesBegan(touches, withEvent) super.touchesBegan(touches, withEvent)
sendTouchEventToSkikoView(touches, SkikoTouchEventKind.STARTED) sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.DOWN)
} }
override fun touchesEnded(touches: Set<*>, withEvent: UIEvent?) { override fun touchesEnded(touches: Set<*>, withEvent: UIEvent?) {
super.touchesEnded(touches, withEvent) super.touchesEnded(touches, withEvent)
sendTouchEventToSkikoView(touches, SkikoTouchEventKind.ENDED) sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP)
} }
override fun touchesMoved(touches: Set<*>, withEvent: UIEvent?) { override fun touchesMoved(touches: Set<*>, withEvent: UIEvent?) {
super.touchesMoved(touches, withEvent) super.touchesMoved(touches, withEvent)
sendTouchEventToSkikoView(touches, SkikoTouchEventKind.MOVED) sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.MOVE)
} }
override fun touchesCancelled(touches: Set<*>, withEvent: UIEvent?) { override fun touchesCancelled(touches: Set<*>, withEvent: UIEvent?) {
super.touchesCancelled(touches, withEvent) super.touchesCancelled(touches, withEvent)
sendTouchEventToSkikoView(touches, SkikoTouchEventKind.CANCELLED) sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP)
} }
private fun sendTouchEventToSkikoView(event: UIEvent, kind: SkikoPointerEventKind) {
val pointers = event.touchesForView(this).orEmpty().map {
val touch = it as UITouch
val (x, y) = touch.locationInView(null).useContents { x to y }
SkikoPointer(
x = x,
y = y,
pressed = touch.isPressed,
device = SkikoPointerDevice.TOUCH,
id = touch.hashCode().toLong(),
pressure = touch.force
)
}
private fun sendTouchEventToSkikoView(touches: Set<*>, kind: SkikoTouchEventKind) { skiaLayer?.skikoView?.onPointerEvent(
val events = touches.map { SkikoPointerEvent(
val event = it as UITouch x = pointers.centroidX,
val (x, y) = event.locationInView(null).useContents { x to y } y = pointers.centroidY,
val timestamp = (event.timestamp * 1_000).toLong() kind = kind,
SkikoTouchEvent(x, y, kind, timestamp, event) timestamp = event.timestamp.toLong() * 1_000,
}.toTypedArray() pointers = pointers,
skiaLayer?.skikoView?.onTouchEvent(events) platform = event
)
)
} }
private val UITouch.isPressed get() =
phase != UITouchPhase.UITouchPhaseEnded &&
phase != UITouchPhase.UITouchPhaseCancelled
override fun inputDelegate(): UITextInputDelegateProtocol? { override fun inputDelegate(): UITextInputDelegateProtocol? {
return _inputDelegate return _inputDelegate
} }
......
...@@ -160,7 +160,6 @@ actual open class SkiaLayer { ...@@ -160,7 +160,6 @@ actual open class SkiaLayer {
var onContentScaleChanged: ((Float) -> Unit)? = null var onContentScaleChanged: ((Float) -> Unit)? = null
actual typealias SkikoTouchPlatformEvent = Any
actual typealias SkikoGesturePlatformEvent = Any actual typealias SkikoGesturePlatformEvent = Any
actual typealias SkikoPlatformInputEvent = KeyboardEvent actual typealias SkikoPlatformInputEvent = KeyboardEvent
actual typealias SkikoPlatformKeyboardEvent = KeyboardEvent actual typealias SkikoPlatformKeyboardEvent = KeyboardEvent
......
...@@ -37,7 +37,6 @@ actual open class SkiaLayer { ...@@ -37,7 +37,6 @@ actual open class SkiaLayer {
} }
// TODO: do properly // TODO: do properly
actual typealias SkikoTouchPlatformEvent = Any
actual typealias SkikoGesturePlatformEvent = Any actual typealias SkikoGesturePlatformEvent = Any
actual typealias SkikoPlatformInputEvent = Any actual typealias SkikoPlatformInputEvent = Any
actual typealias SkikoPlatformKeyboardEvent = Any actual typealias SkikoPlatformKeyboardEvent = Any
......
...@@ -290,7 +290,6 @@ actual open class SkiaLayer { ...@@ -290,7 +290,6 @@ actual open class SkiaLayer {
} }
// TODO: do properly // TODO: do properly
actual typealias SkikoTouchPlatformEvent = NSEvent
actual typealias SkikoGesturePlatformEvent = NSEvent actual typealias SkikoGesturePlatformEvent = NSEvent
actual typealias SkikoPlatformInputEvent = NSEvent actual typealias SkikoPlatformInputEvent = NSEvent
actual typealias SkikoPlatformKeyboardEvent = NSEvent actual typealias SkikoPlatformKeyboardEvent = NSEvent
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment