Make the app always run in landscape mode and use insets

[-] Generate debug builds for only two arches
This commit is contained in:
biroder
2026-04-15 18:22:14 +00:00
parent 85d1dcdd87
commit 192df2280a
8 changed files with 96 additions and 21 deletions
+4
View File
@@ -64,6 +64,10 @@ android {
resValue("string", "app_name", "doukutsu-rs (debug)")
isJniDebuggable = true
ndk {
// ARM for testing on a physical device, and x86_64 for testing on the emulator
abiFilters.addAll(listOf("arm64-v8a", "x86_64"))
}
val applicationId = defaultConfig.applicationId!!
val documentsAuthorityValue = "$applicationId$applicationIdSuffix.documents"
@@ -43,6 +43,7 @@
android:configChanges="orientation|keyboardHidden|screenSize"
android:exported="true"
android:launchMode="standard"
android:resizeableActivity="false"
android:screenOrientation="sensorLandscape">
<meta-data
android:name="android.app.lib_name"
@@ -2,20 +2,21 @@ package io.github.doukutsu_rs;
import static android.os.Build.VERSION.SDK_INT;
import android.app.NativeActivity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.res.Configuration;
import android.hardware.SensorManager;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.view.OrientationEventListener;
import android.view.DisplayCutout;
import android.view.WindowInsets;
import android.widget.Toast;
import java.util.Arrays;
import java.io.File;
public class GameActivity extends org.libsdl.app.SDLActivity {
private int[] displayInsets = new int[]{0, 0, 0, 0};
@Override
protected String[] getLibraries() {
return new String[] {
@@ -24,6 +25,40 @@ public class GameActivity extends org.libsdl.app.SDLActivity {
};
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
this.updateCutouts();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
this.updateCutouts();
}
private void updateCutouts() {
Arrays.fill(this.displayInsets, 0);
WindowInsets insets = getWindow().getDecorView().getRootWindowInsets();
if (insets != null) {
this.displayInsets[0] = Math.max(this.displayInsets[0], insets.getStableInsetLeft());
this.displayInsets[1] = Math.max(this.displayInsets[1], insets.getStableInsetTop());
this.displayInsets[2] = Math.max(this.displayInsets[2], insets.getStableInsetRight());
this.displayInsets[3] = Math.max(this.displayInsets[3], insets.getStableInsetBottom());
}
if (SDK_INT >= Build.VERSION_CODES.P) {
DisplayCutout cutout = insets.getDisplayCutout();
if (cutout != null) {
this.displayInsets[0] = Math.max(this.displayInsets[0], cutout.getSafeInsetLeft());
this.displayInsets[1] = Math.max(this.displayInsets[0], cutout.getSafeInsetTop());
this.displayInsets[2] = Math.max(this.displayInsets[0], cutout.getSafeInsetRight());
this.displayInsets[3] = Math.max(this.displayInsets[0], cutout.getSafeInsetBottom());
}
}
}
public void openDir(String path) {
var uri = DocumentsContract.buildDocumentUri(BuildConfig.DOCUMENTS_AUTHORITY, path);
+1 -1
View File
@@ -168,7 +168,7 @@
"controller": {
"entry": "Controller...",
"keyboard": "Keyboard",
"touch_controls": "Touch controls"
"touch_controls_or_keyboard": "Touch controls/Keyboard"
},
"rebind": "Rebind...",
"rebind_menu": {
+1 -1
View File
@@ -167,7 +167,7 @@
"controller": {
"entry": "コントローラ",
"keyboard": "キーボード",
"touch_controls": "タッチコントロール"
"touch_controls_or_keyboard": "タッチコントロール/キーボードッチ操作/キーボード"
},
"rebind": "再バインド",
"rebind_menu": {
+7
View File
@@ -156,3 +156,10 @@ pub enum SpriteBatchCommand {
DrawRectTinted(Rect<f32>, Rect<f32>, Color),
DrawRectFlipTinted(Rect<f32>, Rect<f32>, bool, bool, Color),
}
pub fn get_scaled_size(width: u32, height: u32) -> (f32, f32) {
let scaled_height = ((height / 480).max(1) * 480) as f32;
let scaled_width = (width as f32 * (scaled_height as f32 / height as f32)).floor();
(scaled_width, scaled_height)
}
+3 -9
View File
@@ -15,7 +15,7 @@ use imgui::{DrawCmdParams, DrawData, DrawIdx, DrawVert};
use winit::window::Icon;
use crate::common::Rect;
use crate::framework::backend::{Backend, BackendEventLoop, BackendRenderer, BackendTexture, SpriteBatchCommand};
use crate::framework::backend::{Backend, BackendEventLoop, BackendRenderer, BackendTexture, SpriteBatchCommand, get_scaled_size};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
@@ -155,7 +155,8 @@ fn get_insets() -> GameResult<(f32, f32, f32, f32)> {
vm_env.delete_local_ref(JObject::from_raw(field));
//Game always runs with horizontal orientation so top and bottom cutouts not needed and only wastes piece of the screen
// The app always run in landscape mode, so the top and bottom cutouts are redundant
// and only waste part of the screen, causing UI elements to overlap.
elements[1] = 0;
elements[3] = 0;
@@ -163,13 +164,6 @@ fn get_insets() -> GameResult<(f32, f32, f32, f32)> {
}
}
fn get_scaled_size(width: u32, height: u32) -> (f32, f32) {
let scaled_height = ((height / 480).max(1) * 480) as f32;
let scaled_width = (width as f32 * (scaled_height as f32 / height as f32)).floor();
(scaled_width, scaled_height)
}
impl BackendEventLoop for GlutinEventLoop {
fn run(&mut self, game: &mut Game, ctx: &mut Context) {
let event_loop = EventLoop::new();
+40 -6
View File
@@ -28,7 +28,7 @@ use sdl2::{controller, keyboard, pixels, EventPump, GameControllerSubsystem, Sdl
use crate::common::{Color, Rect};
use crate::framework::backend::{
Backend, BackendEventLoop, BackendGamepad, BackendRenderer, BackendShader, BackendTexture, SpriteBatchCommand,
VertexData, WindowParams,
VertexData, WindowParams, get_scaled_size
};
use crate::framework::context::Context;
use crate::framework::error::{GameError, GameResult};
@@ -169,13 +169,15 @@ impl SDL2EventLoop {
if cfg!(target_os = "android") {
gl_attr.set_context_profile(GLProfile::GLES);
gl_attr.set_context_version(2, 0);
} else {
gl_attr.set_context_profile(GLProfile::Compatibility);
gl_attr.set_context_version(2, 1);
}
gl_attr.set_context_version(2, 1);
let mut win_builder = video.window("Cave Story (doukutsu-rs)", ctx.window.size_hint.0 as _, ctx.window.size_hint.1 as _);
win_builder.position_centered();
#[cfg(not(target_os = "android"))]
win_builder.resizable();
if ctx.window.mode.is_fullscreen() {
@@ -221,11 +223,31 @@ impl SDL2EventLoop {
}
}
fn get_scaled_size(width: u32, height: u32) -> (f32, f32) {
let scaled_height = ((height / 480).max(1) * 480) as f32;
let scaled_width = (width as f32 * (scaled_height as f32 / height as f32)).floor();
#[cfg(target_os = "android")]
fn get_insets() -> GameResult<(f32, f32, f32, f32)> {
unsafe {
use jni::objects::JObject;
use jni::JavaVM;
(scaled_width, scaled_height)
let vm_ptr = ndk_context::android_context().vm().cast();
let vm = JavaVM::from_raw(vm_ptr)?;
let vm_env = vm.attach_current_thread()?;
let class = vm_env.new_global_ref(JObject::from_raw(ndk_context::android_context().context().cast()))?;
let field = vm_env.get_field(class.as_obj(), "displayInsets", "[I")?.to_jni().l as jni::sys::jintArray;
let mut elements = [0; 4];
vm_env.get_int_array_region(field, 0, &mut elements)?;
vm_env.delete_local_ref(JObject::from_raw(field));
// The app always run in landscape mode, so the top and bottom cutouts are redundant
// and only waste part of the screen, causing UI elements to overlap.
elements[1] = 0;
elements[3] = 0;
Ok((elements[0] as f32, elements[1] as f32, elements[2] as f32, elements[3] as f32))
}
}
impl BackendEventLoop for SDL2EventLoop {
@@ -464,6 +486,18 @@ impl BackendEventLoop for SDL2EventLoop {
game.update(ctx).unwrap();
#[cfg(target_os = "android")]
{
match get_insets() {
Ok(insets) => {
ctx.screen_insets = insets;
}
Err(e) => {
log::error!("Failed to update insets: {}", e);
}
}
}
if let Some(_) = &state.next_scene {
game.scene = mem::take(&mut state.next_scene);
game.scene.as_mut().unwrap().init(state, ctx).unwrap();