Add support for PiP mode
This commit is contained in:
parent
220b0b2f74
commit
ef0163ba9f
27 changed files with 405 additions and 52 deletions
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue