Exploring runtime styling with the Native Window Explorer in Adobe AIR

by Jusitn Imhoff 22

In this Adobe AIR tutorial you will use runtime styling to create a dynamic native window in Adobe AIR.

We will show you how to load and unload styles, performance concerns and best practice while also showing you the value of knowing your environment.

Requirements

  • Flex SDK
  • AppWindow class found in the source code
  • WIN & MAC SWF skins found in the source code

View DemoDownload Source Files

Pre-Requesites

You should feel comfortable working with Adobe Flex Builder and Adobe AIR.
When creating an AIR application, I suggest using ADM from David Deraedt – makes managing your application settings file too easy.

What is the Native Window Explorer?

To give a quick definition, it is a project to create the native look and feel of the hosting operating system in AIR. Leveraging a native UX experience in AIR is the next step in making truly enterprise level applications that are built on top of current user expectations. We want to go beyond that utility window and really be in control of all aspects of the application, including layout and design that is un-inhibited by system chrome restrictions.

Step 1: Create the Project

The first thing we want to do is create a new Adobe AIR application from Flex Builder named “Native Window Explorer”. After creating the project, add the AppWindow class to the source (src) folder and add the WIN & MAC skins to a new folder called skins. Your project structure should be similar to below:

Step 2: Creating the default application skin

To create the default application style in Flex Builder, select File > New > CSS File. Name the file “Default.css” and save it to the skins directory. The Default.css file will act as our global skin contains the type selectors the application will use. Since we want this to be the default style for the application, we will compile the style in the main SWF file by using:

Any value you define in the Default.css file should consist of global styles that are not overwritten by other styles. The goal of the Default.css file is to handle the global skinning that is specifically for your application. If you are not leveraging Flex’s default Halo theme, you can create your own theme that defines type selectors for such components as Buttons or Lists. In this tutorial I am simply setting the global text, and leveraging Halo for the datagrid. Instead of loading this file remotely, we will embed it in the main SWF. I would suggest using this file for setting type selectors (Button, HSlider), compared to class selectors (.newButton, .coolSlider) witch is specific and would not be globally applied. The class selectors should be loaded in remotely if it will be defined and used in multiple style SWFs, or just to keep initial download size down and promote a fast user experience.

Our goal in implementing runtime styling is first identifying a reason. In this case we are going to load different styles / skins for the current host operating system. Other reasons for dynamic skinning could be for providing different experience per use case, allow users to pick their own skin / environment, and for just increasing application performance and initial download size. Even though this tutorial is using AIR, you can leverage the same code in the browser.

Step 3: Its all about the StyleManager

When using runtime skinning, we will be leveraging the StyleManager.loadStyleDeclarations(). The StyleManager is exactly what it sounds like – it manages style inheritance for all visual objects. The StyleManager lets you apply inheritable and non-inheritable styles globally, but one thing to remember is dealing with styles is extremely resource intensive.

So the problem is that we want to leverage the StyleManager.loadStyleDeclarations to apply a remote skin to our application. When this is called, the Flash Player will reapply all styles to the display list which will degrade performance. The solution is the display list, what if you apply the style before anything is added to the display list? To do this, we are going to remotely load the style SWF on pre-initialization, and not until the style SWF is loaded will we let the application continue its initialization. So we want to first attach the handler for preinitialize on the application:

   preinitialize="onPreinitialize(event)"

So our onPreinitialize function looks like this:

   private function onPreinitialize(event:FlexEvent):void {
     var findPlatform:Array=Capabilities.version.split(" ", 1);
	  platform=findPlatform[0];
	  if (platform != "MAC") {
	     platform="WIN"
	  }
	  var eventDispatcher:IEventDispatcher;
	  eventDispatcher=StyleManager.loadStyleDeclarations("skins/" + platform + ".swf");
	  eventDispatcher.addEventListener(StyleEvent.COMPLETE, onStyleComplete);
   }

The first thing we are going to do is find the current operating system. For this we read the System Capabilities from the Flash Player, more specifically, the version of the Flash Player. We split after the first space and take that value as our current platform. We now check to see if we are on a Mac, if we are not we will treat the platform asWindows and set the platform to WIN. Author note: In this code I am being biased and I know that I am treating everything as Mac or not Mac – I apologize to those who I offended.

   var findPlatform:Array=Capabilities.version.split(" ", 1);
   platform=findPlatform[0];
   if (platform != "MAC") {
     platform="WIN"
   }

Moving on, we are creating an EventDispatcher to handle the loading of the remote style SWF. One thing to keep in mind when leveraging the loadStyleDeclarations – the Flash Player will apply new style only once – it caches loaded styles, so if you load the style again it will be loading it from player cache and apply the current style where necessary. We now attach a style event of complete to the dispatcher in order to notify the onStyleCompete method that the style SWF is loaded and ready.

   var eventDispatcher:IEventDispatcher;
   eventDispatcher=StyleManager.loadStyleDeclarations("skins/" + platform + ".swf");
   eventDispatcher.addEventListener(StyleEvent.COMPLETE, onStyleComplete);

But wait, you gave me these style SWFs, how do I make my own? Easy – right click on the css file and choose “Compile CSS to SWF” and you can now load your own skin file dynamically. Not only does this decrease size on your initial download, but now you can manage your skins separately from your application and keep your designers happy.

Step 4: The style is loaded, now what?

We have now been notified that the style is ready and has been added to the StyleManager.

   private function onStyleComplete(event:StyleEvent):void {
	 event.stopImmediatePropagation();
	 super.initialized=true;
   }

The first thing we want to do is allow the Flash Player to continue in its life-cycle. We tell the application that it has initialized and is can now add display objects. In most cases, if you know an event will not be used in another method or in by a child object in your application, you can stop the propagation of that event from traversing further through the application, but more on this in another tutorial.

Step 5: The application has style, know lets manage the UI

We have added a handler to notify us when the application has created and added all display object, we now want to manipulate those objects.

   applicationComplete="setWindow()"

This is where the real customization begins. We want to tell our AppWindow what OS we are on so it can make the necessary changes to its own layout, but we also want to tell our application what current state it must be in. When we change the state of the application, we are using states to dynamically apply changes to the style selector for a specific component. Anything that is a setStyle can be embeed in a style sheet and be applied as a styleName. We now want to center our application and add event listeners to notify our AppWindow that it is either the current window or a background window. The final instructions of the method is to printCapabilities, which takes the Flash Player Capabilities class and dumps it into our datagrid.

   private function setWindow():void {
	 appWindow.os=platform;
	 currentState=platform;
	 nativeWindow.x=Capabilities.screenResolutionX / 4;
	 nativeWindow.y=Capabilities.screenResolutionY / 4;
	 addEventListener(AIREvent.APPLICATION_ACTIVATE, setApplicationFoucs);
	 addEventListener(AIREvent.APPLICATION_DEACTIVATE, setApplicationFoucs);
	 printCapabilities(Capabilities);
   }

Step 6: Show me the States, the Script and the Results

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" preinitialize="onPreinitialize (event)" applicationComplete="setWindow()" showFlexChrome="false" width="600" height="500" layout="absolute" xmlns:techlabs="com.justinimhoff.techlabs.*">
	<mx:states>
		<!---
			 When using states you are calling setStyle on object directly
			 Notice the difference between the setStyle and setProperty, anything that say setStyle can be embedded
			 in the styleSheet and then the object's style can be applied through the object's styleName
		-->
		<mx:State name="MAC">
			<mx:RemoveChild target="{button1}"/>
			<mx:SetStyle target="{userPlatformControl}" name="top" value="50"/>
			<mx:SetStyle target="{techLabsFooter}" name="bottom" value="79"/>
			<mx:SetStyle target="{techLabsFooter}" name="left" value="40"/>
			<mx:SetStyle target="{techLabsFooter}" name="right" value="40"/>
			<mx:SetStyle target="{capabilitiesList}" name="top" value="79"/>
			<mx:SetStyle target="{capabilitiesList}" name="right" value="40"/>
			<mx:SetStyle target="{capabilitiesList}" name="bottom" value="129"/>
			<mx:SetStyle target="{capabilitiesList}" name="left" value="40"/>
			<mx:SetStyle target="{capabilitiesList}" name="left" value="40"/>
			<mx:SetStyle target="{userPlatformControl}" name="left" value="48"/>
			<mx:SetStyle target="{label1}" name="fontSize" value="16"/>
			<mx:SetStyle target="{label1}" name="fontWeight" value="bold"/>
			<mx:SetStyle target="{label1}" name="horizontalCenter" value="0"/>
			<mx:SetStyle target="{label1}" name="top" value="40"/>
			<mx:SetStyle target="{label1}" name="color" value="#333333"/>
			<mx:SetStyle target="{techLabsFooter}" name="verticalAlign" value="middle"/>
			<mx:SetProperty target="{techLabsFooter}" name="height" value="51"/>
			<mx:SetProperty target="{button2}" name="styleName" value="toolBarButtons"/>
			<mx:SetProperty target="{button2}" name="label"/>
		</mx:State>
		<mx:State name="WIN">
			<mx:RemoveChild target="{button2}"/>
			<mx:SetStyle target="{userPlatformControl}" name="left" value="40"/>
			<mx:SetStyle target="{userPlatformControl}" name="right" value="40"/>
			<mx:SetStyle target="{userPlatformControl}" name="top" value="78"/>
			<mx:SetStyle target="{techLabsFooter}" name="bottom" value="79"/>
			<mx:SetStyle target="{techLabsFooter}" name="left" value="40"/>
			<mx:SetStyle target="{techLabsFooter}" name="right" value="40"/>
			<mx:SetStyle target="{capabilitiesList}" name="top" value="107"/>
			<mx:SetStyle target="{capabilitiesList}" name="right" value="40"/>
			<mx:SetStyle target="{capabilitiesList}" name="bottom" value="129"/>
			<mx:SetStyle target="{capabilitiesList}" name="left" value="40"/>
			<mx:SetStyle target="{capabilitiesList}" name="left" value="40"/>
			<mx:SetStyle target="{techLabsFooter}" name="verticalAlign" value="middle"/>
			<mx:SetStyle target="{label1}" name="horizontalCenter" value="0"/>
			<mx:SetStyle target="{label1}" name="top" value="40"/>
			<mx:SetStyle target="{label1}" name="fontSize" value="16"/>
			<mx:SetStyle target="{label1}" name="fontWeight" value="bold"/>
			<mx:SetStyle target="{label1}" name="color" value="#FFFFFF"/>
			<mx:SetProperty target="{userPlatformControl}" name="styleName" value="toolBar"/>
			<mx:SetProperty target="{button1}" name="styleName" value="toolBarButtons"/>
			<mx:SetProperty target="{userPlatformControl}" name="height" value="30"/>
			<mx:SetProperty target="{button1}" name="height" value="25"/>
			<mx:SetProperty target="{techLabsFooter}" name="height" value="51"/>
		</mx:State>
	</mx:states>
	<techlabs:AppWindow width="100%" height="100%" id="appWindow">
		<mx:Label text="Native Window Explorer" id="label1"/>
		<mx:DataGrid id="capabilitiesList"/>
		<mx:HBox id="techLabsFooter" backgroundColor="#333333">
			<mx:Image source="@Embed(source='images/logo-trans1.png')" id="techLabsLogo" left="15"/>
		</mx:HBox>
		<mx:HBox id="userPlatformControl">
			<mx:Button label="MAC Style" click="switchPlatform('MAC')" enabled="{platform == 'WIN'}" id="button1"/>
			<mx:Button label="PC Style" click="switchPlatform('WIN')" enabled="{platform == 'MAC'}" id="button2"/>
		</mx:HBox>
	</techlabs:AppWindow>
	<!--- This compiles the global style in the SWF -->
	<mx:Style source="skins/Default.css"/>
	<mx:Script>

			import mx.core.Container;
			import mx.controls.Text;
			import flash.utils.describeType;
			import mx.collections.ArrayCollection;
			import mx.events.StyleEvent;
			import mx.events.FlexEvent;
			import mx.managers.SystemManager;
			import mx.events.AIREvent;
			import flash.utils.*;

			[Bindable]
			private var platform:String;

			//We are not overriding preinitialization, but are instead adding on to the cycel
			private function onPreinitialize(event:FlexEvent):void {
				var findPlatform:Array=Capabilities.version.split(" ", 1);
				platform=findPlatform[0];
				if (platform != "MAC") {
					platform="WIN"
				}
				var eventDispatcher:IEventDispatcher;
				//Dispatch a styleEvent that the remote style SWF has been successfully loaded
				eventDispatcher=StyleManager.loadStyleDeclarations("skins/" + platform + ".swf");
				eventDispatcher.addEventListener(StyleEvent.COMPLETE, onStyleComplete);
			}

			// We do not want Flex to continue onto initialization until the style SWF is loaded and we are ready to initialize
			override public function set initialized(value:Boolean):void {
				// wait until the style swf is done loading!
			}

			private function onStyleComplete(event:StyleEvent):void {
				//Tell the app that we are ready to initialize
				event.stopImmediatePropagation();
				super.initialized=true;
			}

			//This is used to set the state of the application and set the view of the AppWindow
			private function setWindow():void {
				appWindow.os=platform;
				currentState=platform;
				//Center the window on the desktop
				nativeWindow.x=Capabilities.screenResolutionX / 4;
				nativeWindow.y=Capabilities.screenResolutionY / 4;
				//In the native OS environment we will have multiple window open and we need to notify the user that they have or donot have
				// focuse on the window
				addEventListener(AIREvent.APPLICATION_ACTIVATE, setApplicationFoucs);
				addEventListener(AIREvent.APPLICATION_DEACTIVATE, setApplicationFoucs);
				// Take the current Flash Player Capabilities class and dump it into a datagrid
				printCapabilities(Capabilities);
			}

			private function setApplicationFoucs(event:AIREvent):void {
				event.stopImmediatePropagation();
				if (event.type == "applicationDeactivate") {
					appWindow.active=false;
				} else {
					appWindow.active=true;
				}
			}

			//Dump the Capabilites to a datagrid
			private function printCapabilities(o:Object):void {
				var def:XML=describeType(o);
				var props:[email protected];
				[email protected];
				var capArray:Array=new Array();
				for each (var prop:String in props) {
					var capObj:Object=new Object();
					capObj.propName=prop;
					capObj.propValue=o[prop]
					capArray.push(capObj);
				}
				capabilitiesList.dataProvider=capArray;
			}

			//User defined switch of the current platform
			private function switchPlatform(os:String):void {
				StyleManager.loadStyleDeclarations("skins/" + os + ".swf");
				appWindow.os=os;
				platform=os;
				currentState=os;
			}

This is what you get:

Notice the difference in the headers, the experience generated by each one is very specific and vastly different

If you are reading this, you must be a ninja

I told you I would go over unloading stylse and performance improvements, so that is what i am going to do. Unloading your style is just the opposite as removing it. For this example I chose not to unload my styles because I had two rather small skins, but for larger projects it should be a consideration. When you unload a style SWF, you are removing all the style selectors set by the specific style SWF.

Usage:

   StyleManager.unloadStyleDeclarations("style SWF name");

The great thing about unloading the style is that the global or next level style is not reapplied, but instead objects now inherit the next selector values.

What about performance?

One of the best ways to apply style in Flex 3 is by applying style on pre-initialzation as described in this tutorial. The main reason for this is that there are no object to be notified of changes, instead the object apply the style once it has been created. Most large application are broken up into modules and loaded as need, this same structure that should be used with styles.

You should really read this part – only use setStyle when you are dynamically setting the style on existing objects, but even then you can just use the style name and reuse that style in multiple places. The best way to leverage the setStyle is through using styleDeclarartions:

     import mx.styles.StyleManager;

     private var newDynamicStyle:CSSStyleDeclaration;

     private function addDynamicStyle():void {
        newDynamicStyle = new CSSStyleDeclaration('coolButton');
        newDynamicStyle.setStyle('color', 'blue');
        newDynamicStyle.setStyle('themeColor', 'blue');
        newDynamicStyle.setStyle('fontSize', 16);

        /* Apply the new style to all Buttons. By using a type
           selector, this CSSStyleDeclaration object will replace
           all Button style properties, causing potentially unwanted
           results. */

        StyleManager.setStyleDeclaration("Button", coolButton);

        /* Apply the CSSStyleDeclaration to a specific object.
           If the type or class selector does not currently exist, it is added - if it does,
           it then will replace the current selector.
			You are now applying a specific selector class to the button and only requesting one lookup*/

        StyleManager.setStyleDeclaration(".newButtonStyle", coolButton);

        daButton.stylename = ".newButtonStyle";
     }

Any time you are using styleDeclarations, you have the ability to tell the Flash Player to not update the styles immediately, but instead wait till styleChanged() is called. This would allow you to call the setStyleDeclaration, followed by the clearStyleDeclaration and force Flash to store the style selector, but not apply it until selectors are removed. By default, Flash Player will update the style immediately. Any time you call a styleDeclaration method, you are telling Flash to re-compute the style for every visual component in the application, so use sparingly.

   StyleManager.setStyleDeclaration(".newButtonOne", buttonOneStyle, false);
   StyleManager.setStyleDeclaration(".newButtonTwo", buttonTwoStyle, false);
   StyleManager.setStyleDeclaration(".oldButton", true);

Now take what i have published, impliment, check and ask questions – Thanks for reading

Comments (22)

  1. TypeError: Error #1009: Cannot access a property or method of a null object reference.

  2. I have posted a follow up blog post that describes more about native application windows and gives details on what it mean to create a native window – take a look and as always I am open to comments and questions. Thanks for reading.

    http://bit.ly/HL35L

  3. Machel, it looks like the source code is missing the skins files. They should be updated shortly.

    1. Sorry all! Everything is in place now. Think that now you will not get the same error.

  4. Great post, Justin. I find this post very useful for the dynamic customization of a user’s application experience. Chrome make apps bland.

  5. Hi, nice tutorial. Maybe someday i will develop an apps.. 😀

  6. The downside to custom chrome windows is that the drop shadow is considered part of the stage, so when you drag the resize handle down to the dock, it stops short. I wish you could just set the window “bounds” and it will consider those bounds to be the actual ones instead of including the drop shadow.

  7. @Jonnie, you are correct, the drop shadow is a huge problem. Their is a solution, which is to create another skin that lacks or has decreased shadow, and manage the size of your application. If the (application width – the stage width) gets over a certain value, you can change the skin to the fullscreen skin. The problem with this is a performance decrease. I had this originally in the sample, but it became quiet complex and heavy to manage.

    1. Hi Junior,
      Pls bear in mind this is a AIR application, therefore there are no SWF files. If you want to try the demo, you must download the air file and install it in your desktop.

  8. First of all – thx for that great tutorial!
    I found out a very strange behavior of the app if The dock (MAC) is positiond on the left side of the screen. If you drag the app around than sometimes it disappears. No problem if the dock is on the right side or at the bottom of the screen. Can you imagine why this problem occures?

  9. Mike, you are correct that the application goes offscreen with the dock on the left. I reproduced it with other native window applications, and also ran into an issues when using system chrome the application will resize unexpectedly and also shift from visible to invisible. I will run everything through the debugger and hopefully submit a bug to adobe and keep you updated. Thanks for pointing this out and reading the tutorial.

  10. Mike, here is the bug #22326 if you would like to add any information about what you are seeing. https://bugs.adobe.com/jira/browse/SDK-22326.

  11. @Justin, thx – that was the bug! Weird.
    But I found another problem. With the SDK 3.3 the creation process of the App fails. The first call of “setWindow” comes before the “layoutWindow” was called. That crashes the app because the buttons dont already exist.

    My quick-fix:
    ————-
    private function layoutWindow(event:FlexEvent):void {
    createUIComponents();
    }

    private function createUIComponents():void {
    // This button is the indicator if the UIs are already created!
    if(resizeButton != null) return;

    //You must set the style property of the container after it has been created
    styleName=”active”;

    public function set os(value:String):void {
    createUIComponents();

    Mike

  12. What if you want to add a new skin to your application? You have to update the entire AIR application? Im not to happy with this. I know you can load remote swfs via byte code , but there is a small little indirect “take note” in all Adobe documentation regarding forward compatibility issues of allowLoadBytesCodeExecution which has me worried at the moment about trying to build a application that can have new skins added to it without reinstalling the entire application.

  13. Ian, so what i am understanding is you want an application where by you can update the style of the application without downloading the application. So above there is an example of an embedded style with the default.css that contains the basic style of the components, then there are two dynamic style files for Mac and PC. Depending on your OS it loads different styles. So to take this a step further, when your app checks to see if there is an update available, why not check to see if there is a new skin available – if so download and overwrite the dynamic skin. Your concern seems related to the default.css, which you can just change to dynamic and load one skin in dynamically, download and overwrite the new one and then call a refresh on the StyleManager. Using the StyleManager to apply styles is fully supported by Adobe and is the right way to go.

  14. one problem I found (already mentioned, but I have an easier fix):

    this line:
    applicationComplete=”setWindow()”

    since techlabs:AppWindow isn’t instantiated by the time applicationComplete fires, any reference to it results in a nullpointer error.

    here’s the fix:
    1. remove this:
    applicationComplete=”setWindow()”

    2. change this:

    to this:

    and all works fine.

  15. 2. change this…
    techlabs:AppWindow width=”100%” height=”100%” id=”appWindow”

    to this…
    techlabs:AppWindow width=”100%” height=”100%” id=”appWindow” creationComplete=”setWindow()”

  16. Hi, Justin, This is very useful. I just wonder if you have a newer version of Mac.css and Windows.css to download. Thanks,
    Xu

  17. Xu, I do not have any updated CSS files, but if you tell me what you are looking for I would be happy to help. Thanks for your feedback!

  18. Thank you, Justin. Really appreciate it. I was looking for horizontal scroll.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>