Add support for PiP mode
This commit is contained in:
parent
220b0b2f74
commit
ef0163ba9f
27 changed files with 405 additions and 52 deletions
|
|
@ -43,6 +43,8 @@ void MainLoop::_bind_methods() {
|
|||
BIND_CONSTANT(NOTIFICATION_APPLICATION_FOCUS_IN);
|
||||
BIND_CONSTANT(NOTIFICATION_APPLICATION_FOCUS_OUT);
|
||||
BIND_CONSTANT(NOTIFICATION_TEXT_SERVER_CHANGED);
|
||||
BIND_CONSTANT(NOTIFICATION_APPLICATION_PIP_MODE_ENTERED);
|
||||
BIND_CONSTANT(NOTIFICATION_APPLICATION_PIP_MODE_EXITED);
|
||||
|
||||
ADD_SIGNAL(MethodInfo("on_request_permissions_result", PropertyInfo(Variant::STRING, "permission"), PropertyInfo(Variant::BOOL, "granted")));
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ public:
|
|||
NOTIFICATION_APPLICATION_FOCUS_IN = 2016,
|
||||
NOTIFICATION_APPLICATION_FOCUS_OUT = 2017,
|
||||
NOTIFICATION_TEXT_SERVER_CHANGED = 2018,
|
||||
NOTIFICATION_APPLICATION_PIP_MODE_ENTERED = 2019,
|
||||
NOTIFICATION_APPLICATION_PIP_MODE_EXITED = 2020,
|
||||
};
|
||||
|
||||
virtual void initialize();
|
||||
|
|
|
|||
|
|
@ -1604,6 +1604,14 @@
|
|||
[b]Note:[/b] This method is implemented on Android, iOS, macOS, Windows, and Linux (X11/Wayland).
|
||||
</description>
|
||||
</method>
|
||||
<method name="is_in_pip_mode">
|
||||
<return type="bool" />
|
||||
<param index="0" name="window_id" type="int" default="0" />
|
||||
<description>
|
||||
Returns [code]true[/code] if the application is in picture-in-picture mode.
|
||||
[b]Note:[/b] This method is implemented on Android.
|
||||
</description>
|
||||
</method>
|
||||
<method name="is_touchscreen_available" qualifiers="const">
|
||||
<return type="bool" />
|
||||
<description>
|
||||
|
|
@ -1695,6 +1703,33 @@
|
|||
Sets the current mouse mode. See also [method mouse_get_mode].
|
||||
</description>
|
||||
</method>
|
||||
<method name="pip_mode_enter">
|
||||
<return type="void" />
|
||||
<param index="0" name="window_id" type="int" default="0" />
|
||||
<description>
|
||||
Enters picture-in-picture mode.
|
||||
[b]Note:[/b] This method is implemented on Android.
|
||||
</description>
|
||||
</method>
|
||||
<method name="pip_mode_set_aspect_ratio">
|
||||
<return type="void" />
|
||||
<param index="0" name="numerator" type="int" />
|
||||
<param index="1" name="denominator" type="int" />
|
||||
<param index="2" name="window_id" type="int" default="0" />
|
||||
<description>
|
||||
Specifies the aspect ratio for picture-in-picture mode.
|
||||
[b]Note:[/b] This method is implemented on Android.
|
||||
</description>
|
||||
</method>
|
||||
<method name="pip_mode_set_auto_enter_on_background">
|
||||
<return type="void" />
|
||||
<param index="0" name="auto_enter_on_background" type="bool" />
|
||||
<param index="1" name="window_id" type="int" default="0" />
|
||||
<description>
|
||||
Specifies whether picture-in-picture mode should be entered automatically when the application goes in the background.
|
||||
[b]Note:[/b] This method is implemented on Android.
|
||||
</description>
|
||||
</method>
|
||||
<method name="process_events">
|
||||
<return type="void" />
|
||||
<description>
|
||||
|
|
@ -2753,6 +2788,9 @@
|
|||
<constant name="FEATURE_HDR_OUTPUT" value="35" enum="Feature">
|
||||
Display server supports HDR output. [b]macOS, iOS, visionOS, Windows[/b]
|
||||
</constant>
|
||||
<constant name="FEATURE_PIP_MODE" value="36" enum="Feature">
|
||||
Display server supports putting the application in picture-in-picture mode. [b]Android[/b]
|
||||
</constant>
|
||||
<constant name="ROLE_UNKNOWN" value="0" enum="AccessibilityRole" deprecated="Use [AccessibilityServer] instead.">
|
||||
Unknown or custom role.
|
||||
</constant>
|
||||
|
|
|
|||
|
|
@ -143,5 +143,11 @@
|
|||
<constant name="NOTIFICATION_TEXT_SERVER_CHANGED" value="2018">
|
||||
Notification received when text server is changed.
|
||||
</constant>
|
||||
<constant name="NOTIFICATION_APPLICATION_PIP_MODE_ENTERED" value="2019">
|
||||
Notification received when the application enters picture-in-picture mode.
|
||||
</constant>
|
||||
<constant name="NOTIFICATION_APPLICATION_PIP_MODE_EXITED" value="2020">
|
||||
Notification received when the application exits picture-in-picture mode.
|
||||
</constant>
|
||||
</constants>
|
||||
</class>
|
||||
|
|
|
|||
|
|
@ -1323,6 +1323,12 @@
|
|||
<constant name="NOTIFICATION_TEXT_SERVER_CHANGED" value="2018">
|
||||
Notification received when the [TextServer] is changed.
|
||||
</constant>
|
||||
<constant name="NOTIFICATION_APPLICATION_PIP_MODE_ENTERED" value="2019">
|
||||
Notification received when the application enters picture-in-picture mode.
|
||||
</constant>
|
||||
<constant name="NOTIFICATION_APPLICATION_PIP_MODE_EXITED" value="2020">
|
||||
Notification received when the application exits picture-in-picture mode.
|
||||
</constant>
|
||||
<constant name="NOTIFICATION_ACCESSIBILITY_UPDATE" value="3000">
|
||||
Notification received when an accessibility information update is required.
|
||||
</constant>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ bool DisplayServerAndroid::has_feature(DisplayServerEnums::Feature p_feature) co
|
|||
return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU));
|
||||
} break;
|
||||
#endif
|
||||
case DisplayServerEnums::FEATURE_PIP_MODE: {
|
||||
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
|
||||
ERR_FAIL_NULL_V(godot_java, false);
|
||||
return godot_java->is_pip_mode_supported();
|
||||
} break;
|
||||
|
||||
case DisplayServerEnums::FEATURE_CURSOR_SHAPE:
|
||||
//case DisplayServerEnums::FEATURE_CUSTOM_CURSOR_SHAPE:
|
||||
//case DisplayServerEnums::FEATURE_HIDPI:
|
||||
|
|
@ -1003,3 +1009,31 @@ void DisplayServerAndroid::set_icon(const Ref<Image> &p_icon) {
|
|||
bool DisplayServerAndroid::is_window_transparency_available() const {
|
||||
return GLOBAL_GET_CACHED(bool, "display/window/per_pixel_transparency/allowed");
|
||||
}
|
||||
|
||||
bool DisplayServerAndroid::is_in_pip_mode(DisplayServerEnums::WindowID p_window) {
|
||||
ERR_FAIL_COND_V(p_window != DisplayServerEnums::MAIN_WINDOW_ID, false);
|
||||
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
|
||||
ERR_FAIL_NULL_V(godot_java, false);
|
||||
return godot_java->is_in_pip_mode();
|
||||
}
|
||||
|
||||
void DisplayServerAndroid::pip_mode_enter(DisplayServerEnums::WindowID p_window) {
|
||||
ERR_FAIL_COND(p_window != DisplayServerEnums::MAIN_WINDOW_ID);
|
||||
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
|
||||
ERR_FAIL_NULL(godot_java);
|
||||
godot_java->enter_pip_mode();
|
||||
}
|
||||
|
||||
void DisplayServerAndroid::pip_mode_set_aspect_ratio(int p_numerator, int p_denominator, DisplayServerEnums::WindowID p_window) {
|
||||
ERR_FAIL_COND(p_window != DisplayServerEnums::MAIN_WINDOW_ID);
|
||||
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
|
||||
ERR_FAIL_NULL(godot_java);
|
||||
godot_java->set_pip_mode_aspect_ratio(p_numerator, p_denominator);
|
||||
}
|
||||
|
||||
void DisplayServerAndroid::pip_mode_set_auto_enter_on_background(bool p_auto_enter_on_background, DisplayServerEnums::WindowID p_window) {
|
||||
ERR_FAIL_COND(p_window != DisplayServerEnums::MAIN_WINDOW_ID);
|
||||
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
|
||||
ERR_FAIL_NULL(godot_java);
|
||||
godot_java->set_auto_enter_pip_mode_on_background(p_auto_enter_on_background);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -269,6 +269,11 @@ public:
|
|||
|
||||
virtual bool is_window_transparency_available() const override;
|
||||
|
||||
virtual bool is_in_pip_mode(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
|
||||
virtual void pip_mode_enter(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
|
||||
virtual void pip_mode_set_aspect_ratio(int p_numerator, int p_denominator, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
|
||||
virtual void pip_mode_set_auto_enter_on_background(bool p_auto_enter_on_background, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
|
||||
|
||||
DisplayServerAndroid(const String &p_rendering_driver, DisplayServerEnums::WindowMode p_mode, DisplayServerEnums::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, DisplayServerEnums::Context p_context, int64_t p_parent_window, Error &r_error);
|
||||
~DisplayServerAndroid();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
android:launchMode="singleInstancePerTask"
|
||||
android:excludeFromRecents="false"
|
||||
android:exported="false"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
|
|
|
|||
|
|
@ -92,4 +92,9 @@ public class GodotApp extends GodotActivity {
|
|||
super.onGodotForceQuit(instance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isPiPEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,12 +30,7 @@
|
|||
|
||||
package org.godotengine.editor
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.core.view.isVisible
|
||||
|
|
@ -55,7 +50,6 @@ open class GodotGame : BaseGodotGame() {
|
|||
private val TAG = GodotGame::class.java.simpleName
|
||||
}
|
||||
|
||||
private val gameViewSourceRectHint = Rect()
|
||||
private val expandGameMenuButton: View? by lazy { findViewById(R.id.game_menu_expand_button) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -75,13 +69,6 @@ open class GodotGame : BaseGodotGame() {
|
|||
gameMenuFragment?.expandGameMenu()
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val gameView = findViewById<View>(R.id.godot_fragment_container)
|
||||
gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
||||
gameView.getGlobalVisibleRect(gameViewSourceRectHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCommandLine(): MutableList<String> {
|
||||
|
|
@ -96,27 +83,7 @@ open class GodotGame : BaseGodotGame() {
|
|||
return updatedArgs
|
||||
}
|
||||
|
||||
override fun enterPiPMode() {
|
||||
if (hasPiPSystemFeature()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setSeamlessResizeEnabled(false)
|
||||
}
|
||||
setPictureInPictureParams(builder.build())
|
||||
}
|
||||
|
||||
Log.v(TAG, "Entering PiP mode")
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true the if the device supports picture-in-picture (PiP).
|
||||
*/
|
||||
protected fun hasPiPSystemFeature(): Boolean {
|
||||
return packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||
}
|
||||
override fun isPiPEnabled() = true
|
||||
|
||||
override fun shouldShowGameMenuBar(): Boolean {
|
||||
return intent.getBooleanExtra(
|
||||
|
|
@ -127,21 +94,11 @@ open class GodotGame : BaseGodotGame() {
|
|||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
|
||||
|
||||
// Hide the game menu fragment when in PiP.
|
||||
gameMenuContainer?.isVisible = !isInPictureInPictureMode
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
if (isInPictureInPictureMode && !isFinishing) {
|
||||
// We get in this state when PiP is closed, so we terminate the activity.
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getGodotAppLayout() = R.layout.godot_game_layout
|
||||
|
||||
override fun getEditorWindowInfo() = RUN_GAME_INFO
|
||||
|
|
@ -258,7 +215,7 @@ open class GodotGame : BaseGodotGame() {
|
|||
|
||||
override fun isCloseButtonEnabled() = !isHorizonOSDevice(applicationContext)
|
||||
|
||||
override fun isPiPButtonEnabled() = hasPiPSystemFeature()
|
||||
override fun isPiPButtonEnabled() = isPiPModeSupported()
|
||||
|
||||
override fun isMenuBarCollapsable() = true
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ class EmbeddedGodotGame : GodotGame() {
|
|||
|
||||
override fun isMenuBarCollapsable() = false
|
||||
|
||||
override fun isAlwaysOnTopSupported() = hasPiPSystemFeature()
|
||||
override fun isAlwaysOnTopSupported() = isPiPModeSupported()
|
||||
|
||||
override fun onFullScreenUpdated(enabled: Boolean) {
|
||||
godot?.enableImmersiveMode(enabled)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import androidx.fragment.app.Fragment
|
|||
import org.godotengine.editor.BaseGodotEditor
|
||||
import org.godotengine.editor.BaseGodotEditor.Companion.SNACKBAR_SHOW_DURATION_MS
|
||||
import org.godotengine.editor.R
|
||||
import org.godotengine.godot.feature.PictureInPictureProvider
|
||||
import org.godotengine.godot.utils.DialogUtils
|
||||
|
||||
/**
|
||||
|
|
@ -65,7 +66,7 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
|||
/**
|
||||
* Used to be notified of events fired when interacting with the game menu.
|
||||
*/
|
||||
interface GameMenuListener {
|
||||
interface GameMenuListener : PictureInPictureProvider {
|
||||
|
||||
/**
|
||||
* Kotlin representation of the RuntimeNodeSelect::SelectMode enum in 'scene/debugger/scene_debugger.h'.
|
||||
|
|
@ -109,7 +110,6 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
|||
fun isGameEmbeddingSupported(): Boolean
|
||||
fun embedGameOnPlay(embedded: Boolean)
|
||||
|
||||
fun enterPiPMode() {}
|
||||
fun minimizeGameWindow() {}
|
||||
fun closeGameWindow() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import android.hardware.Sensor
|
|||
import android.hardware.SensorManager
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.widget.FrameLayout
|
||||
|
|
@ -57,6 +58,7 @@ import androidx.core.view.WindowInsetsCompat
|
|||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.google.android.vending.expansion.downloader.*
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.feature.PictureInPictureProvider
|
||||
import org.godotengine.godot.input.GodotEditText
|
||||
import org.godotengine.godot.input.GodotInputHandler
|
||||
import org.godotengine.godot.io.FilePicker
|
||||
|
|
@ -716,6 +718,13 @@ class Godot private constructor(val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
|
||||
runOnRenderThread {
|
||||
GodotLib.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPause(host: GodotHost) {
|
||||
Log.v(TAG, "OnPause: $host")
|
||||
resumed = false
|
||||
|
|
@ -1372,4 +1381,52 @@ class Godot private constructor(val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeIsPiPModeSupported(): Boolean {
|
||||
val hostActivity = getActivity()
|
||||
if (hostActivity is PictureInPictureProvider) {
|
||||
return hostActivity.isPiPModeSupported()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeIsInPiPMode(): Boolean {
|
||||
val hostActivity = getActivity()
|
||||
if (hostActivity is GodotActivity) {
|
||||
return hostActivity.isInPictureInPictureMode
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeEnterPiPMode() {
|
||||
val hostActivity = getActivity()
|
||||
if (hostActivity is PictureInPictureProvider) {
|
||||
runOnHostThread {
|
||||
hostActivity.enterPiPMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeSetPiPModeAspectRatio(numerator: Int, denominator: Int) {
|
||||
val hostActivity = getActivity()
|
||||
if (hostActivity is GodotActivity) {
|
||||
runOnHostThread {
|
||||
hostActivity.updatePiPParams(aspectRatio = Rational(numerator, denominator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeSetAutoEnterPiPModeOnBackground(autoEnterPiPOnBackground: Boolean) {
|
||||
val hostActivity = getActivity()
|
||||
if (hostActivity is GodotActivity) {
|
||||
runOnHostThread {
|
||||
hostActivity.updatePiPParams(enableAutoEnter = autoEnterPiPOnBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,17 +31,25 @@
|
|||
package org.godotengine.godot
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import org.godotengine.godot.feature.PictureInPictureProvider
|
||||
import org.godotengine.godot.utils.CommandLineFileParser
|
||||
import org.godotengine.godot.utils.PermissionsUtil
|
||||
import org.godotengine.godot.utils.ProcessPhoenix
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
* Base abstract activity for Android apps intending to use Godot as the primary screen.
|
||||
|
|
@ -49,7 +57,7 @@ import org.godotengine.godot.utils.ProcessPhoenix
|
|||
* Also a reference implementation for how to setup and use the [GodotFragment] fragment
|
||||
* within an Android app.
|
||||
*/
|
||||
abstract class GodotActivity : FragmentActivity(), GodotHost {
|
||||
abstract class GodotActivity : FragmentActivity(), GodotHost, PictureInPictureProvider {
|
||||
|
||||
companion object {
|
||||
private val TAG = GodotActivity::class.java.simpleName
|
||||
|
|
@ -65,6 +73,12 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||
private final val DEFAULT_WINDOW_ID = 664;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if the activity should automatically enter picture-in-picture when put in the background.
|
||||
*/
|
||||
private val pipAspectRatio = AtomicReference<Rational>()
|
||||
private val autoEnterPiP = AtomicBoolean(false)
|
||||
private val gameViewSourceRectHint = Rect()
|
||||
private val commandLineParams = ArrayList<String>()
|
||||
/**
|
||||
* Interaction with the [Godot] object is delegated to the [GodotFragment] class.
|
||||
|
|
@ -139,6 +153,13 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||
.setPrimaryNavigationFragment(godotFragment)
|
||||
.commitNowAllowingStateLoss()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val gameView = findViewById<View>(R.id.godot_fragment_container)
|
||||
gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
||||
gameView.getGlobalVisibleRect(gameViewSourceRectHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewGodotInstanceRequested(args: Array<String>): Int {
|
||||
|
|
@ -149,7 +170,7 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||
.putExtra(EXTRA_COMMAND_LINE_PARAMS, args)
|
||||
triggerRebirth(null, intent)
|
||||
// fake 'process' id returned by create_instance() etc
|
||||
return DEFAULT_WINDOW_ID;
|
||||
return DEFAULT_WINDOW_ID
|
||||
}
|
||||
|
||||
protected fun triggerRebirth(bundle: Bundle?, intent: Intent) {
|
||||
|
|
@ -167,6 +188,15 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
if (isInPictureInPictureMode && !isFinishing) {
|
||||
// We get in this state when PiP is closed, so we terminate the activity.
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGodotForceQuit(instance: Godot) {
|
||||
runOnUiThread { terminateGodotInstance(instance) }
|
||||
}
|
||||
|
|
@ -196,6 +226,23 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onGodotSetupCompleted() {
|
||||
super.onGodotSetupCompleted()
|
||||
|
||||
if (isPiPEnabled()) {
|
||||
try {
|
||||
// Update the aspect ratio for picture-in-picture mode.
|
||||
val viewportWidth = Integer.parseInt(GodotLib.getGlobal("display/window/size/viewport_width"))
|
||||
val viewportHeight = Integer.parseInt(GodotLib.getGlobal("display/window/size/viewport_height"))
|
||||
pipAspectRatio.set(Rational(viewportWidth, viewportHeight))
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.w(TAG, "Unable to parse viewport dimensions.", e)
|
||||
}
|
||||
|
||||
runOnHostThread { updatePiPParams() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(newIntent: Intent) {
|
||||
intent = sanitizeLaunchIntent(newIntent)
|
||||
super.onNewIntent(intent)
|
||||
|
|
@ -257,4 +304,56 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||
|
||||
@CallSuper
|
||||
override fun getCommandLine(): MutableList<String> = commandLineParams
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
godot?.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if picture-in-picture (PiP) mode is supported.
|
||||
*/
|
||||
override fun isPiPModeSupported() = isPiPEnabled() && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||
|
||||
/**
|
||||
* Returns true if the current activity has enabled picture-in-picture in its manifest declaration using
|
||||
* 'android:supportsPictureInPicture="true"'
|
||||
*/
|
||||
protected open fun isPiPEnabled() = false
|
||||
|
||||
internal fun updatePiPParams(enableAutoEnter: Boolean = autoEnterPiP.get(), aspectRatio: Rational? = pipAspectRatio.get()) {
|
||||
if (isPiPModeSupported()) {
|
||||
autoEnterPiP.set(enableAutoEnter)
|
||||
pipAspectRatio.set(aspectRatio)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val builder = PictureInPictureParams.Builder()
|
||||
.setSourceRectHint(gameViewSourceRectHint)
|
||||
.setAspectRatio(aspectRatio)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setSeamlessResizeEnabled(false)
|
||||
.setAutoEnterEnabled(enableAutoEnter)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
builder.setExpandedAspectRatio(aspectRatio)
|
||||
}
|
||||
setPictureInPictureParams(builder.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun enterPiPMode() {
|
||||
if (isPiPModeSupported()) {
|
||||
updatePiPParams()
|
||||
|
||||
Log.v(TAG, "Entering PiP mode")
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
if (autoEnterPiP.get()) {
|
||||
enterPiPMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -320,4 +320,6 @@ public class GodotLib {
|
|||
static native boolean isProjectManagerHint();
|
||||
|
||||
static native boolean hasFeature(String feature);
|
||||
|
||||
static native void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
/**************************************************************************/
|
||||
/* PictureInPictureProvider.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.feature
|
||||
|
||||
/**
|
||||
* Provides APIs to enable picture-in-picture.
|
||||
*/
|
||||
interface PictureInPictureProvider {
|
||||
|
||||
fun enterPiPMode()
|
||||
|
||||
fun isPiPModeSupported(): Boolean
|
||||
}
|
||||
|
|
@ -731,4 +731,18 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_hasFeature(JNIEnv
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onPictureInPictureModeChanged(JNIEnv *env, jclass clazz, jboolean p_is_in_picture_in_picture_mode) {
|
||||
if (step.get() <= STEP_SETUP) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (os_android->get_main_loop()) {
|
||||
if (p_is_in_picture_in_picture_mode) {
|
||||
os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_PIP_MODE_ENTERED);
|
||||
} else {
|
||||
os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_PIP_MODE_EXITED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,4 +78,5 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResource
|
|||
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isEditorHint(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isProjectManagerHint(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_hasFeature(JNIEnv *env, jclass clazz, jstring p_feature);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onPictureInPictureModeChanged(JNIEnv *env, jclass clazz, jboolean p_is_in_picture_in_picture_mode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,13 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance) {
|
|||
_build_env_execute = p_env->GetMethodID(godot_class, "nativeBuildEnvExecute", "(Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/godotengine/godot/variant/Callable;Lorg/godotengine/godot/variant/Callable;)I");
|
||||
_build_env_cancel = p_env->GetMethodID(godot_class, "nativeBuildEnvCancel", "(I)V");
|
||||
_build_env_clean_project = p_env->GetMethodID(godot_class, "nativeBuildEnvCleanProject", "(Ljava/lang/String;Ljava/lang/String;Lorg/godotengine/godot/variant/Callable;)V");
|
||||
|
||||
// PiP mode method ids.
|
||||
_is_pip_mode_supported = p_env->GetMethodID(godot_class, "nativeIsPiPModeSupported", "()Z");
|
||||
_is_in_pip_mode = p_env->GetMethodID(godot_class, "nativeIsInPiPMode", "()Z");
|
||||
_enter_pip_mode = p_env->GetMethodID(godot_class, "nativeEnterPiPMode", "()V");
|
||||
_set_pip_mode_aspect_ratio = p_env->GetMethodID(godot_class, "nativeSetPiPModeAspectRatio", "(II)V");
|
||||
_set_auto_enter_pip_mode_on_background = p_env->GetMethodID(godot_class, "nativeSetAutoEnterPiPModeOnBackground", "(Z)V");
|
||||
}
|
||||
|
||||
GodotJavaWrapper::~GodotJavaWrapper() {
|
||||
|
|
@ -705,3 +712,47 @@ void GodotJavaWrapper::build_env_clean_project(const String &p_project_path, con
|
|||
env->DeleteLocalRef(j_callback);
|
||||
}
|
||||
}
|
||||
|
||||
bool GodotJavaWrapper::is_pip_mode_supported() {
|
||||
if (_is_pip_mode_supported) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL_V(env, false);
|
||||
return env->CallBooleanMethod(godot_instance, _is_pip_mode_supported);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool GodotJavaWrapper::is_in_pip_mode() {
|
||||
if (_is_in_pip_mode) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL_V(env, false);
|
||||
return env->CallBooleanMethod(godot_instance, _is_in_pip_mode);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void GodotJavaWrapper::enter_pip_mode() {
|
||||
if (_enter_pip_mode) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL(env);
|
||||
env->CallVoidMethod(godot_instance, _enter_pip_mode);
|
||||
}
|
||||
}
|
||||
|
||||
void GodotJavaWrapper::set_pip_mode_aspect_ratio(int p_numerator, int p_denominator) {
|
||||
if (_set_pip_mode_aspect_ratio) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL(env);
|
||||
env->CallVoidMethod(godot_instance, _set_pip_mode_aspect_ratio, p_numerator, p_denominator);
|
||||
}
|
||||
}
|
||||
|
||||
void GodotJavaWrapper::set_auto_enter_pip_mode_on_background(bool p_auto_enter_on_background) {
|
||||
if (_set_auto_enter_pip_mode_on_background) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL(env);
|
||||
env->CallVoidMethod(godot_instance, _set_auto_enter_pip_mode_on_background, p_auto_enter_on_background);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,11 @@ private:
|
|||
jmethodID _build_env_execute = nullptr;
|
||||
jmethodID _build_env_cancel = nullptr;
|
||||
jmethodID _build_env_clean_project = nullptr;
|
||||
jmethodID _is_pip_mode_supported = nullptr;
|
||||
jmethodID _is_in_pip_mode = nullptr;
|
||||
jmethodID _enter_pip_mode = nullptr;
|
||||
jmethodID _set_pip_mode_aspect_ratio = nullptr;
|
||||
jmethodID _set_auto_enter_pip_mode_on_background = nullptr;
|
||||
|
||||
public:
|
||||
GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance);
|
||||
|
|
@ -154,4 +159,10 @@ public:
|
|||
int build_env_execute(const String &p_build_tool, const List<String> &p_arguments, const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_output_callback, const Callable &p_result_callback);
|
||||
void build_env_cancel(int p_job_id);
|
||||
void build_env_clean_project(const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_callback);
|
||||
|
||||
bool is_pip_mode_supported();
|
||||
bool is_in_pip_mode();
|
||||
void enter_pip_mode();
|
||||
void set_pip_mode_aspect_ratio(int p_numerator, int p_denominator);
|
||||
void set_auto_enter_pip_mode_on_background(bool p_auto_enter_on_background);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -435,7 +435,9 @@ void OS_Android::main_loop_focusout() {
|
|||
if (OS::get_singleton()->get_main_loop()) {
|
||||
OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT);
|
||||
}
|
||||
audio_driver_android.set_pause(true);
|
||||
|
||||
// Only pause when we are not in PiP mode.
|
||||
audio_driver_android.set_pause(!DisplayServerAndroid::get_singleton()->is_in_pip_mode());
|
||||
}
|
||||
|
||||
void OS_Android::main_loop_focusin() {
|
||||
|
|
|
|||
|
|
@ -3964,6 +3964,8 @@ void Node::_bind_methods() {
|
|||
BIND_CONSTANT(NOTIFICATION_APPLICATION_FOCUS_IN);
|
||||
BIND_CONSTANT(NOTIFICATION_APPLICATION_FOCUS_OUT);
|
||||
BIND_CONSTANT(NOTIFICATION_TEXT_SERVER_CHANGED);
|
||||
BIND_CONSTANT(NOTIFICATION_APPLICATION_PIP_MODE_ENTERED);
|
||||
BIND_CONSTANT(NOTIFICATION_APPLICATION_PIP_MODE_EXITED);
|
||||
|
||||
BIND_CONSTANT(NOTIFICATION_ACCESSIBILITY_UPDATE);
|
||||
BIND_CONSTANT(NOTIFICATION_ACCESSIBILITY_INVALIDATE);
|
||||
|
|
|
|||
|
|
@ -498,6 +498,8 @@ public:
|
|||
NOTIFICATION_APPLICATION_FOCUS_IN = 2016,
|
||||
NOTIFICATION_APPLICATION_FOCUS_OUT = 2017,
|
||||
NOTIFICATION_TEXT_SERVER_CHANGED = 2018,
|
||||
NOTIFICATION_APPLICATION_PIP_MODE_ENTERED = 2019,
|
||||
NOTIFICATION_APPLICATION_PIP_MODE_EXITED = 2020,
|
||||
|
||||
// Editor specific node notifications
|
||||
NOTIFICATION_EDITOR_PRE_SAVE = 9001,
|
||||
|
|
|
|||
|
|
@ -920,7 +920,9 @@ void SceneTree::_notification(int p_notification) {
|
|||
case NOTIFICATION_WM_ABOUT:
|
||||
case NOTIFICATION_CRASH:
|
||||
case NOTIFICATION_APPLICATION_RESUMED:
|
||||
case NOTIFICATION_APPLICATION_PAUSED: {
|
||||
case NOTIFICATION_APPLICATION_PAUSED:
|
||||
case NOTIFICATION_APPLICATION_PIP_MODE_ENTERED:
|
||||
case NOTIFICATION_APPLICATION_PIP_MODE_EXITED: {
|
||||
// Pass these to nodes, since they are mirrored.
|
||||
get_root()->propagate_notification(p_notification);
|
||||
} break;
|
||||
|
|
|
|||
|
|
@ -1722,6 +1722,11 @@ void DisplayServer::_bind_methods() {
|
|||
ClassDB::bind_method(D_METHOD("unregister_additional_output", "object"), &DisplayServer::unregister_additional_output);
|
||||
ClassDB::bind_method(D_METHOD("has_additional_outputs"), &DisplayServer::has_additional_outputs);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("is_in_pip_mode", "window_id"), &DisplayServer::is_in_pip_mode, DEFVAL(DisplayServerEnums::MAIN_WINDOW_ID));
|
||||
ClassDB::bind_method(D_METHOD("pip_mode_enter", "window_id"), &DisplayServer::pip_mode_enter, DEFVAL(DisplayServerEnums::MAIN_WINDOW_ID));
|
||||
ClassDB::bind_method(D_METHOD("pip_mode_set_aspect_ratio", "numerator", "denominator", "window_id"), &DisplayServer::pip_mode_set_aspect_ratio, DEFVAL(DisplayServerEnums::MAIN_WINDOW_ID));
|
||||
ClassDB::bind_method(D_METHOD("pip_mode_set_auto_enter_on_background", "auto_enter_on_background", "window_id"), &DisplayServer::pip_mode_set_auto_enter_on_background, DEFVAL(DisplayServerEnums::MAIN_WINDOW_ID));
|
||||
|
||||
#ifndef DISABLE_DEPRECATED
|
||||
BIND_ENUM_CONSTANT(DisplayServerEnums::FEATURE_GLOBAL_MENU);
|
||||
#endif // DISABLE_DEPRECATED
|
||||
|
|
@ -1759,6 +1764,7 @@ void DisplayServer::_bind_methods() {
|
|||
BIND_ENUM_CONSTANT(DisplayServerEnums::FEATURE_SELF_FITTING_WINDOWS);
|
||||
BIND_ENUM_CONSTANT(DisplayServerEnums::FEATURE_ACCESSIBILITY_SCREEN_READER);
|
||||
BIND_ENUM_CONSTANT(DisplayServerEnums::FEATURE_HDR_OUTPUT);
|
||||
BIND_ENUM_CONSTANT(DisplayServerEnums::FEATURE_PIP_MODE);
|
||||
|
||||
#ifndef DISABLE_DEPRECATED
|
||||
BIND_ENUM_CONSTANT(DisplayServerEnums::ROLE_UNKNOWN);
|
||||
|
|
|
|||
|
|
@ -531,6 +531,12 @@ public:
|
|||
void unregister_additional_output(Object *p_output);
|
||||
bool has_additional_outputs() const { return additional_outputs.size() > 0; }
|
||||
|
||||
/* PICTURE_IN_PICTURE */
|
||||
virtual bool is_in_pip_mode(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) { return false; }
|
||||
virtual void pip_mode_enter(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) {}
|
||||
virtual void pip_mode_set_aspect_ratio(int p_numerator, int p_denominator, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) {}
|
||||
virtual void pip_mode_set_auto_enter_on_background(bool p_auto_enter_on_background, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) {}
|
||||
|
||||
/* ACCESSIBILITY */
|
||||
|
||||
#ifndef DISABLE_DEPRECATED
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ enum Feature {
|
|||
FEATURE_SELF_FITTING_WINDOWS,
|
||||
FEATURE_ACCESSIBILITY_SCREEN_READER,
|
||||
FEATURE_HDR_OUTPUT,
|
||||
FEATURE_PIP_MODE,
|
||||
};
|
||||
|
||||
/* RENDERING DEVICE */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue