Android: selectable orientation (#8822)

* Android: selectable orientation

* Fix visible rectangle bug, fix auto-rotate behaviour, add translations

* Fix translation

---------

Co-authored-by: vegeta1k95 <vfylfhby>
This commit is contained in:
vegeta1k95
2023-03-06 09:35:14 +01:00
committed by GitHub
parent 41ee20efc5
commit b57232c992
11 changed files with 201 additions and 169 deletions

View File

@ -32,6 +32,7 @@
android:launchMode="singleTask"
android:exported="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:screenOrientation="userLandscape"
tools:ignore="LockedOrientationActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -1378,6 +1378,9 @@ Can be created instantly by =
Defence bonus =
Movement cost =
for =
Landscape (fixed) =
Portrait (fixed) =
Auto (sensor adjusted) =
Missing translations: =
Screen Size =
Screen Mode =

View File

@ -1,7 +1,11 @@
package com.unciv.app
import android.app.Activity
import android.content.pm.ActivityInfo
import android.database.ContentObserver
import android.os.Build
import android.os.Handler
import android.provider.Settings
import android.view.Display
import android.view.Display.Mode
import android.view.WindowManager
@ -11,6 +15,7 @@ import com.unciv.models.translations.tr
import com.unciv.utils.Log
import com.unciv.utils.PlatformDisplay
import com.unciv.utils.ScreenMode
import com.unciv.utils.ScreenOrientation
class AndroidScreenMode(
@ -65,10 +70,6 @@ class AndroidDisplay(private val activity: Activity) : PlatformDisplay {
return displayModes
}
override fun getDefaultMode(): ScreenMode {
return displayModes[0]!!
}
override fun setScreenMode(id: Int, settings: GameSettings) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.runOnUiThread {
@ -80,4 +81,56 @@ class AndroidDisplay(private val activity: Activity) : PlatformDisplay {
}
override fun hasCutout(): Boolean {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
activity.display?.cutout != null
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ->
@Suppress("DEPRECATION")
activity.windowManager.defaultDisplay.cutout != null
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ->
activity.window.decorView.rootWindowInsets.displayCutout != null
else -> false
}
}
override fun setCutout(enabled: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val params = activity.window.attributes
params.layoutInDisplayCutoutMode = when {
enabled -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
else -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
activity.window.attributes = params
}
}
/*
Sources for Info about current orientation in case need:
val windowManager = (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
val displayRotation = windowManager.defaultDisplay.rotation
val currentOrientation = activity.resources.configuration.orientation
const val ORIENTATION_UNDEFINED = 0
const val ORIENTATION_PORTRAIT = 1
const val ORIENTATION_LANDSCAPE = 2
*/
override fun hasOrientation(): Boolean {
return true
}
override fun setOrientation(orientation: ScreenOrientation) {
val mode = when (orientation) {
ScreenOrientation.Landscape -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
ScreenOrientation.Portrait -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
ScreenOrientation.Auto -> ActivityInfo.SCREEN_ORIENTATION_SENSOR
}
// Ensure ActivityTaskManager.getService().setRequestedOrientation isn't called unless necessary!
if (activity.requestedOrientation != mode)
activity.requestedOrientation = mode
}
}

View File

@ -1,35 +1,72 @@
package com.unciv.app
import android.os.Build
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.view.View
import android.view.ViewTreeObserver
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.backends.android.AndroidGraphics
import com.badlogic.gdx.math.Rectangle
import com.unciv.UncivGame
import com.unciv.logic.event.EventBus
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.basescreen.UncivStage
import com.unciv.utils.concurrency.Concurrency
class AndroidGame(private val activity: AndroidLauncher) : UncivGame() {
class AndroidGame : UncivGame() {
/*
Sources for Info about current orientation in case need:
val windowManager = (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
val displayRotation = windowManager.defaultDisplay.rotation
val currentOrientation = activity.resources.configuration.orientation
const val ORIENTATION_UNDEFINED = 0
const val ORIENTATION_PORTRAIT = 1
const val ORIENTATION_LANDSCAPE = 2
*/
fun addScreenObscuredListener() {
val contentView = (Gdx.graphics as AndroidGraphics).view
contentView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun hasDisplayCutout(): Boolean {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
activity.display?.cutout != null
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ->
@Suppress("DEPRECATION")
activity.windowManager.defaultDisplay.cutout != null
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ->
activity.window.decorView.rootWindowInsets.displayCutout != null
else -> false
/** [onGlobalLayout] gets triggered not only when the [View.getWindowVisibleDisplayFrame]
* changes, but also on other things. So we need to check if that was actually
* the thing that changed. */
private var lastFrame: Rect? = null
private var lastVisibleStage: Rectangle? = null
override fun onGlobalLayout() {
if (!isInitialized || screen == null)
return
val currentFrame = Rect()
contentView.getWindowVisibleDisplayFrame(currentFrame)
val stage = (screen as BaseScreen).stage
val horizontalRatio = stage.width / contentView.width
val verticalRatio = stage.height / contentView.height
// Android coordinate system has the origin in the top left,
// while GDX uses bottom left.
val visibleStage = Rectangle(
currentFrame.left * horizontalRatio,
(contentView.height - currentFrame.bottom) * verticalRatio,
currentFrame.width() * horizontalRatio,
currentFrame.height() * verticalRatio
)
if (lastFrame == currentFrame && lastVisibleStage == visibleStage)
return
lastFrame = currentFrame
lastVisibleStage = visibleStage
Concurrency.runOnGLThread {
EventBus.send(UncivStage.VisibleAreaChanged(visibleStage))
}
}
})
}
/** This is needed in onCreate _and_ onNewIntent to open links and notifications
* correctly even if the app was not running */
fun setDeepLinkedGame(intent: Intent) {
deepLinkedMultiplayerGame = if (intent.action != Intent.ACTION_VIEW) null else {
val uri: Uri? = intent.data
uri?.getQueryParameter("id")
}
}
override fun allowPortrait(allow: Boolean) {
activity.allowPortrait(allow)
}
}

View File

@ -1,39 +1,20 @@
package com.unciv.app
import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.Rect
import android.net.Uri
import android.opengl.GLSurfaceView
import android.os.Build
import android.os.Bundle
import android.view.Surface
import android.view.SurfaceHolder
import android.view.View
import android.view.ViewTreeObserver
import android.view.WindowManager
import androidx.core.app.NotificationManagerCompat
import androidx.work.WorkManager
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.backends.android.AndroidApplication
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration
import com.badlogic.gdx.backends.android.AndroidGraphics
import com.badlogic.gdx.math.Rectangle
import com.unciv.UncivGame
import com.unciv.logic.files.UncivFiles
import com.unciv.logic.event.EventBus
import com.unciv.ui.components.Fonts
import com.unciv.ui.screens.basescreen.UncivStage
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.utils.Display
import com.unciv.utils.Log
import com.unciv.utils.concurrency.Concurrency
import java.io.File
open class AndroidLauncher : AndroidApplication() {
private var game: AndroidGame? = null
private var deepLinkedMultiplayerGame: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -59,68 +40,15 @@ open class AndroidLauncher : AndroidApplication() {
val config = AndroidApplicationConfiguration().apply { useImmersiveMode = true }
val settings = UncivFiles.getSettingsForPlatformLaunchers(filesDir.path)
// Setup orientation lock and display cutout
allowPortrait(settings.allowAndroidPortrait)
setDisplayCutout(settings.androidCutout)
// Setup orientation and display cutout
Display.setOrientation(settings.displayOrientation)
Display.setCutout(settings.androidCutout)
game = AndroidGame(this)
game = AndroidGame()
initialize(game, config)
setDeepLinkedGame(intent)
val glView = (Gdx.graphics as AndroidGraphics).view as GLSurfaceView
addScreenObscuredListener(glView)
}
fun allowPortrait(allow: Boolean) {
val orientation = when {
allow -> ActivityInfo.SCREEN_ORIENTATION_USER
else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
// Comparison ensures ActivityTaskManager.getService().setRequestedOrientation isn't called unless necessary
if (requestedOrientation != orientation) requestedOrientation = orientation
}
private fun setDisplayCutout(cutout: Boolean) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return
window.attributes.layoutInDisplayCutoutMode = when {
cutout -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
else -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
}
private fun addScreenObscuredListener(surfaceView: GLSurfaceView) {
surfaceView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
/** [onGlobalLayout] gets triggered not only when the [windowVisibleDisplayFrame][View.getWindowVisibleDisplayFrame] changes, but also on other things.
* So we need to check if that was actually the thing that changed. */
private var lastVisibleDisplayFrame: Rect? = null
override fun onGlobalLayout() {
if (!UncivGame.isCurrentInitialized() || UncivGame.Current.screen == null) {
return
}
val r = Rect()
surfaceView.getWindowVisibleDisplayFrame(r)
if (r.equals(lastVisibleDisplayFrame)) return
lastVisibleDisplayFrame = r
val stage = (UncivGame.Current.screen as BaseScreen).stage
val horizontalRatio = stage.width / surfaceView.width
val verticalRatio = stage.height / surfaceView.height
val visibleStage = Rectangle(
r.left * horizontalRatio,
(surfaceView.height - r.bottom) * verticalRatio, // Android coordinate system has the origin in the top left, while GDX uses bottom left
r.width() * horizontalRatio,
r.height() * verticalRatio
)
Concurrency.runOnGLThread {
EventBus.send(UncivStage.VisibleAreaChanged(visibleStage))
}
}
})
game!!.setDeepLinkedGame(intent)
game!!.addScreenObscuredListener()
}
/**
@ -141,34 +69,29 @@ open class AndroidLauncher : AndroidApplication() {
}
override fun onPause() {
if (UncivGame.isCurrentInitialized()
&& UncivGame.Current.gameInfo != null
&& UncivGame.Current.settings.multiplayer.turnCheckerEnabled
&& UncivGame.Current.files.getMultiplayerSaves().any()
val game = this.game!!
if (game.isInitialized
&& game.gameInfo != null
&& game.settings.multiplayer.turnCheckerEnabled
&& game.files.getMultiplayerSaves().any()
) {
MultiplayerTurnCheckWorker.startTurnChecker(
applicationContext, UncivGame.Current.files,
UncivGame.Current.gameInfo!!, UncivGame.Current.settings.multiplayer
)
applicationContext, game.files, game.gameInfo!!, game.settings.multiplayer)
}
super.onPause()
}
override fun onResume() {
try { // Sometimes this fails for no apparent reason - the multiplayer checker failing to cancel should not be enough of a reason for the game to crash!
try {
WorkManager.getInstance(applicationContext).cancelAllWorkByTag(MultiplayerTurnCheckWorker.WORK_TAG)
with(NotificationManagerCompat.from(this)) {
cancel(MultiplayerTurnCheckWorker.NOTIFICATION_ID_INFO)
cancel(MultiplayerTurnCheckWorker.NOTIFICATION_ID_SERVICE)
}
} catch (ex: Exception) {
} catch (ignore: Exception) {
/* Sometimes this fails for no apparent reason - the multiplayer checker failing to
cancel should not be enough of a reason for the game to crash! */
}
if (deepLinkedMultiplayerGame != null) {
game?.deepLinkedMultiplayerGame = deepLinkedMultiplayerGame
deepLinkedMultiplayerGame = null
}
super.onResume()
}
@ -176,17 +99,7 @@ open class AndroidLauncher : AndroidApplication() {
super.onNewIntent(intent)
if (intent == null)
return
setDeepLinkedGame(intent)
}
private fun setDeepLinkedGame(intent: Intent) {
// This is needed in onCreate _and_ onNewIntent to open links and notifications
// correctly even if the app was not running
deepLinkedMultiplayerGame = if (intent.action != Intent.ACTION_VIEW) null else {
val uri: Uri? = intent.data
uri?.getQueryParameter("id")
}
game?.setDeepLinkedGame(intent)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

View File

@ -9,8 +9,7 @@ import com.unciv.logic.multiplayer.FriendList
import com.unciv.models.UncivSound
import com.unciv.ui.components.FontFamilyData
import com.unciv.ui.components.Fonts
import com.unciv.utils.Display
import com.unciv.utils.ScreenMode
import com.unciv.utils.ScreenOrientation
import java.text.Collator
import java.time.Duration
import java.util.*
@ -90,7 +89,8 @@ class GameSettings {
var lastOverviewPage: String = "Cities"
var allowAndroidPortrait = false // Opt-in to allow Unciv to follow a screen rotation to portrait
/** Orientation for mobile platforms */
var displayOrientation = ScreenOrientation.Landscape
/** Saves the last successful new game's setup */
var lastGameSetup: GameSetupInfo? = null

View File

@ -25,6 +25,7 @@ import com.unciv.ui.components.FontFamilyData
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.UncivSlider
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.center
import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.keyShortcuts
import com.unciv.ui.components.extensions.onActivation
@ -35,6 +36,8 @@ import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.extensions.withoutItem
import com.unciv.utils.Display
import com.unciv.utils.ScreenOrientation
import com.unciv.utils.concurrency.Concurrency
import com.unciv.utils.concurrency.launchOnGLThread
import kotlinx.coroutines.CoroutineScope
@ -54,21 +57,16 @@ fun advancedTab(
addAutosaveTurnsSelectBox(this, settings)
if (UncivGame.Current.hasDisplayCutout())
optionsPopup.addCheckbox(this, "Enable display cutout (requires restart)", settings.androidCutout) {
settings.androidCutout = it
}
if (Display.hasOrientation()) {
addOrientationSelectBox(this, optionsPopup)
}
if (Display.hasCutout()) {
addCutoutCheckbox(this, optionsPopup)
}
addMaxZoomSlider(this, settings)
if (Gdx.app.type == Application.ApplicationType.Android) {
optionsPopup.addCheckbox(this, "Enable portrait orientation", settings.allowAndroidPortrait) {
settings.allowAndroidPortrait = it
// Note the following might close the options screen indirectly and delayed
UncivGame.Current.allowPortrait(it)
}
}
addFontFamilySelect(this, settings, optionsPopup.selectBoxMinWidth, onFontChange)
addFontSizeMultiplier(this, settings, onFontChange)
@ -80,6 +78,31 @@ fun advancedTab(
addEasterEggsCheckBox(this, settings)
}
private fun addCutoutCheckbox(table: Table, optionsPopup: OptionsPopup) {
optionsPopup.addCheckbox(table, "Enable display cutout (requires restart)", optionsPopup.settings.androidCutout)
{
optionsPopup.settings.androidCutout = it
}
}
private fun addOrientationSelectBox(table: Table, optionsPopup: OptionsPopup) {
val settings = optionsPopup.settings
table.add("Screen orientation".toLabel()).left().fillX()
val selectBox = SelectBox<ScreenOrientation>(table.skin)
selectBox.items = Array(ScreenOrientation.values())
selectBox.selected = settings.displayOrientation
selectBox.onChange {
val orientation = selectBox.selected
settings.displayOrientation = orientation
Display.setOrientation(orientation)
}
table.add(selectBox).minWidth(optionsPopup.selectBoxMinWidth).pad(10f).row()
}
private fun addAutosaveTurnsSelectBox(table: Table, settings: GameSettings) {
table.add("Turns between autosaves".toLabel()).left().fillX()

View File

@ -122,7 +122,7 @@ class OptionsPopup(
addCloseButton {
screen.game.musicController.onChange(null)
screen.game.allowPortrait(settings.allowAndroidPortrait)
center(screen.stage)
onClose()
}.padBottom(10f)

View File

@ -1,6 +1,17 @@
package com.unciv.utils
import com.unciv.models.metadata.GameSettings
import com.unciv.models.translations.tr
enum class ScreenOrientation(val description: String) {
Landscape("Landscape (fixed)"),
Portrait("Portrait (fixed)"),
Auto("Auto (sensor adjusted)");
override fun toString(): String {
return description.tr()
}
}
interface ScreenMode {
fun getId(): Int
@ -10,24 +21,25 @@ interface PlatformDisplay {
fun setScreenMode(id: Int, settings: GameSettings) {}
fun getScreenModes(): Map<Int, ScreenMode> { return hashMapOf() }
fun getDefaultMode(): ScreenMode
fun hasCutout(): Boolean { return false }
fun setCutout(enabled: Boolean) {}
fun hasOrientation(): Boolean { return false }
fun setOrientation(orientation: ScreenOrientation) {}
}
object Display {
lateinit var platform: PlatformDisplay
fun getDefaultMode(): ScreenMode {
return platform.getDefaultMode()
}
fun hasOrientation(): Boolean { return platform.hasOrientation() }
fun setOrientation(orientation: ScreenOrientation) { platform.setOrientation(orientation) }
fun getScreenModes(): Map<Int, ScreenMode> {
return platform.getScreenModes()
}
fun hasCutout(): Boolean { return platform.hasCutout() }
fun setCutout(enabled: Boolean) { platform.setCutout(enabled) }
fun setScreenMode(id: Int, settings: GameSettings) {
platform.setScreenMode(id, settings)
}
fun getScreenModes(): Map<Int, ScreenMode> { return platform.getScreenModes() }
fun setScreenMode(id: Int, settings: GameSettings) { platform.setScreenMode(id, settings) }
}

View File

@ -8,10 +8,4 @@ interface PlatformSpecific {
/** Install system audio hooks */
fun installAudioHooks() {}
/** (Android) allow screen orientation switch */
fun allowPortrait(allow: Boolean) {}
/** (Android) returns whether display has cutout */
fun hasDisplayCutout(): Boolean { return false }
}

View File

@ -40,10 +40,6 @@ class DesktopDisplay : PlatformDisplay {
modes[2] = DesktopScreenMode(2, ScreenWindowType.Borderless)
}
override fun getDefaultMode(): ScreenMode {
return modes[0]!!
}
override fun getScreenModes(): Map<Int, ScreenMode> {
return modes
}