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.

Sunday, March 24, 2013

Proposal for a Bill Splitter Programming Kata


You may have noticed that I mentioned several programming exercises, or katas, within this blog. I practiced these katas several times to improve my skills as a programmer and work on some of my weaknesses. Over the last week, I have been thinking about a potential project I could work on outside of my full time job. Instead, I came up with an idea for a new programming kata:

Bill Splitter Kata

  1. Part 1: A group of friends has a small issue. They eat out a lot together. However, splitting the bill at the end of the meal always takes a long time and, in the end, some folks end up overpaying, while others get away with not paying their fair share. The group has asked you to develop a small application that would allow them to split the bill fairly. One person will be appointed to use the application at the end of every meal to enter the bill's data and read off the results to the rest of the group.
    • A function header you can use:
      • []splitTheBill(foodCost, tax, tipPercent, []foodCostPerPerson)
    • Sample
      • input: splitTheBill(100, 10, 20, 30, 10, 45, 15)
      • output: 39 13 58.5 19.5
      • Note: the nth value in the output is the amount of money owed by the nth person in the input
  2. Part 2: the group really likes your progress in part 1, but finds the implementation lacking in features. A member suggests that the application should include lazy calculation. She suggests that for every person whose food cost is entered as 0, the remaining bill shall be split evenly. Implement this functionality.
    • input: splitTheBill(100, 10, 20, 0, 0, 45, 0)
    • output: 23.83 23.83 58.5 23.83
  3. Part 3: the group was using the application to split their bill at a restaurant. However, some of the members did not carry any change and did not want to use a credit card to pay for their food. Add an option for rounding each person's share to the nearest dollar, while still covering the bill.
    • header: []splitTheBill(foodCost, tax, tipPercent,  round, []foodCostPerPerson)
    • input: splitTheBill(100, 10, 20, true, 0, 0, 45, 0)
    • output: 29 29 58 29
    • Note: make sure that those who are paying for an equal amount of the bill end up paying the correct amount. i.e., 29 28 59 29 would not be a correct output for the sample input, even though the bill is fully covered

Feel free to use any programming language.

03/26/2013 Update:

The kata has been updated to be easier to read.

03/26/2013 Update:

I have completed the kata. Parts 1 and 2 took me under an hour total. Part 3 took me over two hours. I believe that each part can be handled in many different ways. I ended up writing a lot of helper functions for the third part. Test driven and test first development could be of big help in this situation. If you have tried the kata as well, please post your experiences in the comments section.

Saturday, February 2, 2013

Highcharts legend woes - tooltips and data labels

Please don't forget to view the update at the bottom of this post.

I was recently introduced to a Javascript graphing library called Highcharts. It's a wonderful tool for anyone who would like to embed a chart into a web page. However, it does not come without some quirks. For example, while there is a way to catch a click event on a legend entry, there is not an easy way to intercept a hover event. I ran into this problem while working on a column chart that would display data labels whenever a legend item is hovered over and keep them hidden otherwise. This issue was solved by using some jQuery trickery. A hover listener was added to the highcharts-legend-item class. The data labels option would be enabled for the every column, or "point", in the chart series whose legend item is being hovered over and hidden for all other series.


This led me to another problem: what if the user wanted to see a data label for a single column? Enabling tooltips seemed to be the right answer. Fortunately for many, but unfortunately for me, tooltips and data labels are handled separately by Highcharts. This means that their positions are controlled by separate functions and their styles are individually set. A quick search told me that I can define a positioner function for the tooltip. The function must return the position where the tooltip would be placed. By using some information about the column being hovered over, I was able to calculate the appropriate position for the placement of tooltip above the hovered column. This position would match the placement of the data labels that would appear when the legend items are hovered over.


Why not simply enable a single data label for a column hover event and disable the rest? Each Highchart has a single tooltip object. As the user moves the mouse pointer between different columns, the tooltip smoothly follows. Using data labels for this application would cause the tooltip to jump. Furthermore, due to the need to iterate through every data label and tell it whether it should be enabled or disabled causes performance issues that would cause the data label posing as a tooltip to show up rather slow, taking away from the smooth user experience of Highcharts.

For my work, please see this jsFiddle.

02/04/2013 Update:

Due to performance issues, I have decided to leave data labels behind and solve my problem using tooltips. Since Highcharts come with a single tooltip element, I ended up simply iterating over every data point in a series, forcing the tooltip to come up via the hover event, and copying the tooltip element to persist it. Surprisingly, the visible performance of this technique was much quicker than using data labels. Please check out the latest jsFiddle here. I am very happy with the outcome, as this method allows a single point of control for the styling and positioning of the tooltips/data labels.

06/18/2013 Update:

It appears that the code no longer functions as intended as of Highcharts version 3.0. I updated the jsFiddle to use Highcharts 2.3.5. See it here. I am sure the code can be modified to work with the latest version.