Tuesday, January 30, 2007

Napkin skins, stage three: the (current) limits of skinning

After seeing what we can do with graphical and programmatic skins, it is time to explore what you can't do with Flex skins (yet). Unfortunately, you need to subclass ComboBox and DataGrid in order to get to the full napkin theme (click on the screenshot to see the app):

The changes at this stage are reflected in the MXML file (to see the complete file, select View Source from the app's context menu):
    <components:ScrawlGrid id="dg" width="100%" height="50%" dataProvider="{employees.employee}">
<mx:DataGridColumn dataField="name" headerText="Name"/>
<components:ScrawlComboBox width="50%" dataProvider="[Lorem, Ipsum, Dolor, Amet]"/>
[Note the use of <components:columns> instead of <mx:columns>.] We are now using the subclasses of ComboBox and DataGrid: components.ScrawlComboBox and components.ScrawlGrid.

ScrawlComboBox differs from ComboBox only in specifying a different dropdownFactory:
    private var _ddFactory:IFactory = new ClassFactory(ScrawlComboBoxDropdown); 
Subclassing ComboBox allows us to do two things: (1) specify a post-it skin for the dropdown list (in scrawl4.css):
    ScrawlComboBoxDropdown { background-image: Embed('assets/postit.jpg'); }
and (2) specify a cross-hatch pattern for rollover and selection in the dropdown list (in ScrawlComboBoxDropdown.as):
    override protected function drawSelectionIndicator(
indicator:Sprite, xx:Number, yy:Number,
ignoredWidth:Number, h:Number, color:uint,
var w:int = unscaledWidth - viewMetrics.left - viewMetrics.right;
ScrawlUtil.drawCrossHatch(Sprite(indicator).graphics, w, h, color, 1.0);
indicator.x = xx;
indicator.y = yy;
Regarding (1): Since the background-image style is inherited, we could have simply created the style for List, which would have the same effect for this app, without subclassing ComboBox (since the default dropdown for a ComboBox is a List), but that would have meant that all Lists in the UI would have had a post-it background, which is not what we want here.

Regarding (2): Note that ScrollComboBoxDropdown subclasses List and overrides drawHighlightIndicator() and drawSelectionIndicator(). Flex makes it easy to skin rollover and selection in a List or DataGrid if all you want to do is change the color:
    roll-over-color: #E4E444;
selection-color: #C0C022;
However, if you want to draw any patterns in there, you currently have to subclass.

Looking at the code for ScrawlGrid, we see that, in addition to the cross-hatch pattern for rollover and selection, there are other things that require overriding DataGrid:

(1) Drawing the column separator: ScrawlGrid overrides drawVerticalLine() to add some randomness so that the lines are not exactly vertical. To get the full effect, we also need to add the following graphical skin as a DataGrid style:
    header-separator-skin: Embed('assets/header_separator.gif'); /* transparent */
Strange but true: the header separator is skinnable, but the column separator as a whole is not.

(2) Drawing a diagonal line fill in the header: ScrawlGrid overrides drawHeaderBackground() -- once again, flex makes it easy to specify a solid background color for the header, but you have to subclass in order to draw any patterns.

We now get to the most difficult part of creating this theme: drawing rollover and selection indicators in the DataGrid headers. In DataGrid, the drawing code for rollover and selection for headers (but not for the DataGrid's contents) is hidden inside mouseOverHandler() and mouseDownHandler(). So we need the hack of overriding mouse handlers in order to change a purely visual part of the app:
    override protected function mouseDownHandler(event:MouseEvent):void
headerRendererHack(event, getStyle("selectionColor"));
Note the call to super.mouseDownHandler() -- we can't just ignore the superclass' mouse handling code the way we did with the draw***() methods. headerRendererHack() draws the cross-hatch pattern in the renderer for the correct header -- but there's a twist to finding that renderer. It turns out that mouseEventToItemRenderer() will return the correct header renderer except for the case where the mouse is over the sort arrow's hit area. What we want in that case is the renderer for the header of the column being sorted. getColumnHeaderRenderer() finds that renderer for us.

That's it! (whew!) We now have reached the final version of the styled, skinned and tweaked Napkin app. Hopefully the tweaking needed at this stage will decrease over time as it is replaced with a more versatile use of programmatic and even graphical skins.


Anonymous said...

This is excellent work, my friend. Your success with quickly skinning a napkin is nothing short of inspiring. I look forward to reading your fresh works in the future.

Anonymous said...

Thks a lot for your work! Now i understand a little bit more the powrfull and complex of skins. Gracias.

Anonymous said...

This is a good read. :)

Anthony said...

Thanks for the tutorial. It was very helpful.

Roman Protsiuk said...

The best skinning-related blog I've ever read. Thanks a lot.

Anonymous said...

These comments have been invaluable to me as is this whole site. I thank you for your comment.

Anonymous said...
This comment has been removed by the author.
Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...

This is a super cool tutorial. However, I'm extremely bummed that Flex 3 and the override for drawHeaderBackground() won't fire. Looks like it works for Flex 2 fine.

Any insights for Flex 3 anyone?

Anonymous said...

We're having having the same issues in Flex 3. This tutorial is amazing and would normally solve all of our styling issues, however Flex 3 seems to have put a rather unfortunate roadblock in the way. We're trying to find a workaround, if we come up with anything I'll post it here.

Unknown said...

Excellent Work. I was very much impressed seeing the application. But i have a simple requirement where in i need to change the header color of the sorted column in the datagrid. I think you can do it with in seconds :) Please help me in doing so.
Thanks in advance