Hacking width and height properties into Flex’s CSS model

As much as I love working with the Flex layout model, in many ways it feels inferior to its HTML cousin. I’ll take Flex’s container components over HTML tables and divs any day, but I’ve always been disappointed with the fairly weak CSS model found in Flex. From an architectural level the framework decisions makes sense and keeps internal logic fast and clean, but from an implementation standpoint, it becomes an annoying roadblock. Heres a quick example of the problem.

<mx:Style>
	.navItem {
		vertical-align: middle;
		corner-radius: 5;
		border-style: solid;
		border-color: #F3CB19;
		background-color: #A87500;
	}
</mx:Style>
<mx:HBox styleName="navItem" height="30"/>
<mx:HBox styleName="navItem" height="30"/>
<mx:HBox styleName="navItem" height="30"/>

In this case, since height is a class property, it must be defined on the HBox instance and can’t be moved into CSS. It makes the code less manageable and adds restrictions to changing themes at runtime.

In order to define class properties with CSS we need to break some of the framework rules. Since we can’t have class metadata defining a value as both a CSS and a class property, we need to leave all metadata as-is, and ‘foward’ the css definitions on to the class. In practical terms what this means is that changing the height in css will change the height on the component, but changing the height directly on the component wont update the css definition. The code below illustrates how this plays out inside a custom component.

package com.craftymind.controls
{
	import mx.containers.HBox;
	import mx.styles.StyleManager;

	public class extHBox extends HBox
	{
		override public function styleChanged(styleProp:String):void{
			super.styleChanged(styleProp);
			if(!styleProp || styleProp == "styleName"){ //if runtime css swap or direct change of stylename
				var classSelector:Object = StyleManager.getStyleDeclaration("." + styleName);
				if(classSelector != null){
					applyProperties(classSelector, ["width", "height", "percentWidth", "percentHeight", "x", "y", "visible"]);
				}
			}
		}
		private function applyProperties(styleObj:Object, arr:Array):void{
			for each (var item:String in arr){
				var prop:Object = styleObj.getStyle(item);
				if(prop != null) this[item] = prop;
			}
		}
	}
}

When the HBox detects that the styleName or runtime definition has changed, it scans the classSelector for width, height, x, y and visible properties to be applied. You can add whatever properties you want in there including text labels or image sources depending on the component being extended. I haven’t fully tested it so I’m not sure if type selectors work, and I know that calling obj.setStyle(“height”, 10); definitely wont work, but your better off calling obj.height = 10 in that case anyway.

Heres a complex menu system controlled completely with css including y positioning, height changes, and header visibility.


View Source

We now have separation of content from style thats alot closer to what HTML offers, and as a bonus, all property-based databinding continues to work on the values passed through CSS.

<cm:extHBox styleName="header">
	<mx:Label text="Header Text"/>
</cm:extHBox>
<cm:extHBox id="nav" styleName="navContainer">
	<cm:extHBox styleName="navItem">
		<mx:Label text="Nav Item 1"/>
	</cm:extHBox>
	<cm:extHBox styleName="navItem">
		<mx:Label text="Nav Item 2"/>
	</cm:extHBox>
	<cm:extHBox styleName="navItem">
		<mx:Label text="Nav Item 3"/>
	</cm:extHBox>
</cm:extHBox>
<mx:Button label="Switch CSS" click="switchCSS()" y="{nav.y+nav.height+2}"/>
.header {
	background-color: #000000;
	border-style: solid;
	border-color: #FFFFFF;
	border-thickness: 2;
	color: #FFFFFF;
	x: 0;
	y: 0;
	percent-width: 100;
	height: 20;
	visible: true;
}
.navContainer {
	background-color: #000000;
	border-style: solid;
	border-color: #FFFFFF;
	border-thickness: 2;
	vertical-align: bottom;
	horizontal-align: left;
	padding-left: 3;
	x: 0;
	y: 20;
	percent-width: 100;
	height: 50;
}
.navItem {
	vertical-align: middle;
	corner-radius: 5;
	border-style: solid;
	border-sides: "left top right";
	border-color: #FFFFFF;
	border-thickness: 2;
	background-color: #000000;
	padding-left: 3;
	padding-right: 3;
	color: #FFFFFF;
	height: 30;
}

Image smoothing in Flex

The Flex Image control doesn’t expose Bitmap smoothing by default but can easily be added in through subclassing the component. Smoothing is a nice feature for removing jaggies from images that have been scaled either up or down, and in practice hasn’t caused any noticable cpu hangups to do the post processing. I’m writing up a patch to submit to the Flex Open Source initiative, but in the meantime, heres a quick and dirty hack to enable it in the Flex 2 and Flex 3 SDK.

Create a new MXML component and name it SmoothImage.mxml, then add the following code.

<?xml version="1.0" encoding="utf-8"?>
<mx:Image xmlns:mx="http://www.adobe.com/2006/mxml">
	<mx:Script>
		<![CDATA[
			import flash.display.Bitmap;
			override protected function updateDisplayList(unscaledWidth:Number,unscaledHeight:Number):void{
				super.updateDisplayList(unscaledWidth, unscaledHeight);
				if(content is Bitmap){
					var bmp:Bitmap = Bitmap(content);
					if(bmp && bmp.smoothing == false){
						bmp.smoothing = true;
					}
				}
			}
		]]>
	</mx:Script>
</mx:Image>

Now just use <local:SmoothImage source=”myimage.jpg”/> the same way you’d use a normal image component and you’re all set. This code will take care of smoothing both dynamically loaded images as well as embedded images in one simple script. It also handles broken images gracefully. Heres a quick sample showing the affects of scaling the Google logo with and without smoothing.

SmoothImage test case


And the code used to create this…

<!-- Top Images -->
<mx:Image source="{googlelogo}" width="60" height="25"/>
<mx:Image source="{googlelogo}" width="200" height="90"/>
<mx:Image source="{googlelogo}" width="400" height="180"/>

<!-- Bottom Images -->
<local:SmoothImage source="{googlelogo}" width="60" height="25"/>
<local:SmoothImage source="{googlelogo}" width="200" height="90"/>
<local:SmoothImage source="{googlelogo}" width="400" height="180"/>

Saving class data to disk in AIR

For eBay Desktop we wrote a simple utility for reading and writing classes directly to disk. Using this method we’re able to load the users existing data from disk, or if its unavailable, create a new class with all the defaults.

First up is the utility class AIRUtils

package com.effectiveui.util
{
	import flash.filesystem.File;
	import flash.filesystem.FileMode;
	import flash.filesystem.FileStream;

	public class AIRUtils
	{
		public static function readFileObject(fil:File):Object{
			var amf:Object;
			if(fil.exists){
				var stream:FileStream = new FileStream();
				stream.open(fil, FileMode.READ);
				amf = stream.readObject();
				stream.close();
			}
			return amf;
		}
		public static function writeFileObject(fil:File, obj:Object):Object{
			var stream:FileStream = new FileStream();
			stream.open(fil, FileMode.WRITE);
			stream.writeObject(obj);
			stream.close();
			return obj;
		}
	}
}

These 2 static methods act as simple helper wrappers to read and write classes out to disk.

The example class we want to save below looks very similar to the type of class we use inside eBay Desktop.

package com.ebay.model
{
	import com.ebay.model.SearchPreferences;

	[Bindable]
	[RemoteClass(alias="com.ebay.model.UserPreferencesModel")]
	public class UserPreferencesModel
	{
		public static const STANDARD:String = "standard";
		public static const SKINNY:String = "skinny";

		public var logoutOnClose:Boolean = false;
		public var viewState = STANDARD;
		public var globalSearchFilters:SearchPreferences = new SearchPreferences();

		private var _maxHistoryItems:uint = 1000;
		public function set maxHistoryItems(max:uint):void{
		   _maxHistoryItems = max;
		}
		public function get maxHistoryItems():uint{
		   return _maxHistoryItems;
		}
	}
}

In order to write a class out to disk and read it back in, you have to add RemoteClass metadata to it. It doesn’t matter what the value is it just needs to be unique to the application. This provides Flash with an identifier for linking amf data to class definitions when its loaded back in. It doesn’t matter what value you put in the RemoteClass tag, but the best practice is to use the class name of the model you’re saving. Additionally, in this example we’d need to make sure both UserPreferencesModel and SearchPreferences model have RemoteClass metadata, since caching the UserPrefencesModel will automatically attempt to cache the SearchPreferences as a child model.
Only public variables will be cached to disk, so both the static values and the private var will be thrown out, but the public getter/setters will be cached.

Interacting with the cached data using AIRUtils looks like this.

private var fileRef:File = File.applicationStorageDirectory.resolvePath("UserPreferences.dat");
private var userPrefs:UserPreferencesModel;

private function loadPrefs():void{
	userPrefs = AIRUtils.readFileObject(fileRef) as UserPreferencesModel || new UserPreferencesModel();
}
private function savePrefs():void{
	AIRUtils.writeFileObject(fileRef, userPrefs);
}

When loadPrefs() is called, the cached class is read in and cast as a UserPreferencesModel. If no file exists in the users cache, we end up creating a new class and the defaults defined in the class will be used. This ensures we have something to work with, all within 1 tidy line of code. To save out the file we just pass the file reference and the class instance to AIRUtils.writeFileObject() and we’re done.