Unverified Commit bf60d5f3 authored by Roman Sedaikin's avatar Roman Sedaikin Committed by GitHub

Added skiko events for android. (#463)

* Added skiko events for android. Added DOUBLETAP gesture.

* More flexible build processs
Co-authored-by: 's avatarNikolay Igotti <igotti@gmail.com>
parent f9444dab
...@@ -112,6 +112,13 @@ class Clocks(private val layer: SkiaLayer): SkikoView { ...@@ -112,6 +112,13 @@ class Clocks(private val layer: SkiaLayer): SkikoView {
canvas.resetMatrix() canvas.resetMatrix()
} }
private fun reset() {
xOffset = 0.0
yOffset = 0.0
rotate = 0.0
scale = 1.0
}
override fun onPointerEvent(event: SkikoPointerEvent) { override fun onPointerEvent(event: SkikoPointerEvent) {
when (event.kind) { when (event.kind) {
SkikoPointerEventKind.DOWN, SkikoPointerEventKind.DOWN,
...@@ -152,20 +159,23 @@ class Clocks(private val layer: SkiaLayer): SkikoView { ...@@ -152,20 +159,23 @@ class Clocks(private val layer: SkiaLayer): SkikoView {
when (event.key) { when (event.key) {
SkikoKey.KEY_NUMPAD_ADD -> scale *= 1.1 SkikoKey.KEY_NUMPAD_ADD -> scale *= 1.1
SkikoKey.KEY_I -> { SkikoKey.KEY_I -> {
if (event.modifiers == SkikoInputModifiers.CONTROL) { if (event.modifiers == SkikoInputModifiers.SHIFT) {
scale *= 1.1 scale *= 1.1
} }
} }
SkikoKey.KEY_NUMPAD_SUBTRACT -> scale *= 0.9 SkikoKey.KEY_NUMPAD_SUBTRACT -> scale *= 0.9
SkikoKey.KEY_O -> { SkikoKey.KEY_O -> {
if (event.modifiers == SkikoInputModifiers.CONTROL) { if (event.modifiers == SkikoInputModifiers.SHIFT) {
scale *= 0.9 scale *= 0.9
} }
} }
SkikoKey.KEY_R -> { SkikoKey.KEY_R -> {
if (event.modifiers == SkikoInputModifiers.SHIFT) { if (event.modifiers == SkikoInputModifiers.SHIFT) {
rotate -= 5.0 rotate -= 5.0
} else if (event.modifiers == SkikoInputModifiers.CONTROL) { }
}
SkikoKey.KEY_L -> {
if (event.modifiers == SkikoInputModifiers.SHIFT) {
rotate += 5.0 rotate += 5.0
} }
} }
...@@ -177,12 +187,7 @@ class Clocks(private val layer: SkiaLayer): SkikoView { ...@@ -177,12 +187,7 @@ class Clocks(private val layer: SkiaLayer): SkikoView {
SkikoKey.KEY_RIGHT -> xOffset += 5.0 SkikoKey.KEY_RIGHT -> xOffset += 5.0
SkikoKey.KEY_NUMPAD_2, SkikoKey.KEY_NUMPAD_2,
SkikoKey.KEY_DOWN -> yOffset += 5.0 SkikoKey.KEY_DOWN -> yOffset += 5.0
SkikoKey.KEY_SPACE -> { SkikoKey.KEY_SPACE -> { reset() }
xOffset = 0.0
yOffset = 0.0
rotate = 0.0
scale = 1.0
}
SkikoKey.KEY_BACKSPACE -> { SkikoKey.KEY_BACKSPACE -> {
if (inputText.isNotEmpty()) { if (inputText.isNotEmpty()) {
inputText = inputText.dropLast(1) inputText = inputText.dropLast(1)
...@@ -207,6 +212,7 @@ class Clocks(private val layer: SkiaLayer): SkikoView { ...@@ -207,6 +212,7 @@ class Clocks(private val layer: SkiaLayer): SkikoView {
xpos = event.x xpos = event.x
ypos = event.y ypos = event.y
} }
SkikoGestureEventKind.DOUBLETAP -> { reset() }
SkikoGestureEventKind.PINCH -> { SkikoGestureEventKind.PINCH -> {
if (event.state == SkikoGestureEventState.STARTED) { if (event.state == SkikoGestureEventState.STARTED) {
k = scale k = scale
......
...@@ -2,9 +2,11 @@ package org.jetbrains.skiko.sample ...@@ -2,9 +2,11 @@ package org.jetbrains.skiko.sample
import android.app.Activity import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
import android.widget.LinearLayout import android.widget.LinearLayout
import org.jetbrains.skiko.GenericSkikoView import org.jetbrains.skiko.GenericSkikoView
import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkikoGestureEventKind
class MainActivity : Activity() { class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
...@@ -19,7 +21,9 @@ class MainActivity : Activity() { ...@@ -19,7 +21,9 @@ class MainActivity : Activity() {
//layoutParams = ViewGroup.LayoutParams(1000, 1200) //layoutParams = ViewGroup.LayoutParams(1000, 1200)
} }
val skiaLayer = SkiaLayer() val skiaLayer = SkiaLayer().apply {
gesturesToListen = SkikoGestureEventKind.values()
}
skiaLayer.skikoView = GenericSkikoView(skiaLayer, Clocks(skiaLayer)) skiaLayer.skikoView = GenericSkikoView(skiaLayer, Clocks(skiaLayer))
skiaLayer.attachTo(holder) skiaLayer.attachTo(holder)
layout.addView(holder) layout.addView(holder)
......
...@@ -113,6 +113,13 @@ class Clocks(private val layer: SkiaLayer): SkikoView { ...@@ -113,6 +113,13 @@ class Clocks(private val layer: SkiaLayer): SkikoView {
canvas.resetMatrix() canvas.resetMatrix()
} }
private fun reset() {
xOffset = 0.0
yOffset = 0.0
rotate = 0.0
scale = 1.0
}
override fun onPointerEvent(event: SkikoPointerEvent) { override fun onPointerEvent(event: SkikoPointerEvent) {
when (event.kind) { when (event.kind) {
SkikoPointerEventKind.DOWN, SkikoPointerEventKind.DOWN,
...@@ -153,20 +160,23 @@ class Clocks(private val layer: SkiaLayer): SkikoView { ...@@ -153,20 +160,23 @@ class Clocks(private val layer: SkiaLayer): SkikoView {
when (event.key) { when (event.key) {
SkikoKey.KEY_NUMPAD_ADD -> scale *= 1.1 SkikoKey.KEY_NUMPAD_ADD -> scale *= 1.1
SkikoKey.KEY_I -> { SkikoKey.KEY_I -> {
if (event.modifiers == SkikoInputModifiers.CONTROL) { if (event.modifiers == SkikoInputModifiers.SHIFT) {
scale *= 1.1 scale *= 1.1
} }
} }
SkikoKey.KEY_NUMPAD_SUBTRACT -> scale *= 0.9 SkikoKey.KEY_NUMPAD_SUBTRACT -> scale *= 0.9
SkikoKey.KEY_O -> { SkikoKey.KEY_O -> {
if (event.modifiers == SkikoInputModifiers.CONTROL) { if (event.modifiers == SkikoInputModifiers.SHIFT) {
scale *= 0.9 scale *= 0.9
} }
} }
SkikoKey.KEY_R -> { SkikoKey.KEY_R -> {
if (event.modifiers == SkikoInputModifiers.SHIFT) { if (event.modifiers == SkikoInputModifiers.SHIFT) {
rotate -= 5.0 rotate -= 5.0
} else if (event.modifiers == SkikoInputModifiers.CONTROL) { }
}
SkikoKey.KEY_L -> {
if (event.modifiers == SkikoInputModifiers.SHIFT) {
rotate += 5.0 rotate += 5.0
} }
} }
...@@ -178,12 +188,7 @@ class Clocks(private val layer: SkiaLayer): SkikoView { ...@@ -178,12 +188,7 @@ class Clocks(private val layer: SkiaLayer): SkikoView {
SkikoKey.KEY_RIGHT -> xOffset += 5.0 SkikoKey.KEY_RIGHT -> xOffset += 5.0
SkikoKey.KEY_NUMPAD_2, SkikoKey.KEY_NUMPAD_2,
SkikoKey.KEY_DOWN -> yOffset += 5.0 SkikoKey.KEY_DOWN -> yOffset += 5.0
SkikoKey.KEY_SPACE -> { SkikoKey.KEY_SPACE -> { reset() }
xOffset = 0.0
yOffset = 0.0
rotate = 0.0
scale = 1.0
}
SkikoKey.KEY_BACKSPACE -> { SkikoKey.KEY_BACKSPACE -> {
if (inputText.isNotEmpty()) { if (inputText.isNotEmpty()) {
inputText = inputText.dropLast(1) inputText = inputText.dropLast(1)
...@@ -208,6 +213,7 @@ class Clocks(private val layer: SkiaLayer): SkikoView { ...@@ -208,6 +213,7 @@ class Clocks(private val layer: SkiaLayer): SkikoView {
xpos = event.x xpos = event.x
ypos = event.y ypos = event.y
} }
SkikoGestureEventKind.DOUBLETAP -> { reset() }
SkikoGestureEventKind.PINCH -> { SkikoGestureEventKind.PINCH -> {
if (event.state == SkikoGestureEventState.STARTED) { if (event.state == SkikoGestureEventState.STARTED) {
k = scale k = scale
......
...@@ -40,7 +40,8 @@ class SkikoAppDelegate : UIResponder, UIApplicationDelegateProtocol { ...@@ -40,7 +40,8 @@ class SkikoAppDelegate : UIResponder, UIApplicationDelegateProtocol {
window = UIWindow(frame = UIScreen.mainScreen.bounds) window = UIWindow(frame = UIScreen.mainScreen.bounds)
window!!.rootViewController = SkikoViewController( window!!.rootViewController = SkikoViewController(
SkikoUIView( SkikoUIView(
SkiaLayer(SkikoGestureEventKind.values()).apply { SkiaLayer().apply {
gesturesToListen = SkikoGestureEventKind.values()
skikoView = GenericSkikoView(this, makeApp(this)) skikoView = GenericSkikoView(this, makeApp(this))
} }
) )
......
...@@ -713,8 +713,10 @@ fun androidClangFor(targetArch: Arch, version: String = "30"): String { ...@@ -713,8 +713,10 @@ fun androidClangFor(targetArch: Arch, version: String = "30"): String {
} }
val ndkHome = if (System.getenv("ANDROID_NDK_HOME").isNullOrEmpty()) { val ndkHome = if (System.getenv("ANDROID_NDK_HOME").isNullOrEmpty()) {
val androidHome = androidHome() val androidHome = androidHome()
val ndkDir1 = file("$androidHome/ndk")
val candidates1 = if (ndkDir1.exists()) ndkDir1.list() else emptyArray()
val ndkVersion = val ndkVersion =
arrayOf(*(file("$androidHome/ndk").list().map { "ndk/$it" }.sortedDescending()).toTypedArray(), "ndk-bundle").find { arrayOf(*(candidates1.map { "ndk/$it" }.sortedDescending()).toTypedArray(), "ndk-bundle").find {
androidHome.resolve(it).exists() androidHome.resolve(it).exists()
} ?: throw GradleException("Cannot find NDK, is it installed (Tools/SDK Manager)?") } ?: throw GradleException("Cannot find NDK, is it installed (Tools/SDK Manager)?")
"$androidHome/$ndkVersion" "$androidHome/$ndkVersion"
...@@ -729,9 +731,14 @@ fun androidClangFor(targetArch: Arch, version: String = "30"): String { ...@@ -729,9 +731,14 @@ fun androidClangFor(targetArch: Arch, version: String = "30"): String {
return clang return clang
} }
fun androidJar(version: String = "30"): String { fun androidJar(askedVersion: String = ""): String {
val androidHome = androidHome() val androidHome = androidHome()
return androidHome.resolve("platforms/android-$version/android.jar").absolutePath val version = if (askedVersion.isEmpty()) {
File(androidHome, "platforms").list().sortedArrayDescending().first()
} else {
"android-$askedVersion"
}
return androidHome.resolve("platforms/$version/android.jar").absolutePath
} }
fun createCompileJvmBindingsTask( fun createCompileJvmBindingsTask(
...@@ -918,7 +925,9 @@ fun createLinkJvmBindings( ...@@ -918,7 +925,9 @@ fun createLinkJvmBindings(
"-shared", "-shared",
"-static-libstdc++", "-static-libstdc++",
"-lGLESv3", "-lGLESv3",
"-lEGL" "-lEGL",
"-llog",
"-landroid"
) )
linker.set(androidClangFor(targetArch)) linker.set(androidClangFor(targetArch))
} }
......
package org.jetbrains.skiko
import android.view.MotionEvent
import android.view.KeyEvent
import kotlin.math.abs
fun toSkikoKeyboardEvent(
event: KeyEvent,
keyCode: Int,
kind: SkikoKeyboardEventKind
): SkikoKeyboardEvent {
return SkikoKeyboardEvent(
key = SkikoKey.valueOf(keyCode),
modifiers = toSkikoModifiers(event),
kind = kind,
timestamp = event.getEventTime(),
platform = event
)
}
fun toSkikoTypeEvent(
event: KeyEvent,
keyCode: Int
): SkikoInputEvent {
return SkikoInputEvent(
input = event.unicodeChar.toChar().toString(),
key = SkikoKey.valueOf(keyCode),
modifiers = toSkikoModifiers(event),
SkikoKeyboardEventKind.TYPE,
platform = event
)
}
private fun toSkikoModifiers(event: KeyEvent): SkikoInputModifiers {
var result = 0
if (event.isAltPressed) {
result = result.or(SkikoInputModifiers.ALT.value)
}
if (event.isShiftPressed) {
result = result.or(SkikoInputModifiers.SHIFT.value)
}
if (event.isCtrlPressed) {
result = result.or(SkikoInputModifiers.CONTROL.value)
}
if (event.isMetaPressed) {
result = result.or(SkikoInputModifiers.META.value)
}
return SkikoInputModifiers(result)
}
fun toSkikoTouchEvent(event: MotionEvent, index: Int, density: Float): SkikoTouchEvent {
return SkikoTouchEvent(
x = (event.getX(index) / density).toDouble(),
y = (event.getY(index) / density).toDouble(),
timestamp = event.getEventTime(),
kind = toSkikoTouchEventKind(event),
platform = event
)
}
fun toSkikoGestureEvent(event: MotionEvent, density: Float): SkikoGestureEvent {
return SkikoGestureEvent(
x = (event.x / density).toDouble(),
y = (event.y / density).toDouble(),
kind = toSkikoGestureEventKind(event),
platform = event
)
}
fun toSkikoScaleGestureEvent(
event: MotionEvent,
scale: Double,
state: SkikoGestureEventState,
density: Float
): SkikoGestureEvent {
return SkikoGestureEvent(
x = (event.x / density).toDouble(),
y = (event.y / density).toDouble(),
scale = scale,
kind = SkikoGestureEventKind.PINCH,
state = state,
platform = event
)
}
private val swipeThreshold = 100
private val swipeVelocityThreshold = 100
internal fun toSkikoGestureDirection(
event1: MotionEvent,
event2: MotionEvent,
velocityX: Float,
velocityY: Float
): SkikoGestureEventDirection {
val dx = event2.x - event1.x
val dy = event2.y - event1.y
if (abs(dx) > abs(dy)) {
if (abs(dx) > swipeThreshold && abs(velocityX) > swipeVelocityThreshold) {
if (dx > 0) {
return SkikoGestureEventDirection.RIGHT
} else {
return SkikoGestureEventDirection.LEFT
}
}
} else {
if (abs(dy) > swipeThreshold && abs(velocityY) > swipeVelocityThreshold) {
if (dy > 0) {
return SkikoGestureEventDirection.DOWN
} else {
return SkikoGestureEventDirection.UP
}
}
}
return SkikoGestureEventDirection.UNKNOWN
}
private fun toSkikoTouchEventKind(event: MotionEvent): SkikoTouchEventKind {
return when (event.action) {
MotionEvent.ACTION_POINTER_DOWN,
MotionEvent.ACTION_DOWN -> SkikoTouchEventKind.STARTED
MotionEvent.ACTION_POINTER_UP,
MotionEvent.ACTION_UP -> SkikoTouchEventKind.ENDED
MotionEvent.ACTION_MOVE -> SkikoTouchEventKind.MOVED
else -> SkikoTouchEventKind.UNKNOWN
}
}
private fun toSkikoGestureEventKind(event: MotionEvent): SkikoGestureEventKind {
return when (event.action) {
MotionEvent.ACTION_UP -> SkikoGestureEventKind.TAP
MotionEvent.ACTION_MOVE -> SkikoGestureEventKind.PAN
else -> SkikoGestureEventKind.UNKNOWN
}
}
\ No newline at end of file
...@@ -4,13 +4,15 @@ import android.content.Context ...@@ -4,13 +4,15 @@ import android.content.Context
import android.opengl.GLES30 import android.opengl.GLES30
import android.opengl.GLSurfaceView import android.opengl.GLSurfaceView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.view.MotionEvent
import android.view.KeyEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import org.jetbrains.skia.* import org.jetbrains.skia.*
import java.nio.IntBuffer import java.nio.IntBuffer
import javax.microedition.khronos.egl.EGLConfig import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10 import javax.microedition.khronos.opengles.GL10
class SkikoSurfaceView(context: Context, layer: SkiaLayer) : GLSurfaceView(context) { class SkikoSurfaceView(context: Context, val layer: SkiaLayer) : GLSurfaceView(context) {
private val renderer = SkikoSurfaceRender(layer) private val renderer = SkikoSurfaceRender(layer)
init { init {
layoutParams = LinearLayout.LayoutParams( layoutParams = LinearLayout.LayoutParams(
...@@ -29,6 +31,37 @@ class SkikoSurfaceView(context: Context, layer: SkiaLayer) : GLSurfaceView(conte ...@@ -29,6 +31,37 @@ class SkikoSurfaceView(context: Context, layer: SkiaLayer) : GLSurfaceView(conte
fun scheduleFrame() { fun scheduleFrame() {
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
internal val gesturesDetector = SkikoGesturesDetector(context, layer)
override fun onTouchEvent(event: MotionEvent): Boolean {
val events: MutableList<SkikoTouchEvent> = mutableListOf()
val count = event.pointerCount
for (index in 0 .. count - 1) {
events.add(toSkikoTouchEvent(event, index, layer.contentScale))
}
layer.skikoView?.onTouchEvent(events.toTypedArray())
return true
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
layer.skikoView?.onKeyboardEvent(
toSkikoKeyboardEvent(event, keyCode, SkikoKeyboardEventKind.DOWN)
)
if (event.unicodeChar != 0) {
layer.skikoView?.onInputEvent(
toSkikoTypeEvent(event, keyCode)
)
}
return true
}
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
layer.skikoView?.onKeyboardEvent(
toSkikoKeyboardEvent(event, keyCode, SkikoKeyboardEventKind.UP)
)
return true
}
} }
private class SkikoSurfaceRender(private val layer: SkiaLayer) : GLSurfaceView.Renderer { private class SkikoSurfaceRender(private val layer: SkiaLayer) : GLSurfaceView.Renderer {
...@@ -86,6 +119,7 @@ private class SkikoSurfaceRender(private val layer: SkiaLayer) : GLSurfaceView.R ...@@ -86,6 +119,7 @@ private class SkikoSurfaceRender(private val layer: SkiaLayer) : GLSurfaceView.R
// This method is called from GL rendering thread, it shall render Skia picture. // This method is called from GL rendering thread, it shall render Skia picture.
override fun onDrawFrame(gl: GL10?) { override fun onDrawFrame(gl: GL10?) {
lockPicture { lockPicture {
canvas?.clear(-1)
canvas?.drawPicture(it.instance) canvas?.drawPicture(it.instance)
Unit Unit
} }
......
package org.jetbrains.skiko package org.jetbrains.skiko
import android.content.Context
import android.view.* import android.view.*
import android.view.inputmethod.InputMethodManager
import org.jetbrains.skia.Canvas import org.jetbrains.skia.Canvas
actual typealias SkikoGesturePlatformEvent = MotionEvent actual typealias SkikoGesturePlatformEvent = MotionEvent
actual typealias SkikoPlatformPointerEvent = MotionEvent actual typealias SkikoPlatformPointerEvent = MotionEvent
// TODO: most likely wrong. actual typealias SkikoTouchPlatformEvent = MotionEvent
actual typealias SkikoPlatformInputEvent = Any actual typealias SkikoPlatformInputEvent = KeyEvent
actual typealias SkikoTouchPlatformEvent = Any
actual typealias SkikoPlatformKeyboardEvent = KeyEvent actual typealias SkikoPlatformKeyboardEvent = KeyEvent
actual open class SkiaLayer { actual open class SkiaLayer {
var gesturesToListen: Array<SkikoGestureEventKind>? = null
set(value) {
field = value
initGestures()
}
private fun initGestures() {
glView?.let { view ->
view.gesturesDetector.setGesturesToListen(gesturesToListen)
if (gesturesToListen != null) {
view.setOnTouchListener { _, event ->
view.gesturesDetector.onTouchEvent(event)
}
}
}
}
private var glView: SkikoSurfaceView? = null private var glView: SkikoSurfaceView? = null
private var container: ViewGroup? = null private var container: ViewGroup? = null
...@@ -41,6 +60,27 @@ actual open class SkiaLayer { ...@@ -41,6 +60,27 @@ actual open class SkiaLayer {
} }
} }
private var _isKeyboardVisible = false
fun showScreenKeyboard() {
if (glView != null) {
val imm = glView!!.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
_isKeyboardVisible = true
}
}
fun hideScreenKeyboard() {
if (glView != null) {
val imm = glView!!.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(glView!!.windowToken, 0)
_isKeyboardVisible = false
}
}
fun isScreenKeyboardOpen() = _isKeyboardVisible
fun attachTo(container: ViewGroup) { fun attachTo(container: ViewGroup) {
initDefaultContext(container.context) initDefaultContext(container.context)
...@@ -49,6 +89,9 @@ actual open class SkiaLayer { ...@@ -49,6 +89,9 @@ actual open class SkiaLayer {
this.container = container this.container = container
this.glView = view this.glView = view
initGestures()
view.setFocusableInTouchMode(true)
needRedraw() needRedraw()
} }
......
package org.jetbrains.skiko
import android.content.Context
import android.view.MotionEvent
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.ScaleGestureDetector
import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
import org.jetbrains.skia.toRadians
internal class SkikoGesturesDetector(
private val context: Context,
private val layer: SkiaLayer
) {
private var gesturesToListen: Array<SkikoGestureEventKind>? = null
fun setGesturesToListen(gestures: Array<SkikoGestureEventKind>?) {
gesturesToListen = gestures
}
private fun containsGesture(
gesture: SkikoGestureEventKind
): Boolean {
gesturesToListen?.let { list ->
return list.contains(gesture)
}
return false
}
fun onTouchEvent(event: MotionEvent): Boolean {
if (containsGesture(SkikoGestureEventKind.PINCH)) {
scaleGesture.onTouchEvent(event)
}
if (containsGesture(SkikoGestureEventKind.ROTATION)) {
rotationGesture.onTouchEvent(event)
}
return simpleGestures.onTouchEvent(event)
}
private val simpleGestures = GestureDetector(context, object: SimpleOnGestureListener() {
override fun onSingleTapUp(event: MotionEvent): Boolean {
if (!containsGesture(SkikoGestureEventKind.TAP)) return false
val density = layer.contentScale
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = (event.x / density).toDouble(),
y = (event.y / density).toDouble(),
kind = SkikoGestureEventKind.TAP,
platform = event
)
)
return true
}
override fun onDoubleTap(event: MotionEvent): Boolean {
if (!containsGesture(SkikoGestureEventKind.DOUBLETAP)) return false
val density = layer.contentScale
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = (event.x / density).toDouble(),
y = (event.y / density).toDouble(),
kind = SkikoGestureEventKind.DOUBLETAP,
platform = event
)
)
return true
}
override fun onLongPress(event: MotionEvent) {
if (!containsGesture(SkikoGestureEventKind.LONGPRESS)) return
val density = layer.contentScale
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = (event.x / density).toDouble(),
y = (event.y / density).toDouble(),
kind = SkikoGestureEventKind.LONGPRESS,
platform = event
)
)
}
override fun onScroll(
event1: MotionEvent,
event2: MotionEvent,
distanceX: Float,
distanceY: Float,
): Boolean {
if (!containsGesture(SkikoGestureEventKind.PAN)) return false
val density = layer.contentScale
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = (event2.x / density).toDouble(),
y = (event2.y / density).toDouble(),
kind = SkikoGestureEventKind.PAN,
platform = event2
)
)
return true
}
override fun onFling(
event1: MotionEvent,
event2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
if (!containsGesture(SkikoGestureEventKind.SWIPE)) return false
var direction = toSkikoGestureDirection(event1, event2, velocityX, velocityY)
if (direction == SkikoGestureEventDirection.UNKNOWN) {
return false
}
val density = layer.contentScale
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = (event2.x / density).toDouble(),
y = (event2.y / density).toDouble(),
direction = direction,
kind = SkikoGestureEventKind.SWIPE,
platform = event2
)
)
return true
}
})
private val scaleGesture = SkikoScaleGestureDetector(context, SkikoScaleGestureListener(layer))
private class SkikoScaleGestureDetector(
context: Context,
private val scaleListener: SkikoScaleGestureListener
) : ScaleGestureDetector(context, scaleListener) {
override fun onTouchEvent(event: MotionEvent): Boolean {
scaleListener.event = event
return super.onTouchEvent(event)
}
}
private class SkikoScaleGestureListener(val layer: SkiaLayer) : SimpleOnScaleGestureListener() {
var event: MotionEvent? = null
var scale: Double = 1.0
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
if (event != null) {
scale *= detector.scaleFactor.toDouble()
layer.skikoView?.onGestureEvent(
toSkikoScaleGestureEvent(
event = event!!,
scale = scale,
state = SkikoGestureEventState.STARTED,
layer.contentScale
)
)
return true
}
return false
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
if (event != null) {
scale *= detector.scaleFactor.toDouble()
layer.skikoView?.onGestureEvent(
toSkikoScaleGestureEvent(
event = event!!,
scale = scale,
state = SkikoGestureEventState.CHANGED,
layer.contentScale
)
)
return true
}
return false
}
override fun onScaleEnd(detector: ScaleGestureDetector) {
if (event != null) {
scale *= detector.scaleFactor.toDouble()
layer.skikoView?.onGestureEvent(
toSkikoScaleGestureEvent(
event = event!!,
scale = scale,
state = SkikoGestureEventState.ENDED,
layer.contentScale
)
)
}
scale = 1.0
}
}
private val rotationGesture = SkikoRotationGestureDetector(SkikoRotationGestureListener(layer))
private class SkikoRotationGestureListener(val layer: SkiaLayer) {
var event: MotionEvent? = null
fun onRotation(detector: SkikoRotationGestureDetector) {
if (event != null) {
val density = layer.contentScale
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = (event!!.x / density).toDouble(),
y = (event!!.y / density).toDouble(),
rotation = detector.angle,
kind = SkikoGestureEventKind.ROTATION,
state = SkikoGestureEventState.CHANGED,
platform = event
)
)
}
}
}
private class SkikoRotationGestureDetector(
private val rotationListener: SkikoRotationGestureListener
) {
private var fX = 0f
private var fY = 0f
private var sX = 0f
private var sY = 0f
private var ptrID1: Int
private var ptrID2: Int
var angle = 0.0
private set
companion object {
private const val INVALID_POINTER_ID = -1
}
init {
ptrID1 = INVALID_POINTER_ID
ptrID2 = INVALID_POINTER_ID
}
fun onTouchEvent(event: MotionEvent): Boolean {
rotationListener.event = event
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> ptrID1 = event.getPointerId(event.actionIndex)
MotionEvent.ACTION_POINTER_DOWN -> {
ptrID2 = event.getPointerId(event.actionIndex)
sX = event.getX(event.findPointerIndex(ptrID1))
sY = event.getY(event.findPointerIndex(ptrID1))
fX = event.getX(event.findPointerIndex(ptrID2))
fY = event.getY(event.findPointerIndex(ptrID2))
}
MotionEvent.ACTION_MOVE -> if (ptrID1 != INVALID_POINTER_ID && ptrID2 != INVALID_POINTER_ID) {
val nfX: Float
val nfY: Float
val nsX: Float
val nsY: Float
nsX = event.getX(event.findPointerIndex(ptrID1))
nsY = event.getY(event.findPointerIndex(ptrID1))
nfX = event.getX(event.findPointerIndex(ptrID2))
nfY = event.getY(event.findPointerIndex(ptrID2))
angle = angleBetweenLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY)
rotationListener.onRotation(this)
}
MotionEvent.ACTION_UP -> ptrID1 = INVALID_POINTER_ID
MotionEvent.ACTION_POINTER_UP -> ptrID2 = INVALID_POINTER_ID
MotionEvent.ACTION_CANCEL -> {
ptrID1 = INVALID_POINTER_ID
ptrID2 = INVALID_POINTER_ID
}
}
return true
}
private fun angleBetweenLines(
fX: Float,
fY: Float,
sX: Float,
sY: Float,
nfX: Float,
nfY: Float,
nsX: Float,
nsY: Float
): Double {
val angle1 = Math.atan2((fY - sY).toDouble(), (fX - sX).toDouble())
val angle2 = Math.atan2((nfY - nsY).toDouble(), (nfX - nsX).toDouble())
return (Math.toDegrees(angle2 - angle1) % 360).toFloat().toRadians()
}
}
}
...@@ -67,10 +67,10 @@ actual enum class SkikoKey(val value: Int) { ...@@ -67,10 +67,10 @@ actual enum class SkikoKey(val value: Int) {
KEY_RIGHT_ALT(KEYCODE_ALT_RIGHT), KEY_RIGHT_ALT(KEYCODE_ALT_RIGHT),
KEY_RIGHT_CONTROL(KEYCODE_CTRL_RIGHT), KEY_RIGHT_CONTROL(KEYCODE_CTRL_RIGHT),
KEY_MENU(KEYCODE_UNKNOWN), KEY_MENU(KEYCODE_UNKNOWN),
KEY_UP(KEYCODE_SYSTEM_NAVIGATION_UP), KEY_UP(KEYCODE_DPAD_UP),
KEY_DOWN(KEYCODE_SYSTEM_NAVIGATION_DOWN), KEY_DOWN(KEYCODE_DPAD_DOWN),
KEY_LEFT(KEYCODE_SYSTEM_NAVIGATION_LEFT), KEY_LEFT(KEYCODE_DPAD_LEFT),
KEY_RIGHT(KEYCODE_SYSTEM_NAVIGATION_RIGHT), KEY_RIGHT(KEYCODE_DPAD_RIGHT),
KEY_F1(KEYCODE_F1), KEY_F1(KEYCODE_F1),
KEY_F2(KEYCODE_F2), KEY_F2(KEYCODE_F2),
KEY_F3(KEYCODE_F3), KEY_F3(KEYCODE_F3),
...@@ -109,4 +109,11 @@ actual enum class SkikoKey(val value: Int) { ...@@ -109,4 +109,11 @@ actual enum class SkikoKey(val value: Int) {
KEY_NUMPAD_MULTIPLY(KEYCODE_NUMPAD_MULTIPLY), KEY_NUMPAD_MULTIPLY(KEYCODE_NUMPAD_MULTIPLY),
KEY_NUMPAD_DIVIDE(KEYCODE_NUMPAD_DIVIDE), KEY_NUMPAD_DIVIDE(KEYCODE_NUMPAD_DIVIDE),
KEY_NUMPAD_DECIMAL(KEYCODE_UNKNOWN); KEY_NUMPAD_DECIMAL(KEYCODE_UNKNOWN);
companion object {
fun valueOf(value: Int): SkikoKey {
val key = SkikoKey.values().firstOrNull { it.value == value }
return key ?: SkikoKey.KEY_UNKNOWN
}
}
} }
\ No newline at end of file
...@@ -203,7 +203,7 @@ expect enum class SkikoKey { ...@@ -203,7 +203,7 @@ expect enum class SkikoKey {
} }
enum class SkikoGestureEventKind { enum class SkikoGestureEventKind {
TAP, PAN, PINCH, ROTATION, LONGPRESS, SWIPE, UNKNOWN TAP, DOUBLETAP, LONGPRESS, PAN, PINCH, ROTATION, SWIPE, UNKNOWN
} }
enum class SkikoGestureEventDirection { enum class SkikoGestureEventDirection {
UP, DOWN, LEFT, RIGHT, UNKNOWN UP, DOWN, LEFT, RIGHT, UNKNOWN
......
package org.jetbrains.skiko package org.jetbrains.skiko
import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.useContents import kotlinx.cinterop.useContents
import org.jetbrains.skia.Canvas import org.jetbrains.skia.Canvas
import org.jetbrains.skia.PictureRecorder
import org.jetbrains.skia.Rect
import org.jetbrains.skiko.context.MetalContextHandler import org.jetbrains.skiko.context.MetalContextHandler
import org.jetbrains.skiko.redrawer.MetalRedrawer import org.jetbrains.skiko.redrawer.MetalRedrawer
import platform.Foundation.NSNotificationCenter
import platform.Foundation.NSSelectorFromString
import platform.UIKit.* import platform.UIKit.*
import platform.darwin.NSObject
import kotlin.system.getTimeNanos import kotlin.system.getTimeNanos
actual open class SkiaLayer { actual open class SkiaLayer {
private val gestures: Array<SkikoGestureEventKind>?
constructor(gestures: Array<SkikoGestureEventKind>? = null) {
this.gestures = gestures
}
fun isShowing(): Boolean { fun isShowing(): Boolean {
return true return true
...@@ -64,8 +53,17 @@ actual open class SkiaLayer { ...@@ -64,8 +53,17 @@ actual open class SkiaLayer {
} }
internal var view: UIView? = null internal var view: UIView? = null
// We need to keep reference to controller as Objective-C will only keep weak reference here. // We need to keep reference to gesturesDetector as Objective-C will only keep weak reference here.
lateinit private var controller: NSObject internal var gesturesDetector = SkikoGesturesDetector(this)
var gesturesToListen: Array<SkikoGestureEventKind>? = null
set(value) {
field = value
initGestures()
}
internal fun initGestures() {
gesturesDetector.setGesturesToListen(gesturesToListen)
}
actual fun attachTo(container: Any) { actual fun attachTo(container: Any) {
attachTo(container as UIView) attachTo(container as UIView)
...@@ -74,113 +72,6 @@ actual open class SkiaLayer { ...@@ -74,113 +72,6 @@ actual open class SkiaLayer {
fun attachTo(view: UIView) { fun attachTo(view: UIView) {
this.view = view this.view = view
contextHandler = MetalContextHandler(this) contextHandler = MetalContextHandler(this)
// See https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events?language=objc
controller = object : NSObject() {
@ObjCAction
fun onTap(sender: UITapGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.TAP,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onLongPress(sender: UILongPressGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.LONGPRESS,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onPinch(sender: UIPinchGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.PINCH,
scale = sender.scale,
velocity = sender.velocity,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onRotation(sender: UIRotationGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.ROTATION,
rotation = sender.rotation,
velocity = sender.velocity,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onSwipe(sender: UISwipeGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.SWIPE,
direction = toSkikoGestureDirection(sender.direction),
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onPan(sender: UIPanGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.PAN,
state = toSkikoGestureState(sender.state)
)
)
}
}
if (!gestures.isNullOrEmpty()) {
// We have ':' in selector to take care of function argument.
if (gestures.contains(SkikoGestureEventKind.TAP)) {
view.addGestureRecognizer(UITapGestureRecognizer(controller, NSSelectorFromString("onTap:")))
}
if (gestures.contains(SkikoGestureEventKind.LONGPRESS)) {
view.addGestureRecognizer(UILongPressGestureRecognizer(controller, NSSelectorFromString("onLongPress:")))
}
if (gestures.contains(SkikoGestureEventKind.PINCH)) {
view.addGestureRecognizer(UIPinchGestureRecognizer(controller, NSSelectorFromString("onPinch:")))
}
if (gestures.contains(SkikoGestureEventKind.ROTATION)) {
view.addGestureRecognizer(UIRotationGestureRecognizer(controller, NSSelectorFromString("onRotation:")))
}
if (gestures.contains(SkikoGestureEventKind.SWIPE)) {
view.addGestureRecognizer(UISwipeGestureRecognizer(controller, NSSelectorFromString("onSwipe:")))
}
if (gestures.contains(SkikoGestureEventKind.PAN)) {
view.addGestureRecognizer(UIPanGestureRecognizer(controller, NSSelectorFromString("onPan:")))
}
}
// TODO: maybe add observer for view.viewDidDisappear() to detach us? // TODO: maybe add observer for view.viewDidDisappear() to detach us?
redrawer = MetalRedrawer(this).apply { redrawer = MetalRedrawer(this).apply {
needRedraw() needRedraw()
......
package org.jetbrains.skiko
import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.useContents
import platform.darwin.NSObject
import platform.Foundation.NSSelectorFromString
import platform.UIKit.*
// See https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events?language=objc
internal class SkikoGesturesDetector(
private val layer: SkiaLayer
) : NSObject() {
val view: UIView?
get() = layer.view
@ObjCAction
fun onTap(sender: UITapGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.TAP,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onDoubleTap(sender: UITapGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.DOUBLETAP,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onLongPress(sender: UILongPressGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.LONGPRESS,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onPinch(sender: UIPinchGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.PINCH,
scale = sender.scale,
velocity = sender.velocity,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onRotation(sender: UIRotationGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.ROTATION,
rotation = sender.rotation,
velocity = sender.velocity,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onSwipe(sender: UISwipeGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.SWIPE,
direction = toSkikoGestureDirection(sender.direction),
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onPan(sender: UIPanGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.PAN,
state = toSkikoGestureState(sender.state)
)
)
}
// We have ':' in selector to take care of function argument.
private val gestureRecognizers = hashMapOf(
SkikoGestureEventKind.TAP to UITapGestureRecognizer(this, NSSelectorFromString("onTap:")),
SkikoGestureEventKind.DOUBLETAP to UITapGestureRecognizer(this, NSSelectorFromString("onDoubleTap:")).apply {
numberOfTapsRequired = 2.toULong()
},
SkikoGestureEventKind.LONGPRESS to UILongPressGestureRecognizer(this, NSSelectorFromString("onLongPress:")),
SkikoGestureEventKind.PINCH to UIPinchGestureRecognizer(this, NSSelectorFromString("onPinch:")),
SkikoGestureEventKind.ROTATION to UIRotationGestureRecognizer(this, NSSelectorFromString("onRotation:")),
SkikoGestureEventKind.SWIPE to UISwipeGestureRecognizer(this, NSSelectorFromString("onSwipe:")),
SkikoGestureEventKind.PAN to UIPanGestureRecognizer(this, NSSelectorFromString("onPan:"))
)
fun setGesturesToListen(gestures: Array<SkikoGestureEventKind>?) {
clearGesturesToListen()
if (!gestures.isNullOrEmpty()) {
for (gesture in gestures) {
if (gestureRecognizers.containsKey(gesture)) {
view?.addGestureRecognizer(gestureRecognizers.get(gesture)!!)
}
}
}
}
private fun clearGesturesToListen() {
for ((key, value) in gestureRecognizers) {
view?.removeGestureRecognizer(value)
}
}
}
...@@ -65,7 +65,10 @@ class SkikoUIView : UIView, UIKeyInputProtocol { ...@@ -65,7 +65,10 @@ class SkikoUIView : UIView, UIKeyInputProtocol {
} }
setFrame(CGRectMake(0.0, 0.0, width, height)) setFrame(CGRectMake(0.0, 0.0, width, height))
contentScaleFactor = UIScreen.mainScreen.scale contentScaleFactor = UIScreen.mainScreen.scale
skiaLayer?.attachTo(this) skiaLayer?.let { layer ->
layer.attachTo(this)
layer.initGestures()
}
return this return this
} }
......
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