Wednesday, April 7, 2010

All views are not created equal

Fun Android developer tip, custom views will not necessarily behave the same when added through XML instead of Java.

Summary
getResources().getDisplayMetrics().density will help with a majority of any scale issues.

Scenario
You created an Android game back in October 2008.  To this end, you used your old school AWT skills to create a custom view, an animation loop, and a touch event listener to get touches using XY coordinates.  Then you went back to your full time job because applications couldn't be sold and there was no advertising platform for Android, yet.

Fast forward to modern times.  You come back to this game and decide to try to add some advertising to support development of a new version of the game.  Whoa, there have been 6 new versions of Android released in the meantime and a lot of different phones are now on the market as well.  You build your game against 1.6 and 2.1 and test on your phones with 3 different size screens (QVGA, HVGA, WVGA -- you do have one of each, right?).  Everything works great.  There are a few details to clean up, but in general, cool beans.

You go to add an ad banner to your app and the example code all points to using XML layouts instead of setting everything up in your Activity.  Everything's set up to use XML layouts now et voila.  Something's wrong.  The ad banner shows up fine, and your game screen shows up fine but touch events are all screwy.  Let's say that a circle is drawn around where each touch happens.  On the HVGA phone, everything looks as it did before.  On the QVGA phone, the circles are way bigger than they should be.  On the WVGA phone, the circles are way smaller than they should be.  Given that everything worked fine when it was all Java, Android knows how to scale things for you, it just doesn't want to do it for you when you use an XML layout because Android is a good father.  Doing something for yourself even when it's already done for you is character building.

Solution
The issue is that you responded to XY touch events with the assumption that XY distance would always be XY distance.  This isn't true for different density displays.  X pixels on low density screens travels much further than the same X pixels on high density screens.  In XML, the general solution for dealing with this is adding dip to the end if things.  Density independent pixels...  mmm...  tasty...  Wait, the draw methods only have float arguments.  Where's my dip goodness?

final float scale = getResources().getDisplayMetrics().density;

Multiply all of your draw coordinates with this scale value and you'll be mostly done.

Bonus tip
If you have xml and xml-hdpi folders and everything works great on Android 1.6+, don't be surprised if things are borked on Android 1.5.  When the documentation says that unknown modifiers will be ignored, it doesn't mean you're all set and Cupcake will use xml and ignore xml-hdpi.  It means it will ignore the "hdpi" modifier and will consider the two folders equivalent.  It will just pick whichever resource it wants to use (read: it will usually choose xml-hdpi out of spite).  To fix this, rename xml-hdpi to xml-hdpi-v4.  "v4" means "use for API level 4 and higher".

Super bonus tip
If you're using values and values-land to store separate parameters to make a widget look nice in both portrait and landscape orientations, it'll work for Android 1.6+ not not for Android 1.5.  Fingers crossed that the 35-40% of Android users on 1.5 today don't have phones that change the home screen to landscape (Sprint, AT&T backflip, Europe, etc.).