Thursday, November 14, 2013

A Different Look at Managing Android Image Resources


I have faced many challenges since I started working as an Application Engineer at YouEye, Inc. Most were involved with the development of software that targeted end users. Of course, it is sometimes necessary to step aside and create some internal tools that can aid the development process - making it quicker and more efficient by removing repetitive, excruciatingly boring tasks. Investing a day to save a week in the long run is always worth it, unless a deadline is just around the corner.

The latest tool that I worked on, easyassets, is a Ruby script that can be described with its long name: a SVG to PNG image asset converter Ruby script for major mobile device's screen sizes and densities. The purpose of the script is to allow designers to quickly generate image assets required by a mobile application in order to support all screen sizes and screen densities by simply providing a vector image on a canvas in the SVG format.

This post describes the thought process behind the development of the easyassets tool. If you would like to skip reading and get the tool, please see this GitHub repositoryYouEye, Inc. has released it to the public under the GNU General Public License, Version 3 (GPLv3).

The Android Developer website has a nice article that outlines how to support multiple screen sizes. A few sections discuss pixel density. A 100 by 100 pixel image will look larger and more "pixelated" on a screen with a small pixel density than on a screen with a large pixel density. Android offers a solution for supporting screens of multiple pixel densities - suffixes for resource drawable directories. These suffixes include -ldpi, -mdpi, -hdpi, -xhdpi, and -xxhdpi. There are a few others, but I will only speak about the ones listed. A 100 by 100 pixel image on an mdpi screen would have to be 75 by 75 pixels in order to appear the same size on a ldpi screen, 150 by 150 pixels in order to appear the same size on a hdpi screen, 200 by 200 pixels on a xhdpi, and 300 by 300 pixels on a xxhdpi. This can be explained by the 3:4:6:8:12 ratio for ldpi:mdpi:hdpi:xhdpi:xxhdpi.

Why can't we just create multiple sizes of an image to support all screen densities? Well, we actually can! However, there are two problems:

  1. It is sometimes tedious to manually create multiple sizes of the same image.
  2. Screen density is not the only screen parameter that we need to be concerned about.

Let's talk about the second point. What if we want to create a splash screen with a large logo for our application? A 200 by 200 pixel image works great for a phone with an mdpi screen. That means it will look great on a tablet with an mdpi screen too, right? Well, the image will look great, but it will not fill an equal area of the screen as it does on the phone.

The above point makes it clear that taking the physical size of a screen into account is important when thinking about image resources. Android makes this possible with additional suffixes for resource drawables: -sw320dp, -sw360dp, -sw480dp, -sw600dp, -sw720dp, and others. In fact, any number can go between "sw" and "dp". The letters "sw" mean "smallest width" while the letters "dp" mean density-independent pixels. A density-independent pixel will always be the same physical size no matter the density of the screen, e.g. 13 dp on a ldpi screen is the same physical size as 13 dp on a xxhdpi screen. "sw360dp" means that the resources in this directory will be supported by devices with a screen that is at least 360 density-independent pixels wide. Larger, newer generation phones generally fall into this category. Resources in the "sw480dp" directory can be used by the largest phones, such as the Samsung Galaxy Note. "sw600dp" is for 7-inch tablets. "sw720dp" is for 10-inch tablets. I picked this particular set of "smallest-width" constraints in order to support Android devices of pretty much all sizes. The ratios for 320:360:480:600:720 can be simplified down to 8:9:12:15:18.

Android allows us to have resource directories with names such as "drawable-sw720dp-xxhdpi". That's right - we can refer to density and size at the same time! Let's combine the ratios for density and screen size. We will choose "1" for the value of a sw320dp-mdpi screen. It will serve as our point of reference.

sw320dp (8)sw360dp (9)sw480dp (12)sw600dp (15)sw720dp (18)
ldpi (3)0.750.843751.1251.406251.6875
mdpi (4)11.1251.51.8752.25
hdpi (6)1.51.68752.252.81253.375
xhdpi (8)22.2533.754.5
xxhdpi (12)33.3754.55.6256.75

Awesome! We now have a way to know how to make an image take up an equal amount of space on a small screen and a large screen across multiple screen pixel densities. Let's go through a simple example. We have a 200 by 200 pixel logo that looks great on a mdpi phone with a small screen. How do we make the same logo take up a relatively same amount of space on a 10-inch tablet with hdpi pixel density? Let's find it in the table - row hdpi and column sw720dp. The value is 3.375. Thus, the logo should be around 200 x 3.375 by 200 x 3.375, or 675 by 675 pixels.

But wait, you might say, this seems to be making things more complicated! We went from having to generate image resources for just various pixel densities to also having to support multiple screen sizes. Now we have to make 25 images! Who would want to spend the tedious hours doing this? No one. And no one has to, if a certain type of image is used - a vector image.

Vector images are great, because they can be resized to any size without quality loss. We can take a vector image that is originally 100 by 100 pixels in size, make it 10,000 by 10,000 pixels, and it'll look just as smooth and not pixelated. I reckoned that if we start from a vector image, we can use a few tools to generate PNG images in all of the sizes we need.

This is how easyassets was born. It uses three libraries - librsvg, imagemagick, and pngquant to convert SVG images to PNG images in multiple sizes, and then trim unneeded blank space around the image. The script uses a hash map version of the above table to generate image resources and automatically place them into appropriate drawable directories. All you have to do is provide the SVG originals in a size that is expected to be used on a sw320dp-mdpi screen, run the tool, and you will have and you will have a set of directories with the PNGs you need. These directories can be pasted into the "res" directory of your project. Please see easyassets' GitHub page for installation and usage instructions.

I recommend starting with a 320 by 480 pixel canvas for each vector image, and drawing vectors in a place where you would like them to appear on the screen. Providing this SVG to the script will give you the perfect sized PNGs for all the screens you would like to support. Of course, there is a downside - having this many images will increase the size of your APK file by quite a bit. My application has 7 images and its file size went up from 3.6 megabytes to 9.6 when I switched from 5 sizes for each image to 25. However, this may not be a too-heavy price to pay for having a perfect image for every screen.

Hopefully you can find this useful! Please feel free to ask questions or make comments about my approach and the easyassets tool either on this post or on the GitHub page.