mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-13 17:28:57 +07:00
Follow screen rotation even to Portrait on Android with Opt-in (#3936)
This commit is contained in:
@ -22,7 +22,6 @@
|
||||
android:name="com.unciv.app.AndroidLauncher"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="userLandscape"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
<intent-filter>
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.unciv.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@ -29,12 +30,19 @@ open class AndroidLauncher : AndroidApplication() {
|
||||
if (externalfilesDir != null) GameSaver.externalFilesDirForAndroid = externalfilesDir.path
|
||||
}
|
||||
|
||||
val config = AndroidApplicationConfiguration().apply { useImmersiveMode = true; }
|
||||
// Manage orientation lock
|
||||
val limitOrientationsHelper = LimitOrientationsHelperAndroid(this)
|
||||
limitOrientationsHelper.limitOrientations(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
|
||||
|
||||
val config = AndroidApplicationConfiguration().apply {
|
||||
useImmersiveMode = true;
|
||||
}
|
||||
val androidParameters = UncivGameParameters(
|
||||
version = BuildConfig.VERSION_NAME,
|
||||
crashReportSender = CrashReportSenderAndroid(this),
|
||||
fontImplementation = NativeFontAndroid(Fonts.ORIGINAL_FONT_SIZE.toInt()),
|
||||
customSaveLocationHelper = customSaveLocationHelper
|
||||
customSaveLocationHelper = customSaveLocationHelper,
|
||||
limitOrientationsHelper = limitOrientationsHelper
|
||||
)
|
||||
val game = UncivGame(androidParameters)
|
||||
initialize(game, config)
|
||||
|
65
android/src/com/unciv/app/LimitOrientationsHelperAndroid.kt
Normal file
65
android/src/com/unciv/app/LimitOrientationsHelperAndroid.kt
Normal file
@ -0,0 +1,65 @@
|
||||
package com.unciv.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Build
|
||||
import com.badlogic.gdx.files.FileHandle
|
||||
import com.unciv.logic.GameSaver
|
||||
import com.unciv.ui.utils.LimitOrientationsHelper
|
||||
import java.io.File
|
||||
|
||||
/** See also interface [LimitOrientationsHelper].
|
||||
*
|
||||
* The Android implementation (currently the only one) effectively ends up doing
|
||||
* [Activity.setRequestedOrientation]
|
||||
*/
|
||||
class LimitOrientationsHelperAndroid(private val activity: Activity) : LimitOrientationsHelper {
|
||||
/*
|
||||
companion object {
|
||||
// from android.content.res.Configuration.java
|
||||
// applicable to activity.resources.configuration
|
||||
const val ORIENTATION_UNDEFINED = 0
|
||||
const val ORIENTATION_PORTRAIT = 1
|
||||
const val ORIENTATION_LANDSCAPE = 2
|
||||
}
|
||||
*/
|
||||
|
||||
private class GameSettingsPreview(var allowAndroidPortrait: Boolean = false)
|
||||
|
||||
override fun allowPortrait(allow: Boolean) {
|
||||
val orientation = when {
|
||||
allow -> ActivityInfo.SCREEN_ORIENTATION_USER
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||
else -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
// Comparison ensures ActivityTaskManager.getService().setRequestedOrientation isn't called unless necessary
|
||||
if (activity.requestedOrientation != orientation) activity.requestedOrientation = orientation
|
||||
}
|
||||
|
||||
override fun limitOrientations(newOrientation: Int) {
|
||||
// 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
|
||||
if (newOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
|
||||
// Currently only the AndroidLauncher onCreate calls this with 'unspecified'.
|
||||
// determine whether to allow portrait from our settings file...
|
||||
// Gdx.files at this point is null, UncivGame.Current worse, so we'll do it classically.
|
||||
// Gdx parts used that *do* work: FileHandle (constructor, exists, reader) and Json
|
||||
val settingsPath = activity.applicationContext.filesDir.absolutePath + File.separator + GameSaver.settingsFileName
|
||||
val settingsFile = FileHandle(settingsPath)
|
||||
val setting =
|
||||
if (!settingsFile.exists()) {
|
||||
GameSettingsPreview()
|
||||
} else try {
|
||||
GameSaver.json().fromJson(GameSettingsPreview::class.java, settingsFile.reader())
|
||||
} catch (ex: java.lang.Exception) {
|
||||
GameSettingsPreview()
|
||||
}
|
||||
allowPortrait(setting.allowAndroidPortrait)
|
||||
} else {
|
||||
// Currently unused
|
||||
if (activity.requestedOrientation != newOrientation) activity.requestedOrientation = newOrientation
|
||||
}
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ import kotlin.concurrent.thread
|
||||
class MainMenuScreen: CameraStageBaseScreen() {
|
||||
private val autosave = "Autosave"
|
||||
private val backgroundTable = Table().apply { background=ImageGetter.getBackground(Color.WHITE) }
|
||||
private val singleColumn = isCrampedPortrait()
|
||||
|
||||
private fun getTableBlock(text: String, icon: String, function: () -> Unit): Table {
|
||||
val table = Table().pad(15f, 30f, 15f, 30f)
|
||||
@ -70,7 +71,8 @@ class MainMenuScreen: CameraStageBaseScreen() {
|
||||
}
|
||||
|
||||
val column1 = Table().apply { defaults().pad(10f) }
|
||||
val column2 = Table().apply { defaults().pad(10f) }
|
||||
val column2 = if(singleColumn) column1 else Table().apply { defaults().pad(10f) }
|
||||
|
||||
val autosaveGame = GameSaver.getSave(autosave, false)
|
||||
if (autosaveGame.exists()) {
|
||||
val resumeTable = getTableBlock("Resume","OtherIcons/Resume") { autoLoadGame() }
|
||||
@ -104,19 +106,21 @@ class MainMenuScreen: CameraStageBaseScreen() {
|
||||
column2.add(modsTable).row()
|
||||
|
||||
|
||||
|
||||
val optionsTable = getTableBlock("Options", "OtherIcons/Options")
|
||||
{ OptionsPopup(this).open() }
|
||||
val optionsTable = getTableBlock("Options", "OtherIcons/Options") {
|
||||
this.openOptionsPopup()
|
||||
}
|
||||
column2.add(optionsTable).row()
|
||||
|
||||
|
||||
val table=Table().apply { defaults().pad(10f) }
|
||||
table.add(column1)
|
||||
table.add(column2)
|
||||
if (!singleColumn) table.add(column2)
|
||||
table.pack()
|
||||
|
||||
stage.addActor(table)
|
||||
table.center(stage)
|
||||
val scrollPane = AutoScrollPane(table)
|
||||
scrollPane.setFillParent(true)
|
||||
stage.addActor(scrollPane)
|
||||
table.center(scrollPane)
|
||||
|
||||
onBackButtonClicked {
|
||||
if(hasOpenPopups()) {
|
||||
|
@ -34,6 +34,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
val fontImplementation = parameters.fontImplementation
|
||||
val consoleMode = parameters.consoleMode
|
||||
val customSaveLocationHelper = parameters.customSaveLocationHelper
|
||||
val limitOrientationsHelper = parameters.limitOrientationsHelper
|
||||
|
||||
lateinit var gameInfo: GameInfo
|
||||
fun isGameInfoInitialized() = this::gameInfo.isInitialized
|
||||
|
@ -2,6 +2,7 @@ package com.unciv
|
||||
|
||||
import com.unciv.logic.CustomSaveLocationHelper
|
||||
import com.unciv.ui.utils.CrashReportSender
|
||||
import com.unciv.ui.utils.LimitOrientationsHelper
|
||||
import com.unciv.ui.utils.NativeFontImplementation
|
||||
|
||||
class UncivGameParameters(val version: String,
|
||||
@ -9,5 +10,6 @@ class UncivGameParameters(val version: String,
|
||||
val cancelDiscordEvent: (() -> Unit)? = null,
|
||||
val fontImplementation: NativeFontImplementation? = null,
|
||||
val consoleMode: Boolean = false,
|
||||
val customSaveLocationHelper: CustomSaveLocationHelper? = null) {
|
||||
}
|
||||
val customSaveLocationHelper: CustomSaveLocationHelper? = null,
|
||||
val limitOrientationsHelper: LimitOrientationsHelper? = null
|
||||
) { }
|
||||
|
@ -11,7 +11,7 @@ import kotlin.concurrent.thread
|
||||
object GameSaver {
|
||||
private const val saveFilesFolder = "SaveFiles"
|
||||
private const val multiplayerFilesFolder = "MultiplayerGames"
|
||||
private const val settingsFileName = "GameSettings.json"
|
||||
const val settingsFileName = "GameSettings.json"
|
||||
|
||||
@Volatile
|
||||
var customSaveLocationHelper: CustomSaveLocationHelper? = null
|
||||
|
@ -44,6 +44,8 @@ class GameSettings {
|
||||
|
||||
var lastOverviewPage: String = "Cities"
|
||||
|
||||
var allowAndroidPortrait = false // Opt-in to allow Unciv to follow a screen rotation to portrait
|
||||
|
||||
init {
|
||||
// 26 = Android Oreo. Versions below may display permanent icon in notification bar.
|
||||
if (Gdx.app?.type == Application.ApplicationType.Android && Gdx.app.version < 26) {
|
||||
|
@ -13,9 +13,13 @@ import com.badlogic.gdx.scenes.scene2d.*
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.*
|
||||
import com.badlogic.gdx.scenes.scene2d.utils.Drawable
|
||||
import com.badlogic.gdx.utils.viewport.ExtendViewport
|
||||
import com.unciv.MainMenuScreen
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.models.Tutorial
|
||||
import com.unciv.ui.tutorials.TutorialController
|
||||
import com.unciv.ui.worldscreen.WorldScreen
|
||||
import com.unciv.ui.worldscreen.mainmenu.OptionsPopup
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
open class CameraStageBaseScreen : Screen {
|
||||
|
||||
@ -28,10 +32,10 @@ open class CameraStageBaseScreen : Screen {
|
||||
|
||||
init {
|
||||
val resolutions: List<Float> = game.settings.resolution.split("x").map { it.toInt().toFloat() }
|
||||
val width = resolutions[0]
|
||||
val height = resolutions[1]
|
||||
|
||||
stage = Stage(ExtendViewport(width, height), SpriteBatch())
|
||||
/** The ExtendViewport sets the _minimum_(!) world size - the actual world size will be larger, fitted to screen/window aspect ratio. */
|
||||
stage = Stage(ExtendViewport(height, height), SpriteBatch())
|
||||
|
||||
stage.addListener(
|
||||
object : InputListener() {
|
||||
@ -118,4 +122,31 @@ open class CameraStageBaseScreen : Screen {
|
||||
return listener
|
||||
}
|
||||
|
||||
fun isPortrait() = stage.viewport.screenHeight > stage.viewport.screenWidth
|
||||
fun isCrampedPortrait() = isPortrait() &&
|
||||
game.settings.resolution.split("x").map { it.toInt() }.last() <= 700
|
||||
|
||||
fun openOptionsPopup() {
|
||||
val limitOrientationsHelper = game.limitOrientationsHelper
|
||||
if (limitOrientationsHelper == null || !game.settings.allowAndroidPortrait || !isCrampedPortrait()) {
|
||||
OptionsPopup(this).open(force = true)
|
||||
return
|
||||
}
|
||||
if (!(this is MainMenuScreen || this is WorldScreen)) {
|
||||
throw IllegalArgumentException("openOptionsPopup called on wrong derivative class")
|
||||
}
|
||||
limitOrientationsHelper.allowPortrait(false)
|
||||
thread(name="WaitForRotation") {
|
||||
var waited = 0
|
||||
while (true) {
|
||||
val newScreen = (UncivGame.Current.screen as? CameraStageBaseScreen)
|
||||
if (waited >= 10000 || newScreen!=null && !newScreen.isPortrait() ) {
|
||||
Gdx.app.postRunnable { OptionsPopup(newScreen ?: this).open(true) }
|
||||
break
|
||||
}
|
||||
Thread.sleep(200)
|
||||
waited += 200
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
core/src/com/unciv/ui/utils/LimitOrientationsHelper.kt
Normal file
22
core/src/com/unciv/ui/utils/LimitOrientationsHelper.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package com.unciv.ui.utils
|
||||
|
||||
import com.unciv.models.metadata.GameSettings
|
||||
|
||||
/** Interface to support managing orientations
|
||||
*
|
||||
* You can turn a mobile device on its side or upside down, and a mobile OS may or may not allow the
|
||||
* position changes to automatically result in App orientation changes. This is about limiting that feature.
|
||||
*/
|
||||
interface LimitOrientationsHelper {
|
||||
/** Set a specific requested orientation or pull the setting from disk and act accordingly
|
||||
* @param newOrientation A SCREEN_ORIENTATION_* value from [ActivityInfo]
|
||||
* or SCREEN_ORIENTATION_UNSPECIFIED to load the setting
|
||||
*/
|
||||
fun limitOrientations(newOrientation: Int)
|
||||
|
||||
/** Pass a Boolean setting as used in [allowAndroidPortrait][GameSettings.allowAndroidPortrait] to the OS.
|
||||
* @param allow `true`: allow all orientations (follows sensor as limited by OS settings)
|
||||
* `false`: allow only landscape orientations (both if supported, otherwise default landscape only)
|
||||
*/
|
||||
fun allowPortrait(allow: Boolean)
|
||||
}
|
@ -237,7 +237,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Cam
|
||||
keyPressDispatcher[Input.Keys.F11] = quickSave // Quick Save
|
||||
keyPressDispatcher[Input.Keys.F12] = quickLoad // Quick Load
|
||||
keyPressDispatcher[Input.Keys.HOME] = { mapHolder.setCenterPosition(gameInfo.currentPlayerCiv.getCapital().location) } // Capital City View
|
||||
keyPressDispatcher['\u000F'] = { OptionsPopup(this).open() } // Ctrl-O: Game Options
|
||||
keyPressDispatcher['\u000F'] = { this.openOptionsPopup() } // Ctrl-O: Game Options
|
||||
keyPressDispatcher['\u0013'] = { game.setScreen(SaveGameScreen(gameInfo)) } // Ctrl-S: Save
|
||||
keyPressDispatcher['\u000C'] = { game.setScreen(LoadGameScreen(this)) } // Ctrl-L: Load
|
||||
keyPressDispatcher['+'] = { this.mapHolder.zoomIn() } // '+' Zoom - Input.Keys.NUMPAD_ADD would need dispatcher patch
|
||||
|
@ -45,6 +45,7 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
|
||||
add(scrollPane).maxHeight(screen.stage.height * 0.6f).row()
|
||||
|
||||
addCloseButton {
|
||||
previousScreen.game.limitOrientationsHelper?.allowPortrait(settings.allowAndroidPortrait)
|
||||
if (previousScreen is WorldScreen)
|
||||
previousScreen.enableNextTurnButtonAfterOptions()
|
||||
}
|
||||
@ -73,10 +74,11 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
|
||||
if (previousScreen is WorldScreen) {
|
||||
previousScreen.game.worldScreen = WorldScreen(previousScreen.gameInfo, previousScreen.viewingCiv)
|
||||
previousScreen.game.setWorldScreen()
|
||||
|
||||
} else if (previousScreen is MainMenuScreen) {
|
||||
previousScreen.game.setScreen(MainMenuScreen())
|
||||
}
|
||||
OptionsPopup(previousScreen.game.screen as CameraStageBaseScreen).open()
|
||||
(previousScreen.game.screen as CameraStageBaseScreen).openOptionsPopup()
|
||||
}
|
||||
|
||||
private fun rebuildOptionsTable() {
|
||||
@ -137,9 +139,17 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
|
||||
settings.showExperimentalWorldWrap)
|
||||
{ settings.showExperimentalWorldWrap = it }
|
||||
|
||||
if (previousScreen.game.limitOrientationsHelper != null) {
|
||||
addYesNoRow("Enable portrait orientation", settings.allowAndroidPortrait) {
|
||||
settings.allowAndroidPortrait = it
|
||||
// Note the following might close the options screen indirectly and delayed
|
||||
previousScreen.game.limitOrientationsHelper!!.allowPortrait(it)
|
||||
}
|
||||
}
|
||||
|
||||
addSoundEffectsVolumeSlider()
|
||||
addMusicVolumeSlider()
|
||||
|
||||
addTranslationGeneration()
|
||||
addModCheckerPopup()
|
||||
addSetUserId()
|
||||
|
@ -35,7 +35,7 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
|
||||
}
|
||||
|
||||
addMenuButton("Victory status") { worldScreen.game.setScreen(VictoryScreen(worldScreen)) }
|
||||
addMenuButton("Options") { OptionsPopup(worldScreen).open(force = true) }
|
||||
addMenuButton("Options") { worldScreen.openOptionsPopup() }
|
||||
addMenuButton("Community") { WorldScreenCommunityPopup(worldScreen).open(force = true) }
|
||||
|
||||
addSquareButton(Constants.close) {
|
||||
|
Reference in New Issue
Block a user