/**************************************************************************/ /* GodotGestureHandler.kt */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2024-present Redot Engine contributors */ /* (see REDOT_AUTHORS.md) */ /* 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 } }