LibGDX: BestFitViewport

Today’s post is in english, because software development and code distribution know only one language: english.

While trying to solve this problem I wrote a new Viewport class (and solved said problem). You are encouraged to read that thread first.

Its name is BestFitViewport and it makes sense only on mobile devices in poorly designed apps that force the user to rotate the device between screens. I know, the right solution would be a good user interface design, but chances are you have no choice because someone else badly designed the app and they pay you to code that crap.

Assuming you end up in such a situation, this class may help you in that:
1. it offers cross platform screen orientation (Android and IOs only, desktop and web app developers DO NOT WANT TO USE THIS, unless they want to force users of a 23 inch. CRT display to physically rotate it now and then…). Please note that I tested this thing only on two Android devices (SGS GT I9000 and SGS GT I9505), however it does not make use of any esoteric features, only two common libgdx classes (Viewport and Camera), so I expect it to work more or less everywhere.
2. It’s way faster than setRequestedOrientation()
3. It works where setRequestedOrientation() was failing (SGS GT I9505), maybe my fault, but I couldn’t spot it

Enough bla bla bla, here is the code:

package org.sulweb.bestfitviewport;

/*
* Copyright © 2014 Lucio Crusca <lucio@sulweb.org>
* Based on LibGDX OrthographicCamera.java source code, nightly 2014-04-20.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/


import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;

public class BestFitCamera extends Camera
{
  public float zoom = 1, stretchFactorX = 1, stretchFactorY = 1;
  public boolean rotated;

  public BestFitCamera()
  {
    this.near = 0;
  }

  public BestFitCamera(float viewportWidth, float viewportHeight)
  {
    this.viewportWidth = viewportWidth;
    this.viewportHeight = viewportHeight;
    this.near = 0;
    update();
  }

  private final Vector3 tmp = new Vector3();

  @Override
  public void update()
  {
    update(true);
  }

  @Override
  public void update(boolean updateFrustum)
  {
    float left = zoom * stretchFactorX * -(viewportWidth / 2);
    float right = zoom * stretchFactorX * (viewportWidth / 2);
    float top = zoom * stretchFactorY * -(viewportHeight / 2);
    float bottom = zoom * stretchFactorY * viewportHeight / 2;
    if (rotated)
    {
      float tmp = left;
      left = top;
      top = tmp;
      tmp = right;
      right = bottom;
      bottom = tmp;
    }
    projection.setToOrtho(left, right, top, bottom, near, far);
    view.setToLookAt(position, tmp.set(position).add(direction), up);
    combined.set(projection);
    Matrix4.mul(combined.val, view.val);

    if (updateFrustum)
    {
      invProjectionView.set(combined);
      Matrix4.inv(invProjectionView.val);
      frustum.update(invProjectionView);
    }
  }

  /**
   * Sets this camera to an orthographic projection using a viewport fitting the screen resolution,
   * centered at (Gdx.graphics.getWidth()/2, Gdx.graphics.getHeight()/2), with the y-axis pointing
   * up or down.
   * 
   * @param yDown
   *          whether y should be pointing down
   */
  public void setToOrtho(boolean yDown)
  {
    setToOrtho(yDown, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
  }

  /**
   * Sets this camera to an orthographic projection, centered at (viewportWidth/2,
   * viewportHeight/2), with the y-axis pointing up or down.
   * 
   * @param yDown
   *          whether y should be pointing down.
   * @param viewportWidth
   * @param viewportHeight
   */
  public void setToOrtho(boolean yDown, float viewportWidth,
      float viewportHeight)
  {
    if (yDown)
    {
      up.set(0, -1, 0);
      direction.set(0, 0, 1);
    }
    else
    {
      up.set(0, 1, 0);
      direction.set(0, 0, -1);
    }
    position
        .set(zoom * viewportWidth / 2.0f, zoom * viewportHeight / 2.0f, 0);
    this.viewportWidth = viewportWidth;
    this.viewportHeight = viewportHeight;
    update();
  }

  /**
   * Rotates the camera by the given angle around the direction vector. The direction and up vector
   * will not be orthogonalized.
   * 
   * @param angle
   */
  public void rotate(float angle)
  {
    rotate(direction, angle);
  }

  /**
   * Moves the camera by the given amount on each axis.
   * 
   * @param x
   *          the displacement on the x-axis
   * @param y
   *          the displacement on the y-axis
   */
  public void translate(float x, float y)
  {
    translate(x, y, 0);
  }

  /**
   * Moves the camera by the given vector.
   * 
   * @param vec
   *          the displacement vector
   */
  public void translate(Vector2 vec)
  {
    translate(vec.x, vec.y, 0);
  }

}
package org.sulweb.bestfitviewport;

/*
* Copyright © 2014 Lucio Crusca <lucio@sulweb.org>
* Based on LibGDX FitViewport.java and ScalingViewport.java source code, nightly 2014-04-20.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Scaling;
import com.badlogic.gdx.utils.viewport.Viewport;

/**
 * This class behaves more or less like a FitViewport; it behaves different
 * when the world units orientation does not match the current display (or
 * window) orientation. This class may be useful only on mobile devices.
 * 
 * You may find this viewport useful only if all the following things hold
 * true for your mobile app:
 * 1. The app has at least one fixed portrait screen
 * 2. The app has at least one fixed landscape screen
 * 3. You agree that 1+2 above is bad user interface design, but your boss does not
 * want to change that
 * 4. You want portable code, i.e. you don't want to call setRequestedOrientation()
 *  because that's Android specific
 * 5. You want a fast orientation switch between screens
 * 6. You're fed up with Android Activity lifecycle and do not want to call setRequestedOrientation()
 * also to avoid a recreate()
 * 7. Your world units are "sane" enough, i.e. the long edge is actually longer, in numbers, 
 * than the short one. If this is not the case for you, it should be reasonably easy to patch
 * my code to support a 800x300 portrait world...
 * 8. Your Activity has a fixed orientation, e.g. the user cannot choose to rotate his device
 * and make the screen layout differently
 * 
 * How to use this class. You basically get the same as a FitViewport that fits the best 
 * fitting orientation, based on world coordinates you pass in and assuming world units
 * are squares.
 * 
 *   stage.setViewport(new BestFitViewport(1000,800));
 *   
 * such a viewport will force landscape orientation and fit the landscape display.
 * 
 *   stage.setViewport(new BestFitViewport(750,1030));
 *   
 * and this one will force portrait orientation and fit the portrait display.
 * 
 * @author Lucio Crusca
 *
 */
public class BestFitViewport extends Viewport
{
  private Scaling scaling;
  private boolean rotated;
  private int screen_width, screen_height;
  private float currentRotation;
  
  public BestFitViewport(float wwidth, float wheight)
  {
    super();
    this.scaling = Scaling.fit;
    this.worldWidth = wwidth;
    this.worldHeight = wheight;
    this.camera = new BestFitCamera();  
  }
  
  private void updateRotationState(int screenW, int screenH)
  {
    this.screen_width = screenW;
    this.screen_height = screenH;
    rotated = (worldWidth > worldHeight && screenW < screenH) || 
              (worldWidth < worldHeight && screenW > screenH);
  }

  @Override
  public void setWorldHeight(float worldHeight)
  {
    setWorldSize(getWorldWidth(), worldHeight);
  }

  @Override
  public void setWorldSize(float worldWidth, float worldHeight)
  {
    this.worldWidth = worldWidth;
    this.worldHeight = worldHeight;
    updateRotationState(screen_width, screen_height);
  }

  @Override
  public void setWorldWidth(float worldWidth)
  {
    setWorldSize(worldWidth, getWorldHeight());
  }

  @Override
  public void update(int screenWidth, int screenHeight, boolean centerCamera)
  {
    if (screenHeight != 0 && screenWidth != 0 &&
        (screenHeight != this.screen_height || screenWidth != this.screen_width)
       )
    {
      updateRotationState(screenWidth, screenHeight);
      Vector2 scaled;
      if (rotated)
      {
        scaled = scaling.apply(worldWidth, worldHeight, screenHeight, screenWidth);
        viewportWidth = Math.round(scaled.y);
        viewportHeight = Math.round(scaled.x);
      }
      else
      {
        scaled = scaling.apply(worldWidth, worldHeight, screenWidth, screenHeight);
        viewportWidth = Math.round(scaled.x);
        viewportHeight = Math.round(scaled.y);
      }
      
      // center the viewport in the middle of the screen
      viewportX = (screenWidth - viewportWidth) / 2;
      viewportY = (screenHeight - viewportHeight) / 2;

      Gdx.gl.glViewport(viewportY, viewportX, viewportWidth, viewportHeight);
      camera.viewportWidth = worldWidth;
      camera.viewportHeight = worldHeight;
      if (centerCamera) 
        camera.position.set(camera.viewportWidth / 2, camera.viewportHeight / 2, 0); 
  
      float previousRotation = currentRotation;
      currentRotation = rotated ? 270 : 0;
      camera.rotate(currentRotation - previousRotation, 0, 0, 1);
      if (rotated)
      {
        ((BestFitCamera)camera).stretchFactorX = (worldHeight / viewportWidth) / (worldWidth / viewportHeight);
        ((BestFitCamera)camera).stretchFactorY = 1 / ((BestFitCamera)camera).stretchFactorX; 
      }
      ((BestFitCamera)camera).rotated = rotated;      
    }
    camera.update(); 
  }
}

No jars provided, if you want to use it, just copy the code over into your project.

Comments

3 responses to “LibGDX: BestFitViewport”

  1. Andrea Avatar
    Andrea

    Ciao,

    volevo sapere se per caso c’e’ una versione di questo viewport che funzioni con l’ultima versione di libgdx la 1.9.2.
    Il codice e’ particolarmente interessante per fare funzionare la modalità landscape/portrait anche su Ios, visto che non dipende da implementazioni specifiche di Android.

    Ringrazio in anticipo per qualsiasi informazione o risposta.
    Andrea.

    1. lucrus Avatar
      lucrus

      Ciao, come già evidenziato nel post, la soluzione migliore è evitare di cambiare orientation. A volte non siamo noi a decidere ed in quei casi è sempre meglio provare a spiegare a chi decide che una app che costringe l’utente a ruotare il device mentre la usa è una schifezza e non a caso non se ne vedono molte in giro.
      Non ho una versione aggiornata di questa classe perché dopo la app in cui mi serviva, sono sempre riuscito a convincere chi di dovere a progettare le app in modo che non cambino orientation.
      Ad ogni modo, cosa è cambiato nella nuova versione di libgdx che rende questa classe incompatibile?

      1. Andrea Avatar
        Andrea

        Io in realtaà non voglio cambiare orientation a runtime ma impostarla una volta per tutte all’inizio. Ovviamente se uso gli xml di android su ios non funzionerebbero.

        Comunque il codice non compila e c’e’ da fare diverse modifiche ma non sono per niente sicuro che poi il codice funzioni. Hanno rivisto diverse cose con i viewport.

        Se vuoi ti posso mandare il codice che ho modificato io per farlo compilare. Ma ho agito solo ed esclusivamente per ottenere un codice compilabile. Il ragionamento che sta dietro al codice ancora non mi e’ perffettamente chiaro.

        Andrea.

Leave a Reply to lucrus Cancel reply

Your email address will not be published. Required fields are marked *