Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swing and bitmaps on retina displays

I've got a Java desktop app that works, amongst other, on OS X.

Now the new MacBook Pro has a retina display and I'm concerned: how is it going to work regarding Swing?

What about when a Java app uses both Swing components and some bitmap graphics (like custom icons / ImageIcon)?

Shall all desktop Java apps be automatically resized (for example by quadrupling every pixel) or am I going to need to create two versions of my icons set (for example one with 24x24 icons and the other with 96x96 icons) and somehow determine that the app is running on a retina display?

like image 505
Cedric Martin Avatar asked Sep 14 '12 19:09

Cedric Martin


3 Answers

Use IconLoader library. It supports HiDPI images http://bulenkov.com/iconloader/ It also provides a way to work with HiDPI images (drawing, etc)

like image 73
Konstantin Bulenkov Avatar answered Sep 28 '22 09:09

Konstantin Bulenkov


On Apple's Java 6 you can provide multiple versions of the same image. Depending on the screen (retina or not), one or the other image is picked and drawn.

However, those images have to loaded in a special way:

Toolkit.getDefaultToolkit().getImage("NSImage://your_image_name_without_extension");

For example, if your (regular resolution) image is called: "scissor.png", you have to create a high resolution version "[email protected]" (following the Apple naming conventions) and place both images in the Resources directory of your app bundle (yes, you need to bundle your app). Then call:

Image img = Toolkit.getDefaultToolkit().getImage("NSImage://scissor");

You can use the resulting image in your buttons and it will be drawn with the right resolution magically.

There are two other "tricks" you can use:

  1. Using an AffineTransform of (0.5, 0.5) on your Graphics2D object before drawing an Image. Also see this java-dev message
  2. Creating a high dpi version of your image programmatically using this hack

The first "trick" (0.5 scaling) by now also works on Oracle's Java 7/8. I.e. if you draw an image with 0.5 scaling directly to the component's Graphics object, it will be rendered in high resolution on Retina displays (and also with half its original size).

Update

Starting with Java 9, there is better built-in support for images with different resolutions via the MultiResolutionImage interface. For more details, please see this answer.

like image 36
Hendrik Avatar answered Sep 28 '22 08:09

Hendrik


I can confirm that the scaling your images works with on Oracle Java 1.8. I cannot get the NSImage hack to work on java 1.7 or 1.8. I think this only works with Java 6 from Mac...

Unless someone else has a better solution, what I do is the following:

Create two sets of icons. If you have a 48pixel width icon create one 48px @normal DPI and another at 96px with 2x DPI. Rename the 2xDPI image as @2x.png to conform with apple naming standards.

Subclass ImageIcon and call it RetinaIcon or whatever. You can test for a Retina display as follows:

public static boolean isRetina() {

boolean isRetina = false;
GraphicsDevice graphicsDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();

try {
              Field field = graphicsDevice.getClass().getDeclaredField("scale");
        if (field != null) {
            field.setAccessible(true);
            Object scale = field.get(graphicsDevice);
            if(scale instanceof Integer && ((Integer) scale).intValue() == 2) {
                isRetina = true;
            }
        }
    } 
    catch (Exception e) {
        e.printStackTrace();
    }
    return isRetina;
}

Make sure to @Override the width and height of the new ImageIcon class as follows:

@Override
public int getIconWidth()
{
    if(isRetina())
    {
        return super.getIconWidth()/2;
    }
    return super.getIconWidth();
}

@Override
public int getIconHeight()
{
    if(isRetina())
    {
        return super.getIconHeight()/2;
    }
    return super.getIconHeight();
}

Once you have a test for the retina screen and your custom width/height methods overridden you can customise the painIcon method as follows:

@Override
public synchronized void paintIcon(Component c, Graphics g, int x, int y) 
{
    ImageObserver observer = getImageObserver();

    if (observer == null) 
    {
        observer = c;
    }

    Image image = getImage();
    int width = image.getWidth(observer);
    int height = image.getHeight(observer);
    final Graphics2D g2d = (Graphics2D)g.create(x, y, width, height);

    if(isRetina())
    {
        g2d.scale(0.5, 0.5);
    }
    else
    {

    }
    g2d.drawImage(image, 0, 0, observer);
    g2d.scale(1, 1);
    g2d.dispose();
}

I do not know how this will work with multiple screens though- is there anyone else that can help out with that???

Hope this code helps out anyway!

Jason Barraclough.

Here is an example of using the scaling as mentioned above: RetinaIcon is on the left. ImageIcon is on the right

like image 31
Jason Barraclough Avatar answered Sep 28 '22 09:09

Jason Barraclough