Browse Source

Add support for pointer capture

Fredia Huya-Kouadio 2 years ago
parent
commit
5149311316

+ 32 - 16
platform/android/android_input_handler.cpp

@@ -207,7 +207,7 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const
 	}
 }
 
-void AndroidInputHandler::_parse_mouse_event_info(int buttons_mask, bool p_pressed, bool p_double_click) {
+void AndroidInputHandler::_parse_mouse_event_info(int buttons_mask, bool p_pressed, bool p_double_click, bool p_source_mouse_relative) {
 	if (!mouse_event_info.valid) {
 		return;
 	}
@@ -215,8 +215,14 @@ void AndroidInputHandler::_parse_mouse_event_info(int buttons_mask, bool p_press
 	Ref<InputEventMouseButton> ev;
 	ev.instance();
 	_set_key_modifier_state(ev);
-	ev->set_position(mouse_event_info.pos);
-	ev->set_global_position(mouse_event_info.pos);
+	if (p_source_mouse_relative) {
+		ev->set_position(hover_prev_pos);
+		ev->set_global_position(hover_prev_pos);
+	} else {
+		ev->set_position(mouse_event_info.pos);
+		ev->set_global_position(mouse_event_info.pos);
+		hover_prev_pos = mouse_event_info.pos;
+	}
 	ev->set_pressed(p_pressed);
 	int changed_button_mask = buttons_state ^ buttons_mask;
 
@@ -226,15 +232,14 @@ void AndroidInputHandler::_parse_mouse_event_info(int buttons_mask, bool p_press
 	ev->set_button_mask(buttons_mask);
 	ev->set_doubleclick(p_double_click);
 	input->parse_input_event(ev);
-	hover_prev_pos = mouse_event_info.pos;
 }
 
-void AndroidInputHandler::_release_mouse_event_info() {
-	_parse_mouse_event_info(0, false, false);
+void AndroidInputHandler::_release_mouse_event_info(bool p_source_mouse_relative) {
+	_parse_mouse_event_info(0, false, false, p_source_mouse_relative);
 	mouse_event_info.valid = false;
 }
 
-void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_android_buttons_mask, Point2 p_event_pos, Vector2 p_delta, bool p_double_click) {
+void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_android_buttons_mask, Point2 p_event_pos, Vector2 p_delta, bool p_double_click, bool p_source_mouse_relative) {
 	int event_buttons_mask = _android_button_mask_to_godot_button_mask(p_event_android_buttons_mask);
 	switch (p_event_action) {
 		case AMOTION_EVENT_ACTION_HOVER_MOVE: // hover move
@@ -259,13 +264,13 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an
 
 			mouse_event_info.valid = true;
 			mouse_event_info.pos = p_event_pos;
-			_parse_mouse_event_info(event_buttons_mask, true, p_double_click);
+			_parse_mouse_event_info(event_buttons_mask, true, p_double_click, p_source_mouse_relative);
 		} break;
 
 		case AMOTION_EVENT_ACTION_UP:
 		case AMOTION_EVENT_ACTION_CANCEL:
 		case AMOTION_EVENT_ACTION_BUTTON_RELEASE: {
-			_release_mouse_event_info();
+			_release_mouse_event_info(p_source_mouse_relative);
 		} break;
 
 		case AMOTION_EVENT_ACTION_MOVE: {
@@ -276,21 +281,32 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an
 			Ref<InputEventMouseMotion> ev;
 			ev.instance();
 			_set_key_modifier_state(ev);
-			ev->set_position(p_event_pos);
-			ev->set_global_position(p_event_pos);
-			ev->set_relative(p_event_pos - hover_prev_pos);
+			if (p_source_mouse_relative) {
+				ev->set_position(hover_prev_pos);
+				ev->set_global_position(hover_prev_pos);
+				ev->set_relative(p_event_pos);
+			} else {
+				ev->set_position(p_event_pos);
+				ev->set_global_position(p_event_pos);
+				ev->set_relative(p_event_pos - hover_prev_pos);
+				mouse_event_info.pos = p_event_pos;
+				hover_prev_pos = p_event_pos;
+			}
 			ev->set_button_mask(event_buttons_mask);
 			input->parse_input_event(ev);
-			mouse_event_info.pos = p_event_pos;
-			hover_prev_pos = p_event_pos;
 		} break;
 
 		case AMOTION_EVENT_ACTION_SCROLL: {
 			Ref<InputEventMouseButton> ev;
 			ev.instance();
 			_set_key_modifier_state(ev);
-			ev->set_position(p_event_pos);
-			ev->set_global_position(p_event_pos);
+			if (p_source_mouse_relative) {
+				ev->set_position(hover_prev_pos);
+				ev->set_global_position(hover_prev_pos);
+			} else {
+				ev->set_position(p_event_pos);
+				ev->set_global_position(p_event_pos);
+			}
 			ev->set_pressed(true);
 			buttons_state = event_buttons_mask;
 			if (p_delta.y > 0) {

+ 3 - 3
platform/android/android_input_handler.h

@@ -86,9 +86,9 @@ private:
 
 	void _wheel_button_click(int event_buttons_mask, const Ref<InputEventMouseButton> &ev, int wheel_button, float factor);
 
-	void _parse_mouse_event_info(int buttons_mask, bool p_pressed, bool p_double_click);
+	void _parse_mouse_event_info(int buttons_mask, bool p_pressed, bool p_double_click, bool p_source_mouse_relative);
 
-	void _release_mouse_event_info();
+	void _release_mouse_event_info(bool p_source_mouse_relative = false);
 
 	void _parse_all_touch(bool p_pressed, bool p_double_tap);
 
@@ -97,7 +97,7 @@ private:
 public:
 	void process_joy_event(const JoypadEvent &p_event);
 	void process_key_event(int p_scancode, int p_physical_scancode, int p_unicode, bool p_pressed);
-	void process_mouse_event(int p_event_action, int p_event_android_buttons_mask, Point2 p_event_pos, Vector2 p_delta, bool p_double_click);
+	void process_mouse_event(int p_event_action, int p_event_android_buttons_mask, Point2 p_event_pos, Vector2 p_delta, bool p_double_click, bool p_source_mouse_relative);
 	void process_touch_event(int p_event, int p_pointer, const Vector<TouchPos> &p_points, bool p_double_tap);
 	void process_magnify(Point2 p_pos, float p_factor);
 	void process_pan(Point2 p_pos, Vector2 p_delta);

+ 1 - 1
platform/android/java/lib/src/org/godotengine/godot/GodotLib.java

@@ -105,7 +105,7 @@ public class GodotLib {
 	/**
 	 * Dispatch mouse events
 	 */
-	public static native void dispatchMouseEvent(int event, int buttonMask, float x, float y, float deltaX, float deltaY, boolean doubleClick);
+	public static native void dispatchMouseEvent(int event, int buttonMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative);
 
 	public static native void magnify(float x, float y, float factor);
 

+ 23 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotView.java

@@ -126,6 +126,29 @@ public class GodotView extends GLSurfaceView {
 		return inputHandler.onGenericMotionEvent(event) || super.onGenericMotionEvent(event);
 	}
 
+	@Override
+	public boolean onCapturedPointerEvent(MotionEvent event) {
+		return inputHandler.onGenericMotionEvent(event);
+	}
+
+	@Override
+	public void onPointerCaptureChange(boolean hasCapture) {
+		super.onPointerCaptureChange(hasCapture);
+		inputHandler.onPointerCaptureChange(hasCapture);
+	}
+
+	@Override
+	public void requestPointerCapture() {
+		super.requestPointerCapture();
+		inputHandler.onPointerCaptureChange(true);
+	}
+
+	@Override
+	public void releasePointerCapture() {
+		super.releasePointerCapture();
+		inputHandler.onPointerCaptureChange(false);
+	}
+
 	/**
 	 * Called from JNI to change the pointer icon
 	 */

+ 3 - 1
platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java

@@ -131,7 +131,9 @@ public class GodotEditText extends EditText {
 					edit.setText("");
 					edit.append(text);
 					if (msg.arg2 != -1) {
-						edit.setSelection(msg.arg1, msg.arg2);
+						int selectionStart = Math.min(msg.arg1, edit.length());
+						int selectionEnd = Math.min(msg.arg2, edit.length());
+						edit.setSelection(selectionStart, selectionEnd);
 						edit.mInputWrapper.setSelection(true);
 					} else {
 						edit.mInputWrapper.setSelection(false);

+ 68 - 15
platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt

@@ -30,7 +30,9 @@
 
 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
@@ -57,6 +59,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
 	private var dragInProgress = false
 	private var scaleInProgress = false
 	private var contextClickInProgress = false
+	private var pointerCaptureInProgress = false
 
 	override fun onDown(event: MotionEvent): Boolean {
 		GodotInputHandler.handleMotionEvent(event.source, MotionEvent.ACTION_DOWN, event.buttonState, event.x, event.y, nextDownIsDoubleTap)
@@ -74,7 +77,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
 	}
 
 	private fun contextClickRouter(event: MotionEvent) {
-		if (scaleInProgress) {
+		if (scaleInProgress || nextDownIsDoubleTap) {
 			return
 		}
 
@@ -97,6 +100,27 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
 		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
+			GodotInputHandler.handleMouseEvent(
+				MotionEvent.ACTION_UP,
+				0,
+				0f,
+				0f,
+				0f,
+				0f,
+				false,
+				true
+			)
+		}
+		pointerCaptureInProgress = hasCapture
+	}
+
 	fun onMotionEvent(event: MotionEvent): Boolean {
 		return when (event.actionMasked) {
 			MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_BUTTON_RELEASE -> {
@@ -110,30 +134,59 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
 	}
 
 	private fun onActionUp(event: MotionEvent): Boolean {
-		if (dragInProgress) {
-			GodotInputHandler.handleMotionEvent(event)
-			dragInProgress = false
+		if (event.actionMasked == MotionEvent.ACTION_CANCEL && pointerCaptureInProgress) {
+			// Don't dispatch the ACTION_CANCEL while a capture is in progress
 			return true
-		} else if (contextClickInProgress) {
-			GodotInputHandler.handleMouseEvent(
-				event.actionMasked,
-				0,
-				event.x,
-				event.y
-			)
+		}
+
+		val sourceMouseRelative = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+			event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE)
+		} else {
+			false
+		}
+
+		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.
+				GodotInputHandler.handleMouseEvent(
+					MotionEvent.ACTION_UP,
+					event.buttonState,
+					event.x,
+					event.y,
+					0f,
+					0f,
+					false,
+					sourceMouseRelative
+				)
+			} else {
+				GodotInputHandler.handleTouchEvent(event)
+			}
+			pointerCaptureInProgress = false
+			dragInProgress = false
 			contextClickInProgress = false
 			return true
 		}
+
 		return false
 	}
 
 	private fun onActionMove(event: MotionEvent): Boolean {
 		if (contextClickInProgress) {
+			val sourceMouseRelative = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+				event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE)
+			} else {
+				false
+			}
 			GodotInputHandler.handleMouseEvent(
 				event.actionMasked,
 				MotionEvent.BUTTON_SECONDARY,
 				event.x,
-				event.y
+				event.y,
+				0f,
+				0f,
+				false,
+				sourceMouseRelative
 			)
 			return true
 		}
@@ -178,7 +231,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
 
 		val x = terminusEvent.x
 		val y = terminusEvent.y
-		if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled) {
+		if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress) {
 			GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f)
 		} else {
 			GodotInputHandler.handleMotionEvent(terminusEvent)
@@ -187,7 +240,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
 	}
 
 	override fun onScale(detector: ScaleGestureDetector?): Boolean {
-		if (detector == null || !panningAndScalingEnabled) {
+		if (detector == null || !panningAndScalingEnabled || pointerCaptureInProgress) {
 			return false
 		}
 		GodotLib.magnify(
@@ -199,7 +252,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
 	}
 
 	override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
-		if (detector == null || !panningAndScalingEnabled) {
+		if (detector == null || !panningAndScalingEnabled || pointerCaptureInProgress) {
 			return false
 		}
 		scaleInProgress = true

+ 31 - 7
platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java

@@ -105,6 +105,10 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 		return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
 	}
 
+	public void onPointerCaptureChange(boolean hasCapture) {
+		godotGestureHandler.onPointerCaptureChange(hasCapture);
+	}
+
 	public boolean onKeyUp(final int keyCode, KeyEvent event) {
 		if (keyCode == KeyEvent.KEYCODE_BACK) {
 			return true;
@@ -202,6 +206,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 			return true;
 		}
 
+		if (godotGestureHandler.onMotionEvent(event)) {
+			// The gesture handler has handled the event.
+			return true;
+		}
+
 		if (event.isFromSource(InputDevice.SOURCE_JOYSTICK) && event.getActionMasked() == MotionEvent.ACTION_MOVE) {
 			// Check if the device exists
 			final int deviceId = event.getDeviceId();
@@ -237,7 +246,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 				}
 				return true;
 			}
-		} else if (isMouseEvent(event)) {
+		} else {
 			return handleMouseEvent(event);
 		}
 
@@ -414,7 +423,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 	}
 
 	private static boolean isMouseEvent(int eventSource) {
-		return ((eventSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) || ((eventSource & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS);
+		boolean mouseSource = ((eventSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) || ((eventSource & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS);
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+			mouseSource = mouseSource || ((eventSource & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE);
+		}
+		return mouseSource;
 	}
 
 	static boolean handleMotionEvent(final MotionEvent event) {
@@ -435,7 +448,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 
 	static boolean handleMotionEvent(int eventSource, int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleTap) {
 		if (isMouseEvent(eventSource)) {
-			return handleMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleTap);
+			return handleMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleTap, false);
 		}
 
 		return handleTouchEvent(eventAction, x, y, doubleTap);
@@ -449,14 +462,25 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 
 		final float verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
 		final float horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
-		return handleMouseEvent(eventAction, buttonsMask, x, y, horizontalFactor, verticalFactor, false);
+		boolean sourceMouseRelative = false;
+		if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+			sourceMouseRelative = event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE);
+		}
+		return handleMouseEvent(eventAction, buttonsMask, x, y, horizontalFactor, verticalFactor, false, sourceMouseRelative);
 	}
 
 	static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y) {
-		return handleMouseEvent(eventAction, buttonsMask, x, y, 0, 0, false);
+		return handleMouseEvent(eventAction, buttonsMask, x, y, 0, 0, false, false);
+	}
+
+	static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, boolean doubleClick) {
+		return handleMouseEvent(eventAction, buttonsMask, x, y, 0, 0, doubleClick, false);
 	}
 
-	static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick) {
+	static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative) {
+		// We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically
+		// follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate
+		// stream of events to the engine.
 		switch (eventAction) {
 			case MotionEvent.ACTION_CANCEL:
 			case MotionEvent.ACTION_UP:
@@ -469,7 +493,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 			case MotionEvent.ACTION_HOVER_MOVE:
 			case MotionEvent.ACTION_MOVE:
 			case MotionEvent.ACTION_SCROLL: {
-				GodotLib.dispatchMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick);
+				GodotLib.dispatchMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative);
 				return true;
 			}
 		}

+ 2 - 2
platform/android/java_godot_lib_jni.cpp

@@ -283,12 +283,12 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env,
 }
 
 // Called on the UI thread
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative) {
 	if (step.get() <= 0) {
 		return;
 	}
 
-	input_handler->process_mouse_event(p_event_type, p_button_mask, Point2(p_x, p_y), Vector2(p_delta_x, p_delta_y), p_double_click);
+	input_handler->process_mouse_event(p_event_type, p_button_mask, Point2(p_x, p_y), Vector2(p_delta_x, p_delta_y), p_double_click, p_source_mouse_relative);
 }
 
 // Called on the UI thread

+ 1 - 1
platform/android/java_godot_lib_jni.h

@@ -45,7 +45,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *en
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *env, jclass clazz, jint event, jint id, jint pos);
 JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jboolean p_double_tap);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_factor);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y);

+ 26 - 0
platform/android/java_godot_view_wrapper.cpp

@@ -42,12 +42,38 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) {
 	if (android_device_api_level >= __ANDROID_API_N__) {
 		_set_pointer_icon = env->GetMethodID(_cls, "setPointerIcon", "(I)V");
 	}
+	if (android_device_api_level >= __ANDROID_API_O__) {
+		_request_pointer_capture = env->GetMethodID(_cls, "requestPointerCapture", "()V");
+		_release_pointer_capture = env->GetMethodID(_cls, "releasePointerCapture", "()V");
+	}
 }
 
 bool GodotJavaViewWrapper::can_update_pointer_icon() const {
 	return _set_pointer_icon != nullptr;
 }
 
+bool GodotJavaViewWrapper::can_capture_pointer() const {
+	return _request_pointer_capture != nullptr && _release_pointer_capture != nullptr;
+}
+
+void GodotJavaViewWrapper::request_pointer_capture() {
+	if (_request_pointer_capture != nullptr) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_NULL(env);
+
+		env->CallVoidMethod(_godot_view, _request_pointer_capture);
+	}
+}
+
+void GodotJavaViewWrapper::release_pointer_capture() {
+	if (_release_pointer_capture != nullptr) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_NULL(env);
+
+		env->CallVoidMethod(_godot_view, _release_pointer_capture);
+	}
+}
+
 void GodotJavaViewWrapper::set_pointer_icon(int pointer_type) {
 	if (_set_pointer_icon != nullptr) {
 		JNIEnv *env = get_jni_env();

+ 7 - 0
platform/android/java_godot_view_wrapper.h

@@ -41,12 +41,19 @@ class GodotJavaViewWrapper {
 private:
 	jclass _cls;
 	jobject _godot_view;
+
+	jmethodID _request_pointer_capture = 0;
+	jmethodID _release_pointer_capture = 0;
 	jmethodID _set_pointer_icon = 0;
 
 public:
 	GodotJavaViewWrapper(jobject godot_view);
 
 	bool can_update_pointer_icon() const;
+	bool can_capture_pointer() const;
+
+	void request_pointer_capture();
+	void release_pointer_capture();
 	void set_pointer_icon(int pointer_type);
 
 	~GodotJavaViewWrapper();

+ 8 - 2
platform/android/os_android.cpp

@@ -274,10 +274,10 @@ Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_han
 }
 
 void OS_Android::set_mouse_mode(MouseMode p_mode) {
-	if (!godot_java->get_godot_view()->can_update_pointer_icon()) {
+	if (!godot_java->get_godot_view()->can_update_pointer_icon() || !godot_java->get_godot_view()->can_capture_pointer()) {
 		return;
 	}
-	if (mouse_mode == p_mode || p_mode == MouseMode::MOUSE_MODE_CAPTURED) {
+	if (mouse_mode == p_mode) {
 		return;
 	}
 
@@ -287,6 +287,12 @@ void OS_Android::set_mouse_mode(MouseMode p_mode) {
 		set_cursor_shape(cursor_shape);
 	}
 
+	if (p_mode == MouseMode::MOUSE_MODE_CAPTURED) {
+		godot_java->get_godot_view()->request_pointer_capture();
+	} else {
+		godot_java->get_godot_view()->release_pointer_capture();
+	}
+
 	mouse_mode = p_mode;
 }