/**************************************************************************/
/* GodotGestureHandler.kt */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
package org.godotengine.godot.input
import android.os.Build
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.InputDevice
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.ScaleGestureDetector.OnScaleGestureListener
import org.godotengine.godot.GodotLib
/**
* Handles regular and scale gesture input related events for the [GodotView] view.
*
* @See https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener
* @See https://developer.android.com/reference/android/view/ScaleGestureDetector.OnScaleGestureListener
*/
internal class GodotGestureHandler(private val inputHandler: GodotInputHandler) : SimpleOnGestureListener(), OnScaleGestureListener {
companion object {
private val TAG = GodotGestureHandler::class.java.simpleName
}
/**
* Enable pan and scale gestures
*/
var panningAndScalingEnabled = false
private var nextDownIsDoubleTap = false
private var dragInProgress = false
private var scaleInProgress = false
private var contextClickInProgress = false
private var pointerCaptureInProgress = false
private var lastDragX: Float = 0.0f
private var lastDragY: Float = 0.0f
override fun onDown(event: MotionEvent): Boolean {
inputHandler.handleMotionEvent(event, MotionEvent.ACTION_DOWN, nextDownIsDoubleTap)
nextDownIsDoubleTap = false
return true
}
override fun onSingleTapUp(event: MotionEvent): Boolean {
inputHandler.handleMotionEvent(event)
return true
}
override fun onLongPress(event: MotionEvent) {
val toolType = GodotInputHandler.getEventToolType(event)
if (toolType != MotionEvent.TOOL_TYPE_MOUSE) {
contextClickRouter(event)
}
}
private fun contextClickRouter(event: MotionEvent) {
if (scaleInProgress || nextDownIsDoubleTap) {
return
}
// Cancel the previous down event
inputHandler.handleMotionEvent(event, MotionEvent.ACTION_CANCEL)
// Turn a context click into a single tap right mouse button click.
inputHandler.handleMouseEvent(
event,
MotionEvent.ACTION_DOWN,
MotionEvent.BUTTON_SECONDARY,
false
)
contextClickInProgress = true
}
fun onPointerCaptureChange(hasCapture: Boolean) {
if (pointerCaptureInProgress == hasCapture) {
return
}
if (!hasCapture) {
// Dispatch a mouse relative ACTION_UP event to signal the end of the capture
inputHandler.handleMouseEvent(MotionEvent.ACTION_UP, true)
}
pointerCaptureInProgress = hasCapture
}
fun onMotionEvent(event: MotionEvent): Boolean {
return when (event.actionMasked) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_BUTTON_RELEASE -> {
onActionUp(event)
}
MotionEvent.ACTION_MOVE -> {
onActionMove(event)
}
else -> false
}
}
private fun onActionUp(event: MotionEvent): Boolean {
if (event.actionMasked == MotionEvent.ACTION_CANCEL && pointerCaptureInProgress) {
// Don't dispatch the ACTION_CANCEL while a capture is in progress
return true
}
if (pointerCaptureInProgress || dragInProgress || contextClickInProgress) {
if (contextClickInProgress || GodotInputHandler.isMouseEvent(event)) {
// This may be an ACTION_BUTTON_RELEASE event which we don't handle,
// so we convert it to an ACTION_UP event.
inputHandler.handleMouseEvent(event, MotionEvent.ACTION_UP)
} else {
inputHandler.handleTouchEvent(event)
}
pointerCaptureInProgress = false
dragInProgress = false
contextClickInProgress = false
lastDragX = 0.0f
lastDragY = 0.0f
return true
}
return false
}
private fun onActionMove(event: MotionEvent): Boolean {
if (contextClickInProgress) {
inputHandler.handleMouseEvent(event, event.actionMasked, MotionEvent.BUTTON_SECONDARY, false)
return true
} else if (!scaleInProgress) {
// The 'onScroll' event is triggered with a long delay.
// Force the 'InputEventScreenDrag' event earlier here.
// We don't toggle 'dragInProgress' here so that the scaling logic can override the drag operation if needed.
// Once the 'onScroll' event kicks-in, 'dragInProgress' will be properly set.
if (lastDragX != event.getX(0) || lastDragY != event.getY(0)) {
lastDragX = event.getX(0)
lastDragY = event.getY(0)
inputHandler.handleMotionEvent(event)
return true
}
}
return false
}
override fun onDoubleTapEvent(event: MotionEvent): Boolean {
if (event.actionMasked == MotionEvent.ACTION_UP) {
nextDownIsDoubleTap = false
inputHandler.handleMotionEvent(event)
} else if (event.actionMasked == MotionEvent.ACTION_MOVE && !panningAndScalingEnabled) {
inputHandler.handleMotionEvent(event)
}
return true
}
override fun onDoubleTap(event: MotionEvent): Boolean {
nextDownIsDoubleTap = true
return true
}
override fun onScroll(
originEvent: MotionEvent?,
terminusEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (scaleInProgress) {
if (dragInProgress || lastDragX != 0.0f || lastDragY != 0.0f) {
if (originEvent != null) {
// Cancel the drag
inputHandler.handleMotionEvent(originEvent, MotionEvent.ACTION_CANCEL)
}
dragInProgress = false
lastDragX = 0.0f
lastDragY = 0.0f
}
}
val x = terminusEvent.x
val y = terminusEvent.y
if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress && !dragInProgress) {
inputHandler.handlePanEvent(x, y, distanceX / 5f, distanceY / 5f)
} else if (!scaleInProgress) {
dragInProgress = true
lastDragX = terminusEvent.getX(0)
lastDragY = terminusEvent.getY(0)
inputHandler.handleMotionEvent(terminusEvent)
}
return true
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
if (!panningAndScalingEnabled || pointerCaptureInProgress || dragInProgress) {
return false
}
if (detector.scaleFactor >= 0.8f && detector.scaleFactor != 1f && detector.scaleFactor <= 1.2f) {
inputHandler.handleMagnifyEvent(detector.focusX, detector.focusY, detector.scaleFactor)
}
return true
}
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
if (!panningAndScalingEnabled || pointerCaptureInProgress || dragInProgress) {
return false
}
scaleInProgress = true
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector) {
scaleInProgress = false
}
}