09.27Tutorial - Creating Custom Flex Components - Skins & CSS Styles
Hi Everyone,
over the span of the next 6 weeks I will be creating 2 custom flex components for Verizon Wireless' MediaStore. One will be a data grid which is meant to be faster and custom fitted to their needs, and another one is to display search results.
Right now, I am at the exploration stage of building things from the ground up. These components need to be skinnable, meaning I need to offer css styling that I will use to draw the respective component's UI. My first challenge was to enable the use of either graphic elements from a .swf or a programmatic skin. In other words, to permit something like this:
-
ContentGrid
-
{
-
headerTopLine : #EDEDED;
-
headerSeparatorAlpha : 0.20;
-
arrowGradient : #424242, #5b5b5b;
-
-
sortArrowSkin : ClassReference("com.ml.styles.view.skins.GridHeaderSortArrowSkin");
-
-
icon : Embed(source="../resources/styles/assets/symbols.swf", symbol="scrollableArrowMenuUpIcon");
-
}
For the first three styles, you can easily get the values using the getStyle() function of the UIComponent (or any derived class). For example, the first three values:
-
// this will convert the hex color value to a uint, to be used directly with the drawing API
-
var topLineColor:uint = getStyle("headerTopLine");
-
-
// this will give you the numeric value, just as set in the css
-
var seperatorAlpha:int = getStyle("headerSeperatorAlpha");
-
-
// this will give you two colors in an array, the second command converts them
-
var colors:Array = getStyle("arrowGradient");
-
StyleManager.getColorNames(colors);
Don't ask me why they called it getColorNames cause that doesn't make sense to me! Maybe it has another reason of being..
Now for the tricky part: to get Class references (skins) and use them, and to get embedded assets from inside a .swf file. The important detail to remember is that for example this line
-
headerBgSkin : ClassReference("com.ml.skins.ContentGridHeaderBgSkin");
could also be a reference to an asset inside a swf like so
-
headerBgSkin : Embed(source="../resources/styles/assets/symbols.swf", symbol="contentGridHeaderBg");
Therefore, when handling this element, you need to think of these two possibilities.
Handling a style that can either be a ProgrammaticSkin or Graphical Asset
First, here is the code for the Programmatic skin
-
package com.ml.skins
-
{
-
import mx.skins.ProgrammaticSkin;
-
-
public class ContentGridHeaderBgSkin extends ProgrammaticSkin
-
{
-
public function ContentGridHeaderBgSkin()
-
{
-
super();
-
}
-
-
override protected function updateDisplayList(w:Number, h:Number):void
-
{
-
var g:Graphics = graphics;
-
g.clear();
-
-
var colors:Array = getStyle("headerBgGradient");
-
StyleManager.getColorNames(colors);
-
-
var matrix:Matrix = new Matrix();
-
matrix.createGradientBox(w, h + 1, Math.PI/2, 0, 0);
-
-
colors = [ colors[0], colors[1] ];
-
var ratios:Array = [ 0, 255 ];
-
var alphas:Array = [ 1.0, 1.0 ];
-
-
-
g.beginGradientFill(GradientType.LINEAR, colors, alphas, ratios, matrix);
-
g.lineStyle(0, 0x000000, 0);
-
g.drawRect(0,0,w,h);
-
g.endFill();
-
-
g.lineStyle(1, getStyle("headerTopLine"), 1);
-
g.moveTo(0,0);
-
g.lineTo(w, 0);
-
-
g.lineStyle(1, getStyle("headerBottomLine"), 1);
-
g.moveTo(0, h - 0.5);
-
g.lineTo(w, h - 0.5);
-
}
-
}
-
}
As you might have noticed, I use styles to decide on colors used to draw the skin. This is an important detail, we'll get back to that shortly.
This skin is used to draw the background of the header of the custom DataGrid. It is, therefore, a graphical asset. I have decided to create this skin in the createChildren function of the DataGrid like so
-
public class ContentGrid extends ScrollControlBase
-
{
-
protected var _headerBgSkin:DisplayObject;
-
-
// ....
-
-
override protected function createChildren():void
-
{
-
super.createChildren();
-
-
// get the reference to the style defined in the CSS
-
var headerSkin:Object = getStyle("headerBgSkin");
-
-
// if the style is a Sprite (therefore a symbol in a .swf)
-
if(headerSkin is DisplayObject)
-
{
-
// nothing fancy here, just use it straight
-
_headerBgSkin = headerSkin;
-
-
// if it is a sprite, make sure it doesn't react to mouse events
-
if(_headerBgSkin is Sprite)
-
(headerSkin as Sprite).mouseEnabled = false;
-
-
}
-
// however if it is a class reference
-
else if(headerSkin is Class)
-
{
-
// instantiate it, assuming it is derived from ProgrammaticSkin
-
_headerBgSkin = new headerSkin();
-
}
-
-
// finally, add it to the displayList
-
addChild(_headerBgSkin);
-
}
-
}
As you can see from the code above, both situations are handled. Now in the updateDisplayList method of our custom component, just in case the developer decided to use a programmatic skin, we want to ensure this skin has access to styles he might want to define inside of the same CSS Class. We do it like so
-
override protected function updateDisplayList(w:Number, h:Number):void
-
{
-
super.updateDisplayList(w, h);
-
-
// if the _headerBgSkin implements the basic style client properties & functions, set his style appropriately
-
if(_headerBgSkin is ISimpleStyleClient)
-
ISimpleStyleClient(_headerBgSkin).styleName = this;
-
-
_headerBgSkin.width = width;
-
_headerBgSkin.height = 32;
-
}
This ensures that these lines from the programmatic skin will work properly:
-
var colors:Array = getStyle("headerBgGradient");
-
StyleManager.getColorNames(colors);
-
-
// ...
-
-
g.lineStyle(1, getStyle("headerTopLine"), 1);
-
-
// and so on
At this point I am thinking of hardcoding defaults inside of the skin, just in case...
Handling Style Changes
In UIComponent derived classes there is a nice function you can derive which has the following signature:
-
override public function styleChanged(styleProp:String):void
This function gets called when a style has changed, or all styles have changed. I found the following implementation in ScrollControlBase
-
override public function styleChanged(styleProp:String):void
-
{
-
var allStyles:Boolean = (styleProp == null || styleProp == "styleName");
-
-
super.styleChanged(styleProp);
-
-
// ....
-
-
// Replace the borderSkin
-
if (allStyles || styleProp == "borderSkin")
-
{
-
if (border)
-
{
-
removeChild(DisplayObject(border));
-
border = null;
-
createBorder();
-
}
-
}
-
}
As you can see from the code above, there is a need to remove the previously created graphic object (border in this case) before re-creating it. I have implemented something similar for my _headerBg (has been shortened to _headerBg from _headerBgSkin by now!). You can see the changes below: for one I put all the code needed to create the _headerBg in a function that I could call from different places, then I added the styleChanged function. A few other modifications have been made, be very attentive! Here it is:
-
package com.ml.contentgrid
-
{
-
import flash.display.DisplayObject;
-
import flash.display.Sprite;
-
-
import mx.collections.ArrayCollection;
-
import mx.core.ScrollControlBase;
-
import mx.styles.ISimpleStyleClient;
-
-
public class ContentGrid extends ScrollControlBase
-
{
-
// {...}
-
protected var _headerBg:DisplayObject;
-
-
// {...}
-
-
override protected function createChildren():void
-
{
-
super.createChildren();
-
-
createHeaderBg();
-
-
_header = new ContentGridHeader();
-
addChild(_header);
-
}
-
-
protected function createHeaderBg():void
-
{
-
var idx:Number = numChildren;
-
-
// first remove existing graphical object... (if it exists)
-
if(_headerBg != null)
-
{
-
idx = getChildIndex(_headerBg);
-
removeChild(_headerBg);
-
_headerBg = null;
-
}
-
-
// create new skin
-
var headerSkin:Object = getStyle("headerBgSkin");
-
-
if(headerSkin is DisplayObject)
-
{
-
_headerBg = headerSkin;
-
-
if(headerSkin is Sprite)
-
(headerSkin as Sprite).mouseEnabled = false;
-
}
-
else if(headerSkin is Class)
-
{
-
_headerBg = new headerSkin();
-
}
-
-
addChildAt(_headerBg, idx);
-
}
-
-
// {...}
-
-
override protected function updateDisplayList(w:Number, h:Number):void
-
{
-
super.updateDisplayList(w, h);
-
-
_headerBg.width = width;
-
_headerBg.height = calculateHeaderHeight();
-
}
-
-
override public function styleChanged(styleProp:String):void
-
{
-
var allStyles:Boolean = (styleProp == null || styleProp == "styleName");
-
super.styleChanged(styleProp);
-
-
if(allStyles || styleProp == "headerBgSkin")
-
{
-
createHeaderBg();
-
}
-
-
if(_headerBg is ISimpleStyleClient)
-
ISimpleStyleClient(_headerBg).styleName = this;
-
-
invalidateDisplayList();
-
}
-
-
// {...}
-
}
-
-
}
That's it for today. As I dig deeper, I will blog about my findings..
Thanks for reading!
Cheers!
Martin

Am about to start creating my own skin enabled components. Wish me luck!
July 22nd, 2009 at 3:45 am