Day 20: Scalable Vector Graphics

Introduction

we created a simple app with an animated sprite. If you tried this app on different screen resolutions, such as Ultra HD tablets or low-resolution phones, you would notice a quality issue. If the device’s smallest native dimension is less than Artenus’ standard 600px, there will be an unclean scale-down, and if it is much larger, you’ll find a blurry image. Why is that? It’s because our sprite uses bitmap graphics with a fixed resolution. Scaling such images always degrades performance to some extent even with the best scaling algorithms.

The solution to this problem is using responsive graphics. Android provides a mechanism called “resource qualifiers” to attack this issue. You can simply provide different resource folders corresponding to different screen resolutions (or other device characteristics), and include screen-specific graphics in them. There is a small problem with this approach. The main implication with this approach is APK size. If you include graphics for a varieties of resolutions, you will have lots of redundancy which adds to the size of your installation file. The other problem is that Android ecosystem accomodates potentially infinite screen sizes and resolutions, and you cannot accommodate all of them in your graphics. Even if you choose a representative set, you will find it hard to provide a consistent UI on all devices. Today we will discuss another approach.

Vector graphics

If you are a web developer or graphics designer, you most probably have already come across SVG or vector graphics. Unlike raster graphics (such as bitmaps), vectors are images that can be scaled to any size without losing their crispness and detail. The reason is that, instead of keeping pixels, they keep line and path information. Examples of applications that can read or work with vector graphics include Adobe Illustrator, Adobe Photoshop (very limited), and HTML5-based browsers.

A widely adopted format for vector graphics is SVG. The SVG specification is an open standard developed by the World Wide Web Consortium (W3C). It is an XML-based vector graphics for two-dimensional graphics. Below is the content for a sample SVG:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
	 width="100px" height="100px" viewBox="0 0 100 100">
	<circle fill="#76B54C" stroke="#000000" cx="50" cy="50" r="45"/>
</svg>

This simply draws a circle with a radius of 45. You can save this XML to a file and open it on your HTML5-enabled browser. To see the scalability of this image, below are two versions of the above image. On the left you can see it at its original size, and on the right you can see it scaled to 200%. You can see that the scaled circle is still as crisp.

SVG Enlarged SVG

Today we use SVG to create scalable graphics for our system.

Using SVG on Android

Android 5.0 (API level 21) supports SVG drawables. However, since Artenus targets API levels as low as 9, we cannot use this feature. On day 3, svg-android was introduced as a tool you need for this tutorial. You may download the library from here. In order to use it with Artenus, you need to place the jar file in the libs directory of the artenus module (create the directory if it doesn’t exist), and add the following line to the dependencies section of the build.gradle file in the same module:

compile files('libs/svg-android-1.1.jar')

Reading an SVG file from resource is pretty simple using this library:

final SVG svg = SVGParser.getSVGFromResource(res, resId);
final Picture pic = svg.getPicture();

/* pic can there be drawn on any bitmap using a Canvas: */
canvas.drawPicture(pic);

This little piece of code will be added to our Texture‘s load method. Open your Texture class and navigate to load. The first thing we need to check is whether or not we are working with an SVG or normal drawable. Artenus takes SVG graphics from the raw resource directory. Any texture in this directory is parsed as SVG, and any texture in the drawable directory is treated as raster graphics. This makes our job easier:

public final void load(Context context) {
	final Resources res = context.getResources();
	final boolean isSVG = res.getResourceTypeName(resId).equalsIgnoreCase("raw");
	Bitmap bmp;

	if(isSVG) {
		// TODO: Parse and render SVG int bmp
	} else {
		// Original code
		BitmapFactory.Options opts = new BitmapFactory.Options();
		opts.inScaled = false;
		bmp = BitmapFactory.decodeResource(context.getResources(), resourceId, opts);

		// Update this texture instance's width and height.
		width = bmp.getWidth();
		height = bmp.getHeight();
	}

	// The rest of original code
	...
}

The above code checks whether the resource identifier associated with the Texture corresponds to a raw resource. If it is, we parse the resource as SVG, and render it into a properly scaled bitmap for the current screen:

// Get scaling factor from Stage.
final float texScale = Stage.getTextureScalingFactor();

// Load the SVG file from the given resource.
final SVG svg = SVGParser.getSVGFromResource(res, resId);
final Picture pic = svg.getPicture();

// Physical width and height of the image, used for drawing
float scaledWidth = pic.getWidth() * texScale;
float scaledHeight = pic.getHeight() * texScale;

// Nominal width and height of the image, used for everything else
width = pic.getWidth();
height = pic.getHeight();

// Create a bitmap with the calculated physical size
final Bitmap.Config conf = Bitmap.Config.ARGB_8888;
final Bitmap tempBmp = Bitmap.createBitmap(Math.round(scaledWidth), Math.round(scaledHeight), conf);

// Create a canvas from the bitmap to draw the SVG
final Canvas canvas = new Canvas(tempBmp);

// Scale the SVG to match the physical size
canvas.scale(texScale, texScale);

// Draw the SVG on the bitmap
canvas.drawPicture(pic);

// Work is done here!
bmp = tempBmp;

One thing stands out in the code above, and that’s Stage.getTextureScalingFactor. We have not yet coded this method in Stage. This magic method returns the factor by which we should scale SVG graphics to match the screen resolution.

There is also a catch in the above code. We mentioned previously in this this tutorial that it is better to use power-of-two (POT) dimensions for textures in OpenGL ES. But when we scale images arbitrarily, we cannot be sure they’d end up so. We can remove this constraint from our SVG subsystem altogether, and handle it ourselves when creating the bitmap. Once we calculate physical width and height values, we extend them to the closest POT value:

// Physical width and height of the image, used for drawing
float scaledWidth = pic.getWidth() * texScale;
float scaledHeight = pic.getHeight() * texScale;

// Calculate the closest POT value larger than scaledWidth
scaledWidth *= (float) Math.pow(2, Math.ceil(Math.log(scaledWidth) / Math.log(2))) / scaledWidth;

// Calculate the closest POT value larger than scaledHeight
scaledHeight *= (float) Math.pow(2, Math.ceil(Math.log(scaledHeight) / Math.log(2))) / scaledHeight;

// Nominal width and height of the image, used for everything else
width = Math.round(scaledWidth / texScale);
height = Math.round(scaledHeight / texScale);

Calculating scaling factor

Now let’s get to the method we used in the previous section, but never implemented: Stage.getTextureScalingFactor. In order to implement this method, we assume that the original dimensions of the SVG file correspond to a screen with the smallest dimension equal to 600 px (width for portrait, and height for a landscape). The method then returns the scaling factor when the screen has a size bigger or smaller than tat.

Open the Stage class, and add this static field and method to it:

private static float texScale = 1.0f;

public static float getTextureScalingFactor() {
	return texScale;
}

The default scaling factor, as you can see, is 1. We should modify this value when we have the real width and height of the working area. That happens in onSurfaceChanged. Find the following lines in this method:

screenWidth = width;
screenHeight = height;

and add the following line of code just after them:

texScale = (float) Math.min(screenHeight, screenWidth) / 600.0f;

The code simply gets the smallest dimension, and computes its ratio to 600, which is exactly what we want.

Next steps

We can now use vector graphics in Artenus. If you are happy enough with the circle SVG provided here, you can download it here and use to play around with the library. If not, you can make your own graphics using an XML editor, or with a more advanced tool such as Adobe Illustrator.

If you use Illustrator, don’t use raster features such as Photoshop Filters, or advanced features such as non-linear gradients, as the svg-android library does not support them. Keep it simple! You can still make stunning graphics without those features.

You may download today’s code below: