Loading Adobe AIR Mobile XHDPI Assets

Posted on August 5, 2013
Share article:

With the release of Samsung Galaxy S4, Flex seems to be stuck in the past. The runtimeDPI property only goes as high as 320 dpi, and S4 has about 440. Loading regular 320 dpi assets onto a full HD screen makes for a microscopic user experience.
So how do we go around this irritating limitation? Simple! Code your own Adobe AIR Mobile XHDPI assets framework.
What we want:

  1. Load dpi specific assets
  2. Detect above-standard dpi-s
  3. Detect tablets
  4. Cache assets for fast runtime loading
  5. Load CSS for XHDPI resolutions

Let’s see how we can achieve all that.
First, define a Constants.as a class to hold specific values. Below is my implementation:

public class Constants {	
        public static const LDPI:String= "ldpi";
        public static const MDPI:String= "mdpi";
        public static const HDPI:String= "hdpi";
        public static const XHDPI:String= "xhdpi";

        protected static var _dpi:String = MDPI; // medium as default 
        protected static var _assetDir:String =""; 
        protected static var _isLandscape:Boolean = false;
        protected static var _isTablet:Boolean = false;

        protected static var _portraitOrientation:String = StageOrientation.DEFAULT; 
        protected static var _landScapeOrientation:String = StageOrientation.ROTATED_RIGHT;

        public static function get DPI():String {
            return _dpi;
        }		
        public function setDpi(value:String):void {
            _dpi = value;
        }		
        public static function get ASSET_DIR():String {
            return _assetDir;
        }		
        public function setAssetDir(value:String):void {
            _assetDir = value;
        }		
        public static function get isLandscape():Boolean {
            return _isLandscape;
        }		
        public function setIsLandscape(value:Boolean):void {
            _isLandscape = value;
        }		
        public static function get isTablet():Boolean {
            return _isTablet;
        }		
        public function setIsTablet(value:Boolean):void {
            _isTablet = value;
        }		
        public static function get portraitOrientation():String {
            return _portraitOrientation;
        }		
        public function setPortraitOrientation(value:String):void {
            _portraitOrientation = value;
        }		
        public static function get landScapeOrientation():String {
            return _landScapeOrientation;
        }		
        public function setLandScapeOrientation(value:String):void {
            _landScapeOrientation = value;
        }		
    }

You might notice that the setters are not static. The reason I did that is because I don’t want to accidentally modify my constants.
The only place I want those to be set is on initialization. Anywhere else in the app, I call the static getters.

Next, we need a class to initialize all these constants. Let’s call that one Styles.as :

import flash.display.Stage;
    import flash.display.StageOrientation;
    import flash.system.Capabilities;

    import mx.styles.IStyleManager2;
    import mx.styles.StyleManager;

    import ro.stancalau.dpi.util.ImageCache;

    public class Styles {
        public function Styles() {}

        public static function loadStyles(styleManager:IStyleManager2 , applicationDPI:int, stage:Stage):void {
            var cons:Constants = new Constants();

            switch (applicationDPI) {
                case 160: { 
                    cons.setAssetDir("../../assets/images/ldpi/"); 
                    cons.setDpi(Constants.LDPI);
                    break; 
                } 
                case 240: { 
                    cons.setAssetDir("../../assets/images/mdpi/");  
                    cons.setDpi(Constants.MDPI);
                    break;
                }
                case 320: { 
                    cons.setAssetDir("../../assets/images/hdpi/");  
                    cons.setDpi(Constants.HDPI);
                    break;
                }                
            }

            //check for huge resolutions
            if (Math.min(Capabilities.screenResolutionX, Capabilities.screenResolutionY) >900 
                && Capabilities.os.toLowerCase().indexOf('windows')<0 ) {

                cons.setAssetDir("../../assets/images/xhdpi/"); 
                cons.setDpi(Constants.XHDPI);
            }			

            //check if device has a landscape screen (usually qwerty devices and tablets) 
            if (stage.width > stage.height)	cons.setIsLandscape(true);

            //check if tablet
            if (Math.min(Capabilities.screenResolutionX, Capabilities.screenResolutionY) >600 
                && Capabilities.os.toLowerCase().indexOf('windows')<0 
                && Constants.isLandscape ) {

                cons.setAssetDir("../../assets/images/mdpi/"); 
                cons.setDpi(Constants.MDPI);
                cons.setIsTablet(true);
                cons.setPortraitOrientation(StageOrientation.ROTATED_LEFT);
                cons.setLandScapeOrientation(StageOrientation.DEFAULT); 
            }

            loadCSS(styleManager);
            preCache();
        }

        private static function loadCSS(styleManager:IStyleManager2):void {
            switch(Constants.DPI) {
                case Constants.XHDPI: {
                    styleManager.loadStyleDeclarations("../assets/css/styleXHDPI.swf");
                    break;
                }
                default: {
                    styleManager.loadStyleDeclarations("../assets/css/style.swf");
                    break;
                }
            }
        }

        private static function preCache():void {
            new ImageCache().init();
            ImageCache.cache.load(Constants.ASSET_DIR+'tree.png');
        }
    }

Notice how we also set different CSS swfs for regular DPIs and for XHDPI. To generate a swf from a .css file, simply right click on the file in Flash Builder and mark “Compile CSS to SWF”. It will automatically generate the swf when compiling.
Another thing to notice is the preCache() function. It’s used to preload all the visual assets. If this is not done, the interface will flicker the first time an asset is displayed on the screen (even though it is a local asset!).

The implementation for the ImageCache.as file is as follows:

public final class ImageCache {
    [Bindable]
    public static var cache:ContentCache;

    public function ImageCache() {
        if (cache != null) return;
        cache = new ContentCache();
    }

    public function init():void {
        cache.enableCaching = true;
        cache.enableQueueing = true;
        cache.maxActiveRequests = 5;
        cache.maxCacheEntries = 200;
    }
}

You will need the static cache property to be bindable for use in mxml later on.

With the basic framework set up, we need to fire it up on start-up.
My main application mxml looks like this:

<?xml version="1.0" encoding="utf-8"?>
<s:ViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009" 
                            xmlns:s="library://ns.adobe.com/flex/spark" 
                            firstView="ro.stancalau.dpi.views.BlankView"
                            addedToStage="onAddedToStage(event)"
                            >
    <fx:Script>
        <![CDATA[
            import mx.events.FlexEvent;

            import ro.stancalau.dpi.config.Constants;
            import ro.stancalau.dpi.config.Styles;
            import ro.stancalau.dpi.views.DpiFrameworkHomeView;

            protected function onAddedToStage(event:Event):void
            {
                Styles.loadStyles(styleManager, applicationDPI, stage);	
                stage.setOrientation(Constants.portraitOrientation);	
                navigator.replaceView(DpiFrameworkHomeView);
            }

        ]]>
    </fx:Script>

</s:ViewNavigatorApplication>

Finally, create a view. The next piece of mxml code loads a tree image in the middle of our app and writes some information on top to signal what was loaded.

<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:s="library://ns.adobe.com/flex/spark" title="HomeView">
    <fx:Script>
        <![CDATA[
            import ro.stancalau.dpi.config.Constants;
            import ro.stancalau.dpi.util.ImageCache;
        ]]>
    </fx:Script>
    <s:Image id="tree" horizontalCenter="0" verticalCenter="0" contentLoader="{ImageCache.cache}"
             source="{Constants.ASSET_DIR+'tree.png'}"/>

    <s:Label top="5" horizontalCenter="0" 
             styleName="treeStyle"
             text="{'ASSET_DIR: '+Constants.ASSET_DIR +'\n' + 'IMG RES: '+tree.width+'x'+tree.height+'px'}" 
    />

</s:View>

Note how we bind our cache to the image’s contentLoader property to specify we already have a set of assets preloaded, among which the target image might also be.

Previously, two swfs generated from CSS files were loaded. They both should have the same properties and classes set. The difference
is that the default style will need metadata to help Flex figure out the style differences between 160dpi, 240dpi and 320dpi. Bellow are both our style sheets.

style.css:

@namespace s "library://ns.adobe.com/flex/spark";
@namespace views "ro.stancalau.dpi.views.*";
.treeStyle{
    color: #119911;
}
@media (application-dpi: 160)  { 
    .treeStyle{
        fontSize: 12px;
    }		
}
@media (application-dpi: 240)  {
    .treeStyle{
        fontSize: 18px;
    }	
}
@media (application-dpi: 320)  {
    .treeStyle{
        fontSize: 24px;
    }	
}

styleXHDPI.css:

@namespace s "library://ns.adobe.com/flex/spark";
@namespace views "ro.stancalau.dpi.views.*";
.treeStyle {
    color: #22FF22;
    fontSize: 32px;
}

One more thing to go over before letting this one rest, are the isLandscape, isTablet, portraitOrientation and landScapeOrientation properties from Constants.as.
These will help when targeting a heterogeneous device array by telling you exactly what you’re dealing with. If these properties don’t seem quite right for your application, you can always tweak the rules in Styles.as to better suit your needs.

Note: These orientation variables only works if <autoOrients> is set to false and <aspectRatio> is not set in the -app.xml file.
What I find most useful about those settings is portraitOrientation and landScapeOrientation, when I have to display the device camera image on the screen and then reverting to the regular menu orientation.

That’s all there is to this AIR Assets for XHDPI tutorial.

Source Download

Share article:

Start the discussion on "Loading Adobe AIR Mobile XHDPI Assets"


    What do you think? Share your thoughts!

    7 + 3 =