Thursday, January 25, 2007

Napkin skins, stage two: programmatic skins

We can build on stage one by using programmatic skins (click on the screenshot to see the app):

There were no significant changes to the MXML file: the only change is the reference to the CSS stylesheet. Looking at scrawl3.css (when you select View Source in the app's context menu), we see that the changes to the stylesheet are limited to styles that are specified as ClassReferences, for example:
    track-skin: ClassReference('skins.scrawl.ScrollTrackSkin');
These are references to programmatic skins written in ActionScript, which you can find in the source tree under skins.scrawl. The simplest one, ScrollTrackSkin.as, specifies the skin for the ScrollBar's track (but not for the thumb or the arrow). The drawing happens in the overridden method updateDisplayList() and, aside from a drawing a simple border around the track, does only one interesting thing:
    // fill with zero alpha: if no fill, mouse events are not recognized
drawRoundRect(1, 1, w-2, h-2, 0, 0x000000, 0.0);
I found that, without this line, clicking on the track does not move the ScrollBar thumb to the location of the click -- the mouse event is ignored, presumably because it doesn't hit any area drawn by the track skin. My solution for that was to add an invisible background for the track. I have also used this solution in my other programmatic skins, but I am not sure that this is the "recommended" way to address this issue.

Another simple programmatic skin is ScrollArrowSkin.as. Someone with a different balance of graphical vs. coding skills would have probably created this as a graphical skin. For me, this was the simplest way to go. One advantage of using the programmatic skin is that you can make slight variations for up-/over-/down-skins within the same skin class. In this case, I have used conditions on the skin's name to determine the color and direction of the arrow, which allowed me to use the same skin class for all ScrollBar arrow skins:
    down-arrow-down-skin: ClassReference('skins.scrawl.ScrollArrowSkin');
down-arrow-over-skin: ClassReference('skins.scrawl.ScrollArrowSkin');
down-arrow-up-skin: ClassReference('skins.scrawl.ScrollArrowSkin');
...
up-arrow-down-skin: ClassReference('skins.scrawl.ScrollArrowSkin');
up-arrow-over-skin: ClassReference('skins.scrawl.ScrollArrowSkin');
up-arrow-up-skin: ClassReference('skins.scrawl.ScrollArrowSkin');
Programmatic skins also allow for some nifty tricks. The code in ComboBoxSkin.as that draws the border of the ComboBox uses Math.random() to draw it differently every time:
    g.moveTo(Math.random() * borderThickness, Math.random() * borderThickness);
g.lineTo(w - (Math.random() * borderThickness), Math.random() * borderThickness);
g.moveTo(w - (Math.random() * borderThickness), Math.random() * borderThickness);
g.lineTo(w - (Math.random() * borderThickness), h - (Math.random() * borderThickness));
...
This helps give the ComboBox that ragged scrawled-on-a-napkin look: the border is different for the up-, over- and down-skins. If you play with the box dividers, you'll see that the ComboBox redraws itself with a slightly different border, even for the same skin. A similar principle is at work for the DataGrid's border (in ScrawlBorder.as), as well as for the cross-hatch pattern for the ScrollBar thumb skin (ScrollThumbSkin.drawCrossHatch()) and the ComboBox's down-skin (ScrawlUtil.drawCrossHatch() in the util package):
    // draw \\\\\\\\
for(i = -h; i < w; i += 6)
{
vstart = Math.max(-i, 0);
vdistance = Math.min(h, w - i);
vstartOffset = vstart + 3 - (6 * Math.random());
g.moveTo(i + vstartOffset, vstartOffset);
g.lineTo(i + vdistance, vdistance + 2 - (4 * Math.random()));
}

// draw ////////
for(i = 0; i < w + h; i += 6)
{
vstart = Math.max(i - w, 0);
vdistance = Math.min(h, i);
vstartOffset = vstart + 3 - (6 * Math.random());
g.moveTo(i - vstartOffset, vstartOffset);
g.lineTo(i - vdistance, vdistance + 2 - (4 * Math.random()));
}
Once again, programmatic skins allow us to call this code only for the ComboBox's down-skin while reusing the same class for all three skins:
    down-skin: ClassReference('skins.scrawl.ComboBoxSkin');
over-skin: ClassReference('skins.scrawl.ComboBoxSkin');
up-skin: ClassReference('skins.scrawl.ComboBoxSkin');
As one final touch, the DataGrid's border looks like it was drawn with a marker, with varying pressure on the marker producing color variations along the way. This effect is accomplished by specifying the following gradient type in ScrawlBorder.updateDisplayList():
    g.lineGradientStyle(GradientType.LINEAR, [borderColor, borderColor], [1.0, 0.5], [0, 255],
null, SpreadMethod.REFLECT);

10 comments:

Daniel Wanja said...

That's so cool. I use Flex to do application prototypes as it's the fastest way out there to assemble a cool UI. Now with that skin people will realize that's just a prototype. But then again it's so cool I will use it for my own apps. Thanks for the article.

Unknown said...

That's pretty neat... good job.

I found a bug for you:
resize the up/down separator enough to cause the combo box to expand up and collapse down. (Test the combo box.) Then resize the up/down separator back to the original position or so (so the combo box opens down). Test the combo box. It expands down but then collapses by moving down.

Okay, so big deal I found a bug--I do every day in my code. My big question is what exactly is involved if you were to fix this? Is the error in your code or hidden in the Flex framework? I seriously want to know what the approach is. Thanks.

Anonymous said...

This is awesome. Great job, and very clever.

Eylon said...

I can reproduce the combo box bug in the original version of the sample app, prior to any of my skinning. Thanks for reporting the bug! I'll file it against the Flex framework.

Unknown said...

Okay, so it sounds like a bug in Flex Framework... so, what would you do in a real job? I'm seriously curious because I'm tempted to use Flex on some jobs but my biggest fear is just this--you find a bug that's not yours... then what? Is this an easy thing to get in and modify? Thanks.

Eylon said...

I think the bug involves several private fields in ComboBox, so it doesn't look like a workaround outside of the framework (say, in a subclass) is trivial. If you run into a problem like this, though, flexcoders is a good place to post your question -- it will likely be answered by someone who knows more about Flex than I do!

Unknown said...

That's interesting--I don't mean to critisize you or your work because this is the coolest Flex app I've seen... even if only a demo.

I also don't want to make a huge deal out of this particular bug--but it's by far my biggest concern buying into Flex. I have very few clients who would accept that sort of bug. The point being, are you just stuck if you encounter something like this? Is the framework nearly perfect and I just happen to find the one obscure bug (I doubt that). What would you (or others) do in this case in a real project. Surely you can't just say, "oh, it's Flex bug"

James Ward said...

Phillip,
Here are 3 possible ways that you would be able to work around the bug:
1) You fix it yourself. The code for the Flex framework is distributed with the SDK, so usually this isn't too hard.
2) You have a support contract and you get support to help you fix it.
3) Someone on flexcoders helps you fix it.

With Flex 1.x we didn't have the Flex code, so #1 wasn't an option. Since Flex 2 ships with the framework code, it's usually much easier to deal with these types of problems.

Unknown said...

Okay, the fact the code is there is pretty much all I suppose. I'm curious about the support concept. I would have thought a support contract would be to help you figure out how to use the framework or to point you in the direction for adding features not included. In this case, it's a bug that one would think gets fixed regardless. Again, I'm not saying this is the most critical bug of all time--but I suspect you would run into this sort of thing at some interval during every project and I was just curious what people do. It's also the main reason I never use the components that ship with Flash (with exceptions). Thanks!

------ said...

Awesome