Follow screen rotation even to Portrait on Android with Opt-in (#3936)

This commit is contained in:
SomeTroglodyte
2021-05-19 22:27:23 +02:00
committed by GitHub
parent 929c357663
commit 3e3bda42e5
13 changed files with 164 additions and 20 deletions

View File

@ -22,7 +22,6 @@
android:name="com.unciv.app.AndroidLauncher" android:name="com.unciv.app.AndroidLauncher"
android:launchMode="singleTask" android:launchMode="singleTask"
android:label="@string/app_name" android:label="@string/app_name"
android:screenOrientation="userLandscape"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
tools:ignore="LockedOrientationActivity"> tools:ignore="LockedOrientationActivity">
<intent-filter> <intent-filter>

View File

@ -1,6 +1,7 @@
package com.unciv.app package com.unciv.app
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@ -29,12 +30,19 @@ open class AndroidLauncher : AndroidApplication() {
if (externalfilesDir != null) GameSaver.externalFilesDirForAndroid = externalfilesDir.path 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( val androidParameters = UncivGameParameters(
version = BuildConfig.VERSION_NAME, version = BuildConfig.VERSION_NAME,
crashReportSender = CrashReportSenderAndroid(this), crashReportSender = CrashReportSenderAndroid(this),
fontImplementation = NativeFontAndroid(Fonts.ORIGINAL_FONT_SIZE.toInt()), fontImplementation = NativeFontAndroid(Fonts.ORIGINAL_FONT_SIZE.toInt()),
customSaveLocationHelper = customSaveLocationHelper customSaveLocationHelper = customSaveLocationHelper,
limitOrientationsHelper = limitOrientationsHelper
) )
val game = UncivGame(androidParameters) val game = UncivGame(androidParameters)
initialize(game, config) initialize(game, config)
@ -89,4 +97,4 @@ open class AndroidLauncher : AndroidApplication() {
} }
} }
class AndroidTvLauncher:AndroidLauncher() class AndroidTvLauncher:AndroidLauncher()

View 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
}
}
}

View File

@ -27,6 +27,7 @@ import kotlin.concurrent.thread
class MainMenuScreen: CameraStageBaseScreen() { class MainMenuScreen: CameraStageBaseScreen() {
private val autosave = "Autosave" private val autosave = "Autosave"
private val backgroundTable = Table().apply { background=ImageGetter.getBackground(Color.WHITE) } private val backgroundTable = Table().apply { background=ImageGetter.getBackground(Color.WHITE) }
private val singleColumn = isCrampedPortrait()
private fun getTableBlock(text: String, icon: String, function: () -> Unit): Table { private fun getTableBlock(text: String, icon: String, function: () -> Unit): Table {
val table = Table().pad(15f, 30f, 15f, 30f) val table = Table().pad(15f, 30f, 15f, 30f)
@ -70,7 +71,8 @@ class MainMenuScreen: CameraStageBaseScreen() {
} }
val column1 = Table().apply { defaults().pad(10f) } 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) val autosaveGame = GameSaver.getSave(autosave, false)
if (autosaveGame.exists()) { if (autosaveGame.exists()) {
val resumeTable = getTableBlock("Resume","OtherIcons/Resume") { autoLoadGame() } val resumeTable = getTableBlock("Resume","OtherIcons/Resume") { autoLoadGame() }
@ -104,19 +106,21 @@ class MainMenuScreen: CameraStageBaseScreen() {
column2.add(modsTable).row() column2.add(modsTable).row()
val optionsTable = getTableBlock("Options", "OtherIcons/Options") {
val optionsTable = getTableBlock("Options", "OtherIcons/Options") this.openOptionsPopup()
{ OptionsPopup(this).open() } }
column2.add(optionsTable).row() column2.add(optionsTable).row()
val table=Table().apply { defaults().pad(10f) } val table=Table().apply { defaults().pad(10f) }
table.add(column1) table.add(column1)
table.add(column2) if (!singleColumn) table.add(column2)
table.pack() table.pack()
stage.addActor(table) val scrollPane = AutoScrollPane(table)
table.center(stage) scrollPane.setFillParent(true)
stage.addActor(scrollPane)
table.center(scrollPane)
onBackButtonClicked { onBackButtonClicked {
if(hasOpenPopups()) { if(hasOpenPopups()) {

View File

@ -34,6 +34,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
val fontImplementation = parameters.fontImplementation val fontImplementation = parameters.fontImplementation
val consoleMode = parameters.consoleMode val consoleMode = parameters.consoleMode
val customSaveLocationHelper = parameters.customSaveLocationHelper val customSaveLocationHelper = parameters.customSaveLocationHelper
val limitOrientationsHelper = parameters.limitOrientationsHelper
lateinit var gameInfo: GameInfo lateinit var gameInfo: GameInfo
fun isGameInfoInitialized() = this::gameInfo.isInitialized fun isGameInfoInitialized() = this::gameInfo.isInitialized

View File

@ -2,6 +2,7 @@ package com.unciv
import com.unciv.logic.CustomSaveLocationHelper import com.unciv.logic.CustomSaveLocationHelper
import com.unciv.ui.utils.CrashReportSender import com.unciv.ui.utils.CrashReportSender
import com.unciv.ui.utils.LimitOrientationsHelper
import com.unciv.ui.utils.NativeFontImplementation import com.unciv.ui.utils.NativeFontImplementation
class UncivGameParameters(val version: String, class UncivGameParameters(val version: String,
@ -9,5 +10,6 @@ class UncivGameParameters(val version: String,
val cancelDiscordEvent: (() -> Unit)? = null, val cancelDiscordEvent: (() -> Unit)? = null,
val fontImplementation: NativeFontImplementation? = null, val fontImplementation: NativeFontImplementation? = null,
val consoleMode: Boolean = false, val consoleMode: Boolean = false,
val customSaveLocationHelper: CustomSaveLocationHelper? = null) { val customSaveLocationHelper: CustomSaveLocationHelper? = null,
} val limitOrientationsHelper: LimitOrientationsHelper? = null
) { }

View File

@ -11,7 +11,7 @@ import kotlin.concurrent.thread
object GameSaver { object GameSaver {
private const val saveFilesFolder = "SaveFiles" private const val saveFilesFolder = "SaveFiles"
private const val multiplayerFilesFolder = "MultiplayerGames" private const val multiplayerFilesFolder = "MultiplayerGames"
private const val settingsFileName = "GameSettings.json" const val settingsFileName = "GameSettings.json"
@Volatile @Volatile
var customSaveLocationHelper: CustomSaveLocationHelper? = null var customSaveLocationHelper: CustomSaveLocationHelper? = null

View File

@ -44,6 +44,8 @@ class GameSettings {
var lastOverviewPage: String = "Cities" var lastOverviewPage: String = "Cities"
var allowAndroidPortrait = false // Opt-in to allow Unciv to follow a screen rotation to portrait
init { init {
// 26 = Android Oreo. Versions below may display permanent icon in notification bar. // 26 = Android Oreo. Versions below may display permanent icon in notification bar.
if (Gdx.app?.type == Application.ApplicationType.Android && Gdx.app.version < 26) { if (Gdx.app?.type == Application.ApplicationType.Android && Gdx.app.version < 26) {

View File

@ -13,9 +13,13 @@ import com.badlogic.gdx.scenes.scene2d.*
import com.badlogic.gdx.scenes.scene2d.ui.* import com.badlogic.gdx.scenes.scene2d.ui.*
import com.badlogic.gdx.scenes.scene2d.utils.Drawable import com.badlogic.gdx.scenes.scene2d.utils.Drawable
import com.badlogic.gdx.utils.viewport.ExtendViewport import com.badlogic.gdx.utils.viewport.ExtendViewport
import com.unciv.MainMenuScreen
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.models.Tutorial import com.unciv.models.Tutorial
import com.unciv.ui.tutorials.TutorialController 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 { open class CameraStageBaseScreen : Screen {
@ -28,10 +32,10 @@ open class CameraStageBaseScreen : Screen {
init { init {
val resolutions: List<Float> = game.settings.resolution.split("x").map { it.toInt().toFloat() } val resolutions: List<Float> = game.settings.resolution.split("x").map { it.toInt().toFloat() }
val width = resolutions[0]
val height = resolutions[1] 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( stage.addListener(
object : InputListener() { object : InputListener() {
@ -118,4 +122,31 @@ open class CameraStageBaseScreen : Screen {
return listener 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
}
}
}
} }

View 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)
}

View File

@ -237,7 +237,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Cam
keyPressDispatcher[Input.Keys.F11] = quickSave // Quick Save keyPressDispatcher[Input.Keys.F11] = quickSave // Quick Save
keyPressDispatcher[Input.Keys.F12] = quickLoad // Quick Load keyPressDispatcher[Input.Keys.F12] = quickLoad // Quick Load
keyPressDispatcher[Input.Keys.HOME] = { mapHolder.setCenterPosition(gameInfo.currentPlayerCiv.getCapital().location) } // Capital City View 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['\u0013'] = { game.setScreen(SaveGameScreen(gameInfo)) } // Ctrl-S: Save
keyPressDispatcher['\u000C'] = { game.setScreen(LoadGameScreen(this)) } // Ctrl-L: Load keyPressDispatcher['\u000C'] = { game.setScreen(LoadGameScreen(this)) } // Ctrl-L: Load
keyPressDispatcher['+'] = { this.mapHolder.zoomIn() } // '+' Zoom - Input.Keys.NUMPAD_ADD would need dispatcher patch keyPressDispatcher['+'] = { this.mapHolder.zoomIn() } // '+' Zoom - Input.Keys.NUMPAD_ADD would need dispatcher patch

View File

@ -45,6 +45,7 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
add(scrollPane).maxHeight(screen.stage.height * 0.6f).row() add(scrollPane).maxHeight(screen.stage.height * 0.6f).row()
addCloseButton { addCloseButton {
previousScreen.game.limitOrientationsHelper?.allowPortrait(settings.allowAndroidPortrait)
if (previousScreen is WorldScreen) if (previousScreen is WorldScreen)
previousScreen.enableNextTurnButtonAfterOptions() previousScreen.enableNextTurnButtonAfterOptions()
} }
@ -73,10 +74,11 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
if (previousScreen is WorldScreen) { if (previousScreen is WorldScreen) {
previousScreen.game.worldScreen = WorldScreen(previousScreen.gameInfo, previousScreen.viewingCiv) previousScreen.game.worldScreen = WorldScreen(previousScreen.gameInfo, previousScreen.viewingCiv)
previousScreen.game.setWorldScreen() previousScreen.game.setWorldScreen()
} else if (previousScreen is MainMenuScreen) { } else if (previousScreen is MainMenuScreen) {
previousScreen.game.setScreen(MainMenuScreen()) previousScreen.game.setScreen(MainMenuScreen())
} }
OptionsPopup(previousScreen.game.screen as CameraStageBaseScreen).open() (previousScreen.game.screen as CameraStageBaseScreen).openOptionsPopup()
} }
private fun rebuildOptionsTable() { private fun rebuildOptionsTable() {
@ -137,9 +139,17 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
settings.showExperimentalWorldWrap) settings.showExperimentalWorldWrap)
{ settings.showExperimentalWorldWrap = it } { 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() addSoundEffectsVolumeSlider()
addMusicVolumeSlider() addMusicVolumeSlider()
addTranslationGeneration() addTranslationGeneration()
addModCheckerPopup() addModCheckerPopup()
addSetUserId() addSetUserId()

View File

@ -35,7 +35,7 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
} }
addMenuButton("Victory status") { worldScreen.game.setScreen(VictoryScreen(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) } addMenuButton("Community") { WorldScreenCommunityPopup(worldScreen).open(force = true) }
addSquareButton(Constants.close) { addSquareButton(Constants.close) {
@ -80,4 +80,4 @@ class WorldScreenCommunityPopup(val worldScreen: WorldScreen) : Popup(worldScree
addCloseButton() addCloseButton()
} }
} }