From 687b97c3a9d96f47018b4fddea4327fb85870584 Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Fri, 5 Sep 2025 16:31:48 -0400 Subject: [PATCH] v3.2.4: added support for 'medium' display cutouts like dynamic island --- .../com/watabou/utils/PlatformSupport.java | 4 +- .../android/AndroidPlatformSupport.java | 30 +++++++++++++ .../scenes/GameScene.java | 42 ++++++++++++++----- .../shatteredpixeldungeon/ui/StatusPane.java | 5 ++- 4 files changed, 68 insertions(+), 13 deletions(-) diff --git a/SPD-classes/src/main/java/com/watabou/utils/PlatformSupport.java b/SPD-classes/src/main/java/com/watabou/utils/PlatformSupport.java index 2d016643f..43fbb0ff8 100644 --- a/SPD-classes/src/main/java/com/watabou/utils/PlatformSupport.java +++ b/SPD-classes/src/main/java/com/watabou/utils/PlatformSupport.java @@ -53,9 +53,9 @@ public abstract class PlatformSupport { ); } - //returns a display cutout (if one is present) in device pixels, or null is none is present + //returns a display cutout (if one is present) in device pixels, or empty if none is present public RectF getDisplayCutout(){ - return null; + return new RectF(); } public abstract void updateSystemUI(); diff --git a/android/src/main/java/com/shatteredpixel/shatteredpixeldungeon/android/AndroidPlatformSupport.java b/android/src/main/java/com/shatteredpixel/shatteredpixeldungeon/android/AndroidPlatformSupport.java index 29942aac6..67c9e6964 100644 --- a/android/src/main/java/com/shatteredpixel/shatteredpixeldungeon/android/AndroidPlatformSupport.java +++ b/android/src/main/java/com/shatteredpixel/shatteredpixeldungeon/android/AndroidPlatformSupport.java @@ -66,6 +66,35 @@ public class AndroidPlatformSupport extends PlatformSupport { } } + @Override + public RectF getDisplayCutout() { + RectF cutoutRect = new RectF(); + + //some extra logic here is because cutouts can apparently be returned inverted + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + DisplayCutout cutout = AndroidLauncher.instance.getApplicationWindow().getDecorView().getRootWindowInsets().getDisplayCutout(); + + Rect largest = null; + if (cutout != null) { + for (Rect r : cutout.getBoundingRects()) { + if (largest == null + || Math.abs(r.height() * r.width()) > Math.abs(largest.height() * largest.width())) { + largest = r; + } + } + } + + if (largest != null){ + cutoutRect.left = Math.min(largest.left, largest.right); + cutoutRect.right = Math.max(largest.left, largest.right); + cutoutRect.top = Math.min(largest.top, largest.bottom); + cutoutRect.bottom = Math.max(largest.top, largest.bottom); + } + } + + return cutoutRect; + } + @Override public RectF getSafeInsets( int level ) { RectF insets = new RectF(); @@ -90,6 +119,7 @@ public class AndroidPlatformSupport extends PlatformSupport { boolean largeCutout = false; int screenSize = Game.width * Game.height; for (Rect r : cutout.getBoundingRects()){ + //use abs as some cutouts can apparently be returned inverted int cutoutSize = Math.abs(r.height() * r.width()); //display cutouts are considered large if they take up more than 0.605% of the screen //in reality we want less than about 0.5%, but some cutouts over-report their size diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/scenes/GameScene.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/scenes/GameScene.java index dff2db621..b88cc431e 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/scenes/GameScene.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/scenes/GameScene.java @@ -140,6 +140,7 @@ import com.watabou.noosa.audio.Sample; import com.watabou.noosa.particles.Emitter; import com.watabou.noosa.tweeners.Tweener; import com.watabou.utils.Callback; +import com.watabou.utils.DeviceCompat; import com.watabou.utils.GameMath; import com.watabou.utils.PlatformSupport; import com.watabou.utils.Point; @@ -234,7 +235,8 @@ public class GameScene extends PixelScene { } RectF insets = getCommonInsets(); - insets.top = Game.platform.getSafeInsets(PlatformSupport.INSET_LRG).scale(1f/defaultZoom).top; + //we want to check if large is the same as blocking here + float largeInsetTop = Game.platform.getSafeInsets(PlatformSupport.INSET_LRG).scale(1f/defaultZoom).top; scene = this; @@ -368,32 +370,52 @@ public class GameScene extends PixelScene { menu = new MenuPane(); menu.camera = uiCamera; - menu.setPos( uiCamera.width-MenuPane.WIDTH-insets.right, insets.top); + menu.setPos( uiCamera.width-MenuPane.WIDTH-insets.right, largeInsetTop); add(menu); - //TODO buff indicator and boss HP need to be moved down slightly on iOS devices with dynamic island - //TODO buff indicator probably cut off on some Android devices, get display cutout size and check? - //TODO need to reject inputs on the bars (just turn then into hotareas? //TODO make top bar transparent and add 1px of top status and menu bar to it? + //most cutouts supported by the game are small + // but some are more 'medium' can can be supported with a little UI offsetting + float mediumCutoutOffset = 0; + if (largeInsetTop != insets.top){ + //most notably iOS's Dynamic island, which must exist in this case + if (DeviceCompat.isiOS()){ + //TODO we should handle this logic in platformSupport, not hardcode it here + mediumCutoutOffset = 7; + } else if (DeviceCompat.isAndroid()) { + //some android hole punches can also be big too + RectF cutout = Game.platform.getDisplayCutout().scale(1f / defaultZoom); + //if the cutout is positioned to obstruct the buff bar + //TODO could buff bar just be squished in some cases here? + if (cutout.left < 80 + && cutout.top < 10 + && cutout.right > 32 + && cutout.bottom > 11) { + mediumCutoutOffset = (int) Math.floor(cutout.bottom - 11); + } + } + } + status = new StatusPane( SPDSettings.interfaceSize() > 0 ); status.camera = uiCamera; - status.setRect(insets.left, uiSize > 0 ? uiCamera.height-39-insets.bottom : insets.top, uiCamera.width - insets.left - insets.right, 0 ); + StatusPane.cutoutOffset = mediumCutoutOffset; + status.setRect(insets.left, uiSize > 0 ? uiCamera.height-39-insets.bottom : largeInsetTop, uiCamera.width - insets.left - insets.right, 0 ); add(status); - if (uiSize < 2 && insets.top > 0) { - SkinnedBlock bar = new SkinnedBlock(uiCamera.width, insets.top, TextureCache.createSolid(0xFF1C1E18)); + if (uiSize < 2 && largeInsetTop != 0) { + SkinnedBlock bar = new SkinnedBlock(uiCamera.width, largeInsetTop, TextureCache.createSolid(0xFF1C1E18)); bar.camera = uiCamera; add(bar); - PointerArea blocker = new PointerArea(0, 0, uiCamera.width, insets.top); + PointerArea blocker = new PointerArea(0, 0, uiCamera.width, largeInsetTop); blocker.camera = uiCamera; add(blocker); } boss = new BossHealthBar(); boss.camera = uiCamera; - boss.setPos( insets.left + 6 + (uiCamera.width - insets.left - insets.right - boss.width())/2, insets.top + 21); + boss.setPos( insets.left + 6 + (uiCamera.width - insets.left - insets.right - boss.width())/2, largeInsetTop + 21 + mediumCutoutOffset); add(boss); resume = new ResumeIndicator(); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/StatusPane.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/StatusPane.java index ef151ec3f..e7aebe87a 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/StatusPane.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/StatusPane.java @@ -78,6 +78,9 @@ public class StatusPane extends Component { private boolean large; + //lower the buff indicator to avoid larger cutouts (e.g. iPhone dynamic island) + public static float cutoutOffset; + public StatusPane( boolean large ){ super(); @@ -229,7 +232,7 @@ public class StatusPane extends Component { heroInfoOnBar.setRect(heroInfo.right(), y, 50, 9); - buffs.setRect( x + 31, y + 7, 50, 8 ); + buffs.setRect( x + 31, y + 7 + cutoutOffset, 50, 8 ); busy.x = x + 1; busy.y = y + 37;