From 892dee6814526372c81d9d72276e0d9df86da0c5 Mon Sep 17 00:00:00 2001 From: Frosty-J <60154347+Frosty-J@users.noreply.github.com> Date: Sat, 7 Sep 2024 10:18:39 +0100 Subject: [PATCH] Add support for predictive back gestures (#7429) * Remove back/menu methods (deprecated 5 years ago) * Add enableOnBackInvokedCallback to gdx-tests manifest * Add back invoke support and test * Update CHANGES * Apply formatter * Realised where I went wrong with inner class * No-one else is using @Nullable so I guess I shouldn't either * Suppose it covers GWT as well * Undo constructor nonsense * Fix malformed Javadoc * Apply formatter * Continuation of e074acc reversion, nearly forgot * Revisit BackTest * Apply formatter * Make test exclusive to Android It doesn't make sense for different platforms. Input.Keys.BACK doesn't catch the browser back button on GWT. * Less ambiguous class name * Apply formatter * Less dumb-dumb anchor * final PredictiveBackHandler * Apply formatter * Mostly revert 5b2d0721931197aaba0e0fb85bfd449d5e27bbf4 * Remove unneeded `if` nesting --------- Co-authored-by: GitHub Action --- CHANGES | 2 + .../backends/android/DefaultAndroidInput.java | 53 +++++++++++++- .../headless/mock/input/MockInput.java | 20 ------ gdx/src/com/badlogic/gdx/AbstractInput.java | 20 ------ gdx/src/com/badlogic/gdx/Input.java | 28 -------- .../com/badlogic/gdx/input/RemoteInput.java | 20 ------ tests/gdx-tests-android/AndroidManifest.xml | 3 +- .../gdx/tests/android/AndroidTestStarter.java | 4 +- .../gdx/tests/AbstractTestWrapper.java | 20 ------ .../src/com/badlogic/gdx/tests/BackTest.java | 69 +++++++++++++++++++ 10 files changed, 127 insertions(+), 112 deletions(-) create mode 100644 tests/gdx-tests/src/com/badlogic/gdx/tests/BackTest.java diff --git a/CHANGES b/CHANGES index 8988ecb57c6..dd4dedf7848 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,8 @@ - Optimization of SpriteBatch in GL30 default modes, as indices do not need to be updated - Desktop: Added support for 8/32/64-bit PCM and MP3 WAVs - Desktop: Improved support for surround sound audio files (#6792) +- Android: Added support for predictive back gesture (requires android:enableOnBackInvokedCallback="true" in manifest) +- API Removal: Removed deprecated back and menu key methods. Use `setCatchKey` and `isCatchKey` instead. - Cache packed color on sprite to improve performance - Improve JsonReader, add JsonSkimmer, JsonString - Add proper glTexImage2D support on GWT diff --git a/backends/gdx-backend-android/src/com/badlogic/gdx/backends/android/DefaultAndroidInput.java b/backends/gdx-backend-android/src/com/badlogic/gdx/backends/android/DefaultAndroidInput.java index 9d232e84476..dd4fbbb5241 100644 --- a/backends/gdx-backend-android/src/com/badlogic/gdx/backends/android/DefaultAndroidInput.java +++ b/backends/gdx-backend-android/src/com/badlogic/gdx/backends/android/DefaultAndroidInput.java @@ -17,6 +17,7 @@ package com.badlogic.gdx.backends.android; import android.animation.Animator; +import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; @@ -27,6 +28,7 @@ import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; +import android.os.Build; import android.os.Handler; import android.text.*; import android.text.InputFilter.LengthFilter; @@ -41,10 +43,14 @@ import android.view.inputmethod.InputMethodManager; import android.widget.*; import android.widget.TextView.OnEditorActionListener; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; + import com.badlogic.gdx.AbstractInput; import com.badlogic.gdx.Application; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Graphics.DisplayMode; +import com.badlogic.gdx.Input; import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.backends.android.keyboardheight.KeyboardHeightObserver; import com.badlogic.gdx.backends.android.keyboardheight.KeyboardHeightProvider; @@ -60,8 +66,8 @@ /** An implementation of the {@link Input} interface for Android. * - * @author mzechner */ -/** @author jshapcot */ + * @author mzechner + * @author jshapcot */ public class DefaultAndroidInput extends AbstractInput implements AndroidInput, KeyboardHeightObserver { static class KeyEvent { @@ -144,6 +150,7 @@ protected TouchEvent newObject () { private final AndroidApplicationConfiguration config; protected final Orientation nativeOrientation; private long currentEventTimeStamp = 0; + private PredictiveBackHandler predictiveBackHandler; private SensorEventListener accelerometerListener; private SensorEventListener gyroscopeListener; @@ -179,6 +186,10 @@ public DefaultAndroidInput (Application activity, Context context, Object view, haptics = new AndroidHaptics(context); + if (Build.VERSION.SDK_INT >= 33) { + this.predictiveBackHandler = new PredictiveBackHandler(); + } + int rotation = getRotation(); DisplayMode mode = app.getGraphics().getDisplayMode(); if (((rotation == 0 || rotation == 180) && (mode.width >= mode.height)) @@ -1423,6 +1434,44 @@ public void onDreamingStopped () { unregisterSensorListeners(); } + @Override + public void setCatchKey (int keycode, boolean catchKey) { + super.setCatchKey(keycode, catchKey); + if (keycode == Keys.BACK && predictiveBackHandler != null) { + if (catchKey) + predictiveBackHandler.register(); + else + predictiveBackHandler.unregister(); + } + } + + /** Handle predictive back gestures on Android 13 and newer, replacing the BACK key event for exiting the + * activity. + * @see Add support for the + * predictive back gesture - Android Developers */ + @TargetApi(33) + private class PredictiveBackHandler { + + private final OnBackInvokedDispatcher dispatcher = ((Activity)app).getOnBackInvokedDispatcher(); + private final OnBackInvokedCallback callback = new OnBackInvokedCallback() { + @Override + public void onBackInvoked () { + if (processor != null) { + processor.keyDown(Keys.BACK); + } + } + }; + + private void register () { + dispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, callback); + } + + private void unregister () { + dispatcher.unregisterOnBackInvokedCallback(callback); + } + + } + /** Our implementation of SensorEventListener. Because Android doesn't like it when we register more than one Sensor to a * single SensorEventListener, we add one of these for each Sensor. Could use an anonymous class, but I don't see any harm in * explicitly defining it here. Correct me if I am wrong. */ diff --git a/backends/gdx-backend-headless/src/com/badlogic/gdx/backends/headless/mock/input/MockInput.java b/backends/gdx-backend-headless/src/com/badlogic/gdx/backends/headless/mock/input/MockInput.java index 73c980bed7e..3dbf908e59d 100644 --- a/backends/gdx-backend-headless/src/com/badlogic/gdx/backends/headless/mock/input/MockInput.java +++ b/backends/gdx-backend-headless/src/com/badlogic/gdx/backends/headless/mock/input/MockInput.java @@ -222,26 +222,6 @@ public long getCurrentEventTime () { return 0; } - @Override - public void setCatchBackKey (boolean catchBack) { - - } - - @Override - public boolean isCatchBackKey () { - return false; - } - - @Override - public void setCatchMenuKey (boolean catchMenu) { - - } - - @Override - public boolean isCatchMenuKey () { - return false; - } - @Override public void setCatchKey (int keycode, boolean catchKey) { diff --git a/gdx/src/com/badlogic/gdx/AbstractInput.java b/gdx/src/com/badlogic/gdx/AbstractInput.java index 5b0c54632a0..1da563270b3 100644 --- a/gdx/src/com/badlogic/gdx/AbstractInput.java +++ b/gdx/src/com/badlogic/gdx/AbstractInput.java @@ -37,26 +37,6 @@ public boolean isKeyJustPressed (int key) { return justPressedKeys[key]; } - @Override - public boolean isCatchBackKey () { - return keysToCatch.contains(Keys.BACK); - } - - @Override - public void setCatchBackKey (boolean catchBack) { - setCatchKey(Keys.BACK, catchBack); - } - - @Override - public boolean isCatchMenuKey () { - return keysToCatch.contains(Keys.MENU); - } - - @Override - public void setCatchMenuKey (boolean catchMenu) { - setCatchKey(Keys.MENU, catchMenu); - } - @Override public void setCatchKey (int keycode, boolean catchKey) { if (!catchKey) { diff --git a/gdx/src/com/badlogic/gdx/Input.java b/gdx/src/com/badlogic/gdx/Input.java index f135a0e953e..9fe3eb660a9 100644 --- a/gdx/src/com/badlogic/gdx/Input.java +++ b/gdx/src/com/badlogic/gdx/Input.java @@ -890,34 +890,6 @@ public enum VibrationType { /** @return the time of the event currently reported to the {@link InputProcessor}. */ public long getCurrentEventTime (); - /** @deprecated use {@link Input#setCatchKey(int keycode, boolean catchKey)} instead - * - * Sets whether the BACK button on Android should be caught. This will prevent the app from being paused. Will have - * no effect on the desktop. - * - * @param catchBack whether to catch the back button */ - @Deprecated - public void setCatchBackKey (boolean catchBack); - - /** @deprecated use {@link Input#isCatchKey(int keycode)} instead - * @return whether the back button is currently being caught */ - @Deprecated - public boolean isCatchBackKey (); - - /** @deprecated use {@link Input#setCatchKey(int keycode, boolean catchKey)} instead - * - * Sets whether the MENU button on Android should be caught. This will prevent the onscreen keyboard to show up. - * Will have no effect on the desktop. - * - * @param catchMenu whether to catch the menu button */ - @Deprecated - public void setCatchMenuKey (boolean catchMenu); - - /** @deprecated use {@link Input#isCatchKey(int keycode)} instead - * @return whether the menu button is currently being caught */ - @Deprecated - public boolean isCatchMenuKey (); - /** Sets whether the given key on Android or GWT should be caught. No effect on other platforms. All keys that are not caught * may be handled by other apps or background processes on Android, or may trigger default browser behaviour on GWT. For * example, media or volume buttons are handled by background media players if present, or Space key triggers a scroll. All diff --git a/gdx/src/com/badlogic/gdx/input/RemoteInput.java b/gdx/src/com/badlogic/gdx/input/RemoteInput.java index c8d0478c0f6..48bc8efcba5 100644 --- a/gdx/src/com/badlogic/gdx/input/RemoteInput.java +++ b/gdx/src/com/badlogic/gdx/input/RemoteInput.java @@ -511,26 +511,6 @@ public float getRoll () { return compass[2]; } - @Override - public void setCatchBackKey (boolean catchBack) { - - } - - @Override - public boolean isCatchBackKey () { - return false; - } - - @Override - public void setCatchMenuKey (boolean catchMenu) { - - } - - @Override - public boolean isCatchMenuKey () { - return false; - } - @Override public void setCatchKey (int keycode, boolean catchKey) { diff --git a/tests/gdx-tests-android/AndroidManifest.xml b/tests/gdx-tests-android/AndroidManifest.xml index 87b9a04718f..9bcf06514a8 100644 --- a/tests/gdx-tests-android/AndroidManifest.xml +++ b/tests/gdx-tests-android/AndroidManifest.xml @@ -14,7 +14,8 @@ android:label="@string/app_name" android:theme="@style/AppTheme" android:allowBackup="false" - tools:ignore="GoogleAppIndexingWarning"> + android:enableOnBackInvokedCallback="true" + tools:ignore="UnusedAttribute, GoogleAppIndexingWarning"> diff --git a/tests/gdx-tests-android/src/com/badlogic/gdx/tests/android/AndroidTestStarter.java b/tests/gdx-tests-android/src/com/badlogic/gdx/tests/android/AndroidTestStarter.java index 5852c700a40..652ef8efb35 100644 --- a/tests/gdx-tests-android/src/com/badlogic/gdx/tests/android/AndroidTestStarter.java +++ b/tests/gdx-tests-android/src/com/badlogic/gdx/tests/android/AndroidTestStarter.java @@ -28,6 +28,7 @@ import android.widget.ArrayAdapter; import android.widget.ListView; +import com.badlogic.gdx.tests.BackTest; import com.badlogic.gdx.tests.utils.GdxTests; public class AndroidTestStarter extends ListActivity { @@ -36,8 +37,9 @@ public class AndroidTestStarter extends ListActivity { @Override public void onCreate (Bundle savedInstanceState) { super.onCreate(savedInstanceState); - GdxTests.tests.add(MatrixTest.class); + if (!GdxTests.tests.contains(MatrixTest.class)) GdxTests.tests.add(MatrixTest.class); if (!GdxTests.tests.contains(APKExpansionTest.class)) GdxTests.tests.add(APKExpansionTest.class); + if (!GdxTests.tests.contains(BackTest.class)) GdxTests.tests.add(BackTest.class); List testNames = GdxTests.getNames(); setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, testNames)); diff --git a/tests/gdx-tests/src/com/badlogic/gdx/tests/AbstractTestWrapper.java b/tests/gdx-tests/src/com/badlogic/gdx/tests/AbstractTestWrapper.java index dd3c867b8f6..5cde9bef713 100644 --- a/tests/gdx-tests/src/com/badlogic/gdx/tests/AbstractTestWrapper.java +++ b/tests/gdx-tests/src/com/badlogic/gdx/tests/AbstractTestWrapper.java @@ -360,26 +360,6 @@ public long getCurrentEventTime () { return input.getCurrentEventTime(); } - @Override - public void setCatchBackKey (boolean catchBack) { - input.setCatchBackKey(catchBack); - } - - @Override - public boolean isCatchBackKey () { - return input.isCatchBackKey(); - } - - @Override - public void setCatchMenuKey (boolean catchMenu) { - input.setCatchMenuKey(catchMenu); - } - - @Override - public boolean isCatchMenuKey () { - return input.isCatchMenuKey(); - } - @Override public void setCatchKey (int keycode, boolean catchKey) { input.setCatchKey(keycode, catchKey); diff --git a/tests/gdx-tests/src/com/badlogic/gdx/tests/BackTest.java b/tests/gdx-tests/src/com/badlogic/gdx/tests/BackTest.java new file mode 100644 index 00000000000..a096a7adc3a --- /dev/null +++ b/tests/gdx-tests/src/com/badlogic/gdx/tests/BackTest.java @@ -0,0 +1,69 @@ + +package com.badlogic.gdx.tests; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.InputAdapter; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.tests.utils.GdxTest; +import com.badlogic.gdx.utils.ScreenUtils; +import com.badlogic.gdx.utils.viewport.FitViewport; +import com.badlogic.gdx.utils.viewport.Viewport; + +/** Check if predictive back gesture works, loosely modeled upon Android's back stack. Tap the screen to increment the counter. Go + * back to decrement the counter. If the counter is 0, the test will be exited. */ +public class BackTest extends GdxTest { + + private SpriteBatch batch; + private BitmapFont font; + private final Viewport viewport = new FitViewport(160, 90); + + private int stackDepth; + + @Override + public void create () { + batch = new SpriteBatch(); + font = new BitmapFont(); + Gdx.input.setInputProcessor(new InputAdapter() { + + @Override + public boolean touchDown (int screenX, int screenY, int pointer, int button) { + int screenWidth = Gdx.graphics.getBackBufferWidth(); + float safeZone = screenWidth * .1f; + if (screenX >= safeZone && screenX < screenWidth - safeZone) { + stackDepth++; + Gdx.input.setCatchKey(Input.Keys.BACK, stackDepth > 0); + return true; + } + return false; + } + + @Override + public boolean keyDown (int keycode) { + if (keycode == Input.Keys.BACK) { + stackDepth--; + Gdx.input.setCatchKey(Input.Keys.BACK, stackDepth > 0); + return true; + } + return false; + } + }); + } + + @Override + public void render () { + ScreenUtils.clear(Color.BLACK); + batch.begin(); + font.draw(batch, "Stack depth: " + stackDepth, 20, 50); + batch.end(); + } + + @Override + public void resize (int width, int height) { + viewport.update(width, height, true); + batch.setProjectionMatrix(viewport.getCamera().combined); + } + +}