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:launchMode="singleTask"
android:label="@string/app_name"
android:screenOrientation="userLandscape"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
tools:ignore="LockedOrientationActivity">
<intent-filter>

View File

@ -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)

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() {
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()) {

View File

@ -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

View File

@ -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
) { }

View File

@ -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

View File

@ -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) {

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

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.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

View File

@ -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()

View File

@ -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) {