Skip to content

Commit

Permalink
Change how is Retina handled on iOS (#3709)
Browse files Browse the repository at this point in the history
* Change how retina resolutions are handled

This is a breaking change, but makes the API consistent with core.
Fixes #3789 (resize() being triggered with same dimensions)
Fixes touch mapping on older iOS versions when screen is rotated

* Update CHANGES

Co-authored-by: obigu <torinthechosen@gmail.com>
Co-authored-by: Benjamin Schulte <MrStahlfelge@users.noreply.github.com>
  • Loading branch information
3 people committed Oct 13, 2020
1 parent bfdf206 commit 858dc54
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 108 deletions.
3 changes: 2 additions & 1 deletion CHANGES
Expand Up @@ -13,7 +13,8 @@
- Gdx.files.external on Android now uses app external storage - see wiki article File handling for more information
- Improved text, cursor and selection rendering in TextArea.
- API Addition: Added setProgrammaticChangeEvents, updateVisualValue, round methods to ProgressBar/Slider.
- Keyboard events working on RoboVM on iOS 13.5 and up, uses same API as on other platforms
- iOS: Keyboard events working on RoboVM on iOS 13.5 and up, uses same API as on other platforms
- iOS: Changed how Retina/hdpi handled on iOS, see #3709
- API Addition: Added AndroidLiveWallpaper.notifyColorsChanged() to communicate visually significant colors back to the wallpaper engine.
- API Change: AssetManager invokes the loaded callback when an asset is unloaded from the load queue if the asset is already loaded.
- GWT: changed audio backend to WebAudio API. Now working on mobiles, pitch implemented. Configuration change: preferFlash removed. When updating existing projects, you can remove the soundmanager js files from your webapp folder and the references to it from index.html
Expand Down
Expand Up @@ -808,16 +808,16 @@ private static class NSArrayExtensions extends NSExtensions {
private void toTouchEvents (long touches) {
long array = NSSetExtensions.allObjects(touches);
int length = (int)NSArrayExtensions.count(array);
final IOSScreenBounds screenBounds = app.getScreenBounds();
for (int i = 0; i < length; i++) {
long touchHandle = NSArrayExtensions.objectAtIndex$(array, i);
UITouch touch = UI_TOUCH_WRAPPER.wrap(touchHandle);
final int locX, locY;
// Get and map the location to our drawing space
{
CGPoint loc = touch.getLocationInView(touch.getWindow());
final CGRect bounds = app.getCachedBounds();
locX = (int)(loc.getX() * app.displayScaleFactor - bounds.getMinX());
locY = (int)(loc.getY() * app.displayScaleFactor - bounds.getMinY());
CGPoint loc = touch.getLocationInView(app.graphics.view);
locX = (int)(loc.getX() - screenBounds.x);
locY = (int)(loc.getY() - screenBounds.y);
// app.debug("IOSInput","pos= "+loc+" bounds= "+bounds+" x= "+locX+" locY= "+locY);
}

Expand Down
Expand Up @@ -32,14 +32,12 @@
import org.robovm.apple.uikit.UIInterfaceOrientation;
import org.robovm.apple.uikit.UIPasteboard;
import org.robovm.apple.uikit.UIScreen;
import org.robovm.apple.uikit.UIUserInterfaceIdiom;
import org.robovm.apple.uikit.UIViewController;
import org.robovm.apple.uikit.UIWindow;
import org.robovm.rt.bro.Bro;

import com.badlogic.gdx.Application;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.ApplicationLogger;
import com.badlogic.gdx.Audio;
import com.badlogic.gdx.Files;
import com.badlogic.gdx.Gdx;
Expand Down Expand Up @@ -102,9 +100,9 @@ public void willTerminate (UIApplication application) {
ApplicationLogger applicationLogger;

/** The display scale factor (1.0f for normal; 2.0f to use retina coordinates/dimensions). */
float displayScaleFactor;
float pixelsPerPoint;

private CGRect lastScreenBounds = null;
private IOSScreenBounds lastScreenBounds = null;

Array<Runnable> runnables = new Array<Runnable>();
Array<Runnable> executedRunnables = new Array<Runnable>();
Expand All @@ -124,34 +122,15 @@ final boolean didFinishLaunching (UIApplication uiApp, UIApplicationLaunchOption
UIApplication.getSharedApplication().setIdleTimerDisabled(config.preventScreenDimming);

Gdx.app.debug("IOSApplication", "iOS version: " + UIDevice.getCurrentDevice().getSystemVersion());
// fix the scale factor if we have a retina device (NOTE: iOS screen sizes are in "points" not pixels by default!)

Gdx.app.debug("IOSApplication", "Running in " + (Bro.IS_64BIT ? "64-bit" : "32-bit") + " mode");

float scale = (float) UIScreen.getMainScreen().getNativeScale();
if (scale >= 2.0f) {
Gdx.app.debug("IOSApplication", "scale: " + scale);
if (UIDevice.getCurrentDevice().getUserInterfaceIdiom() == UIUserInterfaceIdiom.Pad) {
// it's an iPad!
displayScaleFactor = config.displayScaleLargeScreenIfRetina * scale;
} else {
// it's an iPod or iPhone
displayScaleFactor = config.displayScaleSmallScreenIfRetina * scale;
}
} else {
// no retina screen: no scaling!
if (UIDevice.getCurrentDevice().getUserInterfaceIdiom() == UIUserInterfaceIdiom.Pad) {
// it's an iPad!
displayScaleFactor = config.displayScaleLargeScreenIfNonRetina;
} else {
// it's an iPod or iPhone
displayScaleFactor = config.displayScaleSmallScreenIfNonRetina;
}
}
// iOS counts in "points" instead of pixels. Points are logical pixels
pixelsPerPoint = (float)UIScreen.getMainScreen().getNativeScale();
Gdx.app.debug("IOSApplication", "Pixels per point: " + pixelsPerPoint);

// setup libgdx
this.input = createInput();
this.graphics = createGraphics(scale);
this.graphics = createGraphics();
Gdx.gl = Gdx.gl20 = graphics.gl20;
Gdx.gl30 = graphics.gl30;
this.files = new IOSFiles();
Expand All @@ -177,8 +156,8 @@ protected IOSAudio createAudio (IOSApplicationConfiguration config) {
return new OALIOSAudio(config);
}

protected IOSGraphics createGraphics(float scale) {
return new IOSGraphics(scale, this, config, input, config.useGL30);
protected IOSGraphics createGraphics() {
return new IOSGraphics(this, config, input, config.useGL30);
}

protected IOSGraphics.IOSUIViewController createUIViewController (IOSGraphics graphics) {
Expand All @@ -201,11 +180,9 @@ public UIWindow getUIWindow () {
return uiWindow;
}

/** GL View spans whole screen, that is, even under the status bar. iOS can also rotate the screen, which is not handled
* consistently over iOS versions. This method returns, in pixels, rectangle in which libGDX draws.
*
* @return dimensions of space we draw to, adjusted for device orientation */
protected CGRect getBounds () {
/** @see IOSScreenBounds for detailed explanation
* @return logical dimensions of space we draw to, adjusted for device orientation */
protected IOSScreenBounds computeBounds () {
final CGRect screenBounds = UIScreen.getMainScreen().getBounds();
final CGRect statusBarFrame = uiApp.getStatusBarFrame();
final UIInterfaceOrientation statusBarOrientation = uiApp.getStatusBarOrientation();
Expand All @@ -220,34 +197,40 @@ protected CGRect getBounds () {
case LandscapeLeft:
case LandscapeRight:
if (screenHeight > screenWidth) {
debug("IOSApplication", "Switching reported width and height (w=" + screenWidth + " h=" + screenHeight + ")");
debug("IOSApplication", "Switching reported width and height (original was w=" + screenWidth + " h="
+ screenHeight + ")");
double tmp = screenHeight;
// noinspection SuspiciousNameCombination
screenHeight = screenWidth;
screenWidth = tmp;
}
}

// update width/height depending on display scaling selected
screenWidth *= displayScaleFactor;
screenHeight *= displayScaleFactor;

if (statusBarHeight != 0.0) {
debug("IOSApplication", "Status bar is visible (height = " + statusBarHeight + ")");
statusBarHeight *= displayScaleFactor;
screenHeight -= statusBarHeight;
} else {
debug("IOSApplication", "Status bar is not visible");
}
final int offsetX = 0;
final int offsetY = (int)Math.round(statusBarHeight);

final int width = (int)Math.round(screenWidth);
final int height = (int)Math.round(screenHeight);

final int backBufferWidth = (int)Math.round(screenWidth * pixelsPerPoint);
final int backBufferHeight = (int)Math.round(screenHeight * pixelsPerPoint);

debug("IOSApplication", "Total computed bounds are w=" + screenWidth + " h=" + screenHeight);
debug("IOSApplication", "Computed bounds are x=" + offsetX + " y=" + offsetY + " w=" + width + " h=" + height + " bbW= "
+ backBufferWidth + " bbH= " + backBufferHeight);

return lastScreenBounds = new CGRect(0.0, statusBarHeight, screenWidth, screenHeight);
return lastScreenBounds = new IOSScreenBounds(offsetX, offsetY, width, height, backBufferWidth, backBufferHeight);
}

protected CGRect getCachedBounds () {
/** @return area of screen in UIKit points on which libGDX draws, with 0,0 being upper left corner */
public IOSScreenBounds getScreenBounds () {
if (lastScreenBounds == null)
return getBounds();
return computeBounds();
else
return lastScreenBounds;
}
Expand Down
Expand Up @@ -47,39 +47,6 @@ public class IOSApplicationConfiguration {
/** number of frames per second, 60 is default **/
public int preferredFramesPerSecond = 60;

/** Scale factor to use on large screens with retina display, i.e. iPad 3+ (has no effect on non-retina screens).
* <ul>
* <li>1.0 = no scaling (everything is in pixels)
* <li>0.5 = LibGDX will behave as you would only have half the pixels. I.e. instead of 2048x1536 you will work in 1024x768.
* This looks pixel perfect and will save you the trouble to create bigger graphics for the retina display.
* <li>any other value: scales the screens according to your scale factor. A scale factor oof 0.75, 0.8, 1.2, 1.5 etc. works
* very well without any artifacts!
* </ul> */
public float displayScaleLargeScreenIfRetina = 1.0f;
/** Scale factor to use on small screens with retina display, i.e. iPhone 4+, iPod 4+ (has no effect on non-retina screens).
* <ul>
* <li>1.0 = no scaling (everything is in pixels)
* <li>0.5 = LibGDX will behave as you would only have half the pixels. I.e. instead of 960x640 you will work in 480x320. This
* looks pixel perfect and will save you the trouble to create bigger graphics for the retina display.
* <li>any other value: scales the screens according to your scale factor. A scale factor of 0.75, 0.8, 1.2, 1.5 etc. works
* very well without any artifacts!
* </ul> */
public float displayScaleSmallScreenIfRetina = 1.0f;
/** Scale factor to use on large screens without retina display, i.e. iPad 1+2 (has no effect on retina screens).
* <ul>
* <li>1.0 = no scaling (everything is in pixels)
* <li>any other value: scales the screens according to your scale factor. A scale factor of 0.75, 0.8, 1.2, 1.5 etc. works
* very well without any artifacts!
* </ul> */
public float displayScaleLargeScreenIfNonRetina = 1.0f;
/** Scale factor to use on small screens without retina display, i.e. iPhone 1-3, iPod 1-3 (has no effect on retina screens).
* <ul>
* <li>1.0 = no scaling (everything is in pixels)
* <li>any other value: scales the screens according to your scale factor. A scale factor of 0.75, 0.8, 1.2, 1.5 etc. works
* very well without any artifacts!
* </ul> */
public float displayScaleSmallScreenIfNonRetina = 1.0f;

/** whether to use the accelerometer, default true **/
public boolean useAccelerometer = true;
/** the update interval to poll the accelerometer with, in seconds **/
Expand Down
Expand Up @@ -65,7 +65,6 @@ public class IOSGraphics extends NSObject implements Graphics, GLKViewDelegate,
public static class IOSUIViewController extends GLKViewController {
final IOSApplication app;
final IOSGraphics graphics;
boolean created = false;

protected IOSUIViewController (IOSApplication app, IOSGraphics graphics) {
this.app = app;
Expand Down Expand Up @@ -124,14 +123,16 @@ public UIRectEdge getPreferredScreenEdgesDeferringSystemGestures() {
public void viewDidLayoutSubviews () {
super.viewDidLayoutSubviews();
// get the view size and update graphics
CGRect bounds = app.getBounds();
graphics.width = (int)bounds.getWidth();
graphics.height = (int)bounds.getHeight();
graphics.makeCurrent();
if (graphics.created) {
final IOSScreenBounds oldBounds = graphics.screenBounds;
final IOSScreenBounds newBounds = app.computeBounds();
graphics.screenBounds = newBounds;
// Layout may happen without bounds changing, don't trigger resize in that case
if (graphics.created && (newBounds.width != oldBounds.width || newBounds.height != oldBounds.height)) {
graphics.makeCurrent();
graphics.updateSafeInsets();
app.listener.resize(graphics.width, graphics.height);
app.listener.resize(newBounds.width, newBounds.height);
}

}

@Override
Expand Down Expand Up @@ -166,19 +167,11 @@ public void pressesEnded(NSSet<UIPress> presses, UIPressesEvent event) {
}
}

static class IOSUIView extends GLKView {

public IOSUIView (CGRect frame, EAGLContext context) {
super(frame, context);
}
}

IOSApplication app;
IOSInput input;
GL20 gl20;
GL30 gl30;
int width;
int height;
IOSScreenBounds screenBounds;
int safeInsetLeft, safeInsetTop, safeInsetBottom, safeInsetRight;
long lastFrameTime;
float deltaTime;
Expand All @@ -205,13 +198,11 @@ public IOSUIView (CGRect frame, EAGLContext context) {
GLKView view;
IOSUIViewController viewController;

public IOSGraphics (float scale, IOSApplication app, IOSApplicationConfiguration config, IOSInput input, boolean useGLES30) {
public IOSGraphics (IOSApplication app, IOSApplicationConfiguration config, IOSInput input, boolean useGLES30) {
this.config = config;

final CGRect bounds = app.getBounds();
// setup view and OpenGL
width = (int)bounds.getWidth();
height = (int)bounds.getHeight();
screenBounds = app.computeBounds();

if (useGLES30) {
context = new EAGLContext(EAGLRenderingAPI.OpenGLES3);
Expand All @@ -226,7 +217,7 @@ public IOSGraphics (float scale, IOSApplication app, IOSApplicationConfiguration
gl30 = null;
}

view = new GLKView(new CGRect(0, 0, bounds.getWidth(), bounds.getHeight()), context) {
view = new GLKView(new CGRect(0, 0, screenBounds.width, screenBounds.height), context) {
@Method(selector = "touchesBegan:withEvent:")
public void touchesBegan (@Pointer long touches, UIEvent event) {
IOSGraphics.this.input.onTouch(touches);
Expand Down Expand Up @@ -296,7 +287,7 @@ public void draw (CGRect rect) {
IOSDevice device = config.knownDevices.get(machineString);
if (device == null) app.error(tag, "Machine ID: " + machineString + " not found, please report to LibGDX");
int ppi = device != null ? device.ppi : 163;
density = device != null ? device.ppi/160f : scale;
density = device != null ? device.ppi/160f : app.pixelsPerPoint;
ppiX = ppi;
ppiY = ppi;
ppcX = ppiX / 2.54f;
Expand Down Expand Up @@ -346,6 +337,8 @@ public void draw (GLKView view, CGRect rect) {
gl20.glViewport(IOSGLES20.x, IOSGLES20.y, IOSGLES20.width, IOSGLES20.height);

if (!created) {
final int width = screenBounds.width;
final int height = screenBounds.height;
gl20.glViewport(0, 0, width, height);

String versionString = gl20.glGetString(GL20.GL_VERSION);
Expand Down Expand Up @@ -437,22 +430,27 @@ public void setGL30 (GL30 gl30) {

@Override
public int getWidth () {
return width;
return screenBounds.width;
}

@Override
public int getHeight () {
return height;
return screenBounds.height;
}

@Override
public int getBackBufferWidth() {
return width;
return screenBounds.backBufferWidth;
}

@Override
public int getBackBufferHeight() {
return height;
return screenBounds.backBufferHeight;
}

/** @return amount of pixels per point */
public float getBackBufferScale() {
return app.pixelsPerPoint;
}

@Override
Expand Down
@@ -0,0 +1,42 @@

package com.badlogic.gdx.backends.iosrobovm;

/** Represents the bounds inside GL view to which libGDX draws. These bounds may be same as view's dimensions, but may differ in
* some cases:
* <ul>
* <li>Status bar is visible - drawing area is not under the status bar</li>
* <li>Screen is rotated - in some iOS versions the rotation reporting behavior is different and this needs to be handled</li>
* </ul>
*
* <h3>IMPLEMENTATION & WARNING - Read carefully</h3> Accounting for status bar is not completely clean and relies on a
* coincidence, related to coordinate system origins. When status bar is present, x and y grows (in practice only y does) to
* offset the drawing area from the status bar and width and height (and their backBuffer values of course) shrink so the
* remaining surface fits the rest of the screen.
*
* When touch events arrive, IOSInput subtracts x and y from their coordinates to account for the shift.
*
* The unclean part is in the actual rendering - since the offset is essentially faked, there is no way to supply it to the libGDX
* application. But this does not become a problem, as long as Y and HEIGHT add up to the GL view's height (or X and WIDTH,
* although that is not used in practice), because GL's coordinate system (as far as the glViewport is concerned) starts in the
* LOWER left corner. So in practice, the rendering part of libGDX can be completely oblivious to any x/y offsets.
*
* This may become a problem when interfacing with UIKit, for example when placing banner ADs or using UIKit views over the
* libGDX's GL view. In such case, overriding {@link IOSApplication#computeBounds()} and providing custom, correct values is
* recommended. */
public final class IOSScreenBounds {
/** Offset from top left corner in points */
public final int x, y;
/** Dimensions of drawing surface in points */
public final int width, height;
/** Dimensions of drawing surface in pixels */
public final int backBufferWidth, backBufferHeight;

public IOSScreenBounds (int x, int y, int width, int height, int backBufferWidth, int backBufferHeight) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.backBufferWidth = backBufferWidth;
this.backBufferHeight = backBufferHeight;
}
}

0 comments on commit 858dc54

Please sign in to comment.