Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change how is Retina handled on iOS #3709

Merged
merged 3 commits into from Oct 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGES
Expand Up @@ -12,7 +12,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;
}
}