Prerequisite knowledge
An intermediate understanding of developing Adobe AIR applications for mobile devices and programming in ActionScript 3 will help you make the most of this article.
Required products
Adobe AIR
User level

Whether you're adapting a game that runs in Flash Player in the browser to run on iOS (using Adobe AIR) or writing an Adobe AIR application for Android tablets, you'll need to support at least a few different screen resolutions. My post-apocalyptic strategy game Rebuild (for more background, read the Rebuild postmortem) was designed to be played in the browser with Flash Player at 800 x 600 pixels, so it took a few tricks to adapt it to run on the plethora of mobile devices out there.
In this article, I share some of those tricks (based on Adobe AIR 3.2) and sample code for:
  • Detecting screen size
  • Forcing orientation
  • Calculating physical dimensions
  • Scaling and centering an interface
  • Converting vectors to scaled bitmaps
  • Detecting and limiting Apple iOS devices
  • Limiting Android devices

Detecting screen size

There are four different ways to check dimensions.
The stage.width and stage.height properties indicate the width and height of the contents of the stage in pixels. If your SWF file is 500 x 500 pixels and contains a 100 x 100 pixel square, these properties will return 100 x 100.
You can use stage.stageWidth and stage.stageHeight to access the current width and height, in pixels, of the size of your SWF file in either Flash Player or Adobe AIR. In the above scenario, they would return 500 and 500 . They may be set incorrectly during the first few frames. In StageScaleMode.NO_SCALE , a resize event is triggered when these change.
The stage.fullScreenWidth and stage.fullScreenHeight properties return the width and height of the screen when going to full size, including space taken up by toolbars. These properties only change when width and height are swapped because of a change in orientation.
Lastly, the Capabilities.screenResolutionX and Capabilities.screenResolutionY properties return the maximum horizontal and vertical resolutions of the screen. In other words, they tell you the size of the device's entire screen. If your game is running in a window (for example, in a simulator on your computer), these properties will still return the size of the entire monitor.
Ideally, your application should check stage.stageWidth and stage.stageHeight , then listen for resize events (which includes changes in orientation). Depending on the device, these values may be completely wrong at first, and then after a few seconds you'll see several resize events as the application becomes fullscreen, toolbars disappear, and orientation corrects itself. Here's how to check resolution on the fly:
private static function handleResize(...ig) :void { var deviceSize:Rectangle = new Rectangle(0, 0, stage.stageWidth, stage.stageHeight); // adjust the gui to fit the new device resolution } stage.addEventListener(Event.RESIZE, handleResize); // call handleResize to initialize the first time handleResize();
For my game Rebuild, I wanted to get the dimensions a single time as soon as the game started, so I could initialize the GUI only once. The Stage.fullScreenWidth and Stage.fullScreenHeight properties provide a better approximation in this case, although they don't include any system toolbars that appear above your application (for example, those found on the Kindle Fire). The orientation may also be wrong, but because my game forces landscape orientation, I can assume that the wider dimension is always width, like this:
var deviceSize:Rectangle = new Rectangle(0, 0, Math.max(stage.fullScreenWidth, stage.fullScreenHeight), Math.min(stage.fullScreenWidth, stage.fullScreenHeight));

Forcing orientation

In your application.xml file, you can set the default aspectRatio to either landscape or portrait . For iOS this also determines which splash screen graphic you need— Default-Landscape.jpg or Default-Portrait.jpg . If you want to support both modes, set autoOrients to true , in which case a resize event will fire any time the user tilts the device to change orientation. Your application may briefly start in the wrong orientation even when autoOrients is set to false .
I use the following setup to force landscape orientation:
<initialWindow> <visible>true</visible> <fullScreen>true</fullScreen> <autoOrients>false</autoOrients> <aspectRatio>landscape</aspectRatio> <renderMode>gpu</renderMode> ... </initialWindow>
I change aspectRatio to force portrait orientation:
<initialWindow> <visible>true</visible> <fullScreen>true</fullScreen> <autoOrients>false</autoOrients> <aspectRatio>portrait</aspectRatio> <renderMode>gpu</renderMode> ... </initialWindow>
For games like Rebuild it's generally accepted to stick to one aspect ratio rather than switch back and forth, however most applications usually support both aspect ratios.

Calculating physical dimensions

Although a small phone and a large tablet could have the same number of pixels, they would have very different physical sizes. For example, when you touch an iPhone 4's retina 960 x 640 display, your finger covers four times more pixels than when you touch an iPad 1's 1024 x 768 screen. This is important to consider for legibility of fonts and icons, and particularly for the size of buttons.
Looking at the dots-per-inch (more properly called PPI or pixels-per-inch), you can estimate how big your interface should be to avoid fat-finger mistakes. Apple suggests the minimum size for buttons and interactive objects should be 44 x 44 pixels for a 163 dpi screen (for example, an iPhone 3GS).
Android uses the concept of density-independent pixels (dp), which is a normalized pixel unit for defining resolution compared to a 160 dpi screen. A device's dp is calculated as pixels * 160 / DPI. To determine the dpi, dp, and size in inches you can use the following code:
var dpi:Number = Capabilities.screenDPI; var dpWide:Number = stage.fullScreenWidth * 160 / dpi; var inchesWide:Number = stage.fullScreenWidth / dpi;
Unfortunately devices often report the wrong dpi (notably for iOS displays). Capabilities.screenDPI is actually a best guess based on other information, but it may also be wrong. For comparison, you can get the original dpi reported by the operating system as follows:
var serverString:String = unescape(Capabilities.serverString); var reportedDpi:Number = Number(serverString.split("&DP=", 2)[1]);
Rebuild's iOS build has two static interface layouts: one for iPad and another with larger text and buttons for iPhone and iPod Touch. You can use the current device's dpi and physical size to pick which layout to use.

Scaling and centering an interface

Adobe AIR has several built-in scaling modes, but I found that SHOW_ALL and EXACT_FIT behave oddly on devices. To have full control of scaling on devices (especially useful when resizing bitmaps), I found it's best to use NO_SCALE and handle scaling yourself.
Rebuild for Android uses a 1024 x 600 GUI that gets scaled up or down accordingly, then centered horizontally at the top of the screen. It's done when the game first loads, like this:
stage.scaleMode = StageScaleMode.NO_SCALE; stage.align = StageAlign.TOP_LEFT; var guiSize:Rectangle = new Rectangle(0, 0, 1024, 600); var deviceSize:Rectangle = new Rectangle(0, 0, Math.max(stage.fullScreenWidth, stage.fullScreenHeight), Math.min(stage.fullScreenWidth, stage.fullScreenHeight)); var appScale:Number = 1; var appSize:Rectangle = guiSize.clone(); var appLeftOffset:Number = 0; // if device is wider than GUI's aspect ratio, height determines scale if ((deviceSize.width/deviceSize.height) > (guiSize.width/guiSize.height)) { appScale = deviceSize.height / guiSize.height; appSize.width = deviceSize.width / appScale; appLeftOffset = Math.round((appSize.width - guiSize.width) / 2); } // if device is taller than GUI's aspect ratio, width determines scale else { appScale = deviceSize.width / guiSize.width; appSize.height = deviceSize.height / appScale; appLeftOffset = 0; } // scale the entire interface base.scale = appScale; // map stays at the top left and fills the whole screen = 0; // menus are centered horizontally base.menus.x = appLeftOffset; // crop some menus which are designed to run off the sides of the screen base.scrollRect = appSize;
The relative sizes and positions of objects inside the base stay the same regardless of scaling. For example, items in my scrolling inventory list are always spaced 50 pixels (px) apart. But you will need to adjust for scale any time you use MouseEvent.stageX , MouseEvent.stageY , or DisplayObject.localToGlobal .

Converting vectors to scaled bitmaps

The browser version of Rebuild uses vector objects for buildings, icons, buttons, and backgrounds. They look great when scaled and keep file sizes low, but that scaling can be too complex to render efficiently on devices. They also use filters that aren't supported in GPU rendering mode in Adobe AIR and have jagged edges when stage.quality is lowered to increase performance. My solution is to replace them with bitmaps created at the scale I calculated earlier. I do this once when the game first loads.
If you don't use vectors, you can also use the code below to scale down large bitmaps to the needed size so they look smoother and take up less memory. However, bitmaps that include text or icons may become illegible if scaled down too far. You may get better results by having two or more sets of pre-rendered bitmaps and selecting one of them based on screen dpi.
// the original object's size (won't include glow effects!) var objectBounds:Rectangle = object.getBounds(object); objectBounds.x *= object.scaleX; objectBounds.y *= object.scaleY; objectBounds.width *= object.scaleX; objectBounds.height *= object.scaleY; // the target bitmap size var scaledBounds:Rectangle = objectBounds.clone(); scaledBounds.x *= appScale; scaledBounds.y *= appScale; scaledBounds.width *= appScale; scaledBounds.height *= appScale; // scale and translate up-left to fit the entire object var matrix:Matrix = new Matrix(); matrix.scale(object.scaleX, object.scaleY); matrix.scale(appScale, appScale); matrix.translate(-scaledBounds.x, -scaledBounds.y); // briefly increase stage quality while creating bitmapData stage.quality = StageQuality.HIGH; var bitmapData:BitmapData = new BitmapData(scaledBounds.width, scaledBounds.height, true); bitmapData.draw(object, matrix); stage.quality = StageQuality.LOW; // line up bitmap with the original object and replace it var bitmap:Bitmap = new Bitmap(bitmapData); bitmap.x = objectBounds.x + object.x; bitmap.y = objectBounds.y + object.y; object.parent.addChildAt(bitmap, object.parent.getChildIndex(object)); object.parent.removeChild(object); // invert the scale of the bitmap so it fits within the original gui // this will be reversed when the entire application base is scaled bitmap.scaleX = bitmap.scaleY = (1 / appScale);
If you need two or more of the same Bitmap object, you should save and reuse a single BitmapData object because it takes very little memory to create a second Bitmap object from it. Remember to call Bitmap.bitmapData.dispose() when you're done with it, to immediately remove it from memory rather than waiting for it to be garbage collected.

Detecting and limiting Apple iOS devices

The iPhone, iPad, and iPod touch report their names in Capabilities.os , so it's possible to detect exactly which one the game is running on and adjust accordingly. For example, Rebuild has fewer animations and smaller maps on the slower iPad 1 and iPhone 3GS. The following code shows how to detect the iOS device and adjust the game accordingly:
public static function getDevice():String { var info:Array = Capabilities.os.split(" "); if (info[0] + " " + info[1] != "iPhone OS") { return UNKNOWN; } // ordered from specific (iPhone1,1) to general (iPhone) for each (var device:String in IOS_DEVICES) { if (info[3].indexOf(device) != -1) { return device; } } return UNKNOWN; } public static const IPHONE_1G:String = "iPhone1,1"; // first gen is 1,1 public static const IPHONE_3G:String = "iPhone1"; // second gen is 1,2 public static const IPHONE_3GS:String = "iPhone2"; // third gen is 2,1 public static const IPHONE_4:String = "iPhone3"; // normal:3,1 verizon:3,3 public static const IPHONE_4S:String = "iPhone4"; // 4S is 4,1 public static const IPHONE_5PLUS:String = "iPhone"; public static const TOUCH_1G:String = "iPod1,1"; public static const TOUCH_2G:String = "iPod2,1"; public static const TOUCH_3G:String = "iPod3,1"; public static const TOUCH_4G:String = "iPod4,1"; public static const TOUCH_5PLUS:String = "iPod"; public static const IPAD_1:String = "iPad1"; // iPad1 is 1,1 public static const IPAD_2:String = "iPad2"; // wifi:2,1 gsm:2,2 cdma:2,3 public static const IPAD_3:String = "iPad3"; // (guessing) public static const IPAD_4PLUS:String = "iPad"; public static const UNKNOWN:String = "unknown"; private static const IOS_DEVICES:Array = [IPHONE_1G, IPHONE_3G, IPHONE_3GS, IPHONE_4, IPHONE_4S, IPHONE_5PLUS, IPAD_1, IPAD_2, IPAD_3, IPAD_4PLUS, TOUCH_1G, TOUCH_2G, TOUCH_3G, TOUCH_4G, TOUCH_5PLUS];
Although I've listed all Apple mobile devices above, you'll never need to support the first or second generation iPhone and iPod Touch, because they lack the necessary architecture (armv7) and graphical libraries (opengles-2) to run AIR 3.2 applications.
There are a few ways to further limit your application to only run on a subset of compatible iOS devices.
You can set your application to only target iPads, or only target iPhone/iPod Touch via UIDeviceFamily in application.xml; for example:
<iPhone> <InfoAdditions><![CDATA[ <key>UIDeviceFamily</key> <array> <!-- iPhone support --> <string>1</string> <!-- uncomment for iPad support <string>2</string> --> </array> ... ]]></InfoAdditions> </iPhone>
You can get more specific by setting UIRequiredDeviceCapabilities in the same application.xml. For example requiring a still-camera means the device must have a camera. Requiring a gyroscope conveniently limits your application to the newer iPhone 4+, iPod Touch 4+, and iPad 2+. Wikipedia has a definitive list of iOS devices and their features.
To enable double-density displays in iPhone 4S+ (iPad 3 retina support isn't yet available in AIR 3.2), set requestedDisplayResolution to high . This changes the reported screen resolution in iPhones from 480 x 320 (163 dpi) to 960 x 640 (326 dpi). Make sure your application scales down appropriately for the iPhone 3GS and iPod Touch 3gen, which don't support retina display.
<iPhone> ... <!-- requestedDisplayResolution standard (the default), or high. --> <requestedDisplayResolution>high</requestedDisplayResolution> </iPhone>

Limiting Android devices

There is a large number of different Android devices that do not report their names via Capabilities.os , and there is no way to determine the CPU capabilities or RAM of the device. However, you can target specific Android devices (for example just tablets) in Google Play and other Android app stores by requiring a certain screen size and density in application.xml.
The screenSize attribute refers to how many inches wide the shorter size is:
  • small (approximately 2 to 3 inches)
  • normal (approximately 3 to 5 inches)
  • large (approximately 5 to 7 inches)
  • xlarge (approximately 7 inches or more)
The screenDensity attribute reflects the calculated pixels per inch:
  • ldpi (from 100 to 120 dpi)
  • mdpi (from 120 to 180 dpi)
  • tvdpi (approximately 213 dpi)
  • hdpi (from 180 to 260 dpi)
  • xhdpi (more than 260 dpi)
To check the latest values for screenSize and screenDensity, see Compatible screens element in the API guide for Android developers.  
The following excerpt from application.xml indicates that Rebuild does not support small or low-density screens:
<android> <manifestAdditions><![CDATA[ <manifest android:installLocation="preferExternal"> <supports-screens android:smallScreens="false" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true"/> <compatible-screens> <!-- list the screens you support here --> <screen android:screenSize="normal" android:screenDensity="hdpi" /> <screen android:screenSize="normal" android:screenDensity="xhdpi" /> <screen android:screenSize="large" android:screenDensity="ldpi" /> <screen android:screenSize="large" android:screenDensity="mdpi" /> <screen android:screenSize="large" android:screenDensity="hdpi" /> <screen android:screenSize="large" android:screenDensity="xhdpi" /> <screen android:screenSize="xlarge" android:screenDensity="ldpi" /> <screen android:screenSize="xlarge" android:screenDensity="mdpi" /> <screen android:screenSize="xlarge" android:screenDensity="hdpi" /> <screen android:screenSize="xlarge" android:screenDensity="xhdpi" /> </compatible-screens> ... </manifest> ]]></manifestAdditions> </android>
As of early 2012, the most popular Android devices are the Samsung Galaxy phone series (800 x 480), the Motorola Droids (960 x 540), the HTC Evo and Desire (800 x 480), the Samsung Galaxy Tab (1280 x 800), and the Kindle Fire (1024 x 600). However, every country has different popular models and no Android device has a definitive lead on the others, so targeting specific devices is not recommended.
I tested locally using a Nexus One (800x480 released in Jan 2010) which was the low-end of phones I wanted to support, and a Kindle Fire which is an average tablet. As frustrating as slow devices may be, it's better to develop using older hardware so you get a good idea of your minimum performance. Services like TestDroid can help you test remotely on a wider range of devices.

Where to go from here

I put off porting Rebuild to Android for months because I was afraid of handling so many devices, but in the end it was pretty simple to get the scaling in place and only took a few days to get going. I also ported to the BlackBerry PlayBook, which turned out to be well worth the relatively little effort it required.
Adobe AIR makes it easy to run the same code on a thousand different mobile devices. Adobe's done most of the hard work, so if you're planning to target one device, you might as well support all of them in all their different shapes and sizes.