Saturnboy
 9.20

Flex 4 introduced an awesome new skinning architecture. Among other things, the new architecture provides significantly better separation between a component and its skin. Flex 4 also promotes the use of states to the point where they are virtually mandatory in any non-trivial app. And that brings us to the question of the day:

How do you communicate state information from the host component down to its skin?

As always, we’ll dive into some examples to explore how things work. In our first example, we just want our skin to mirror the states of its host component. So, we begin with a simple component based on SkinnableComponent. And then we add three states: base, happy, sad. The code:

<?xml version="1.0" encoding="utf-8"?>
<s:SkinnableComponent
        xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        skinClass="skins.SmileySkin">
 
    <s:states>
        <s:State name="base" />
        <s:State name="happy" />
        <s:State name="sad" />
    </s:states>
</s:SkinnableComponent>

Right now our component is just a bag of states. But going forward, we know we want to skin it with skin that has same three states. So we force the skin state to mirror the host state with three simple steps:

  1. SkinState – Add SkinState metadata to the host component.
  2. getCurrentSkinState() – Override the getCurrentSkinState() method to return the host’s currentState. (This is the mirror!)
  3. invalidateSkinState() – Force the skin state to update by calling invalidateSkinState() every time the host’s state changes.

Implementing these changes, we arrive at:

<?xml version="1.0" encoding="utf-8"?>
<s:SkinnableComponent
        xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        skinClass="skins.SmileySkin">
 
    <fx:Metadata>
        [SkinState("base")]
        [SkinState("happy")]
        [SkinState("sad")]
    </fx:Metadata>
 
    <fx:Script>
        <![CDATA[
            override protected function getCurrentSkinState():String {
                return currentState;
            }
        ]]>
    </fx:Script>
 
    <s:states>
        <s:State name="base" enterState="invalidateSkinState()" />
        <s:State name="happy" enterState="invalidateSkinState()" />
        <s:State name="sad" enterState="invalidateSkinState()" />
    </s:states>
</s:SkinnableComponent>

Step 1 is straightforward. In Step 2, we override and return currentState. And Step 3 is achieved by calling invalidateSkinState() directly in each state’s enterState event handler. So to answer our original question: skin state is literally communicated from the host component down to the skin via the getCurrentSkinState() method. If we pass in currentState then we get a perfect state mirror (aka skin state = host component state).

We finish off our example app by adding a silly yellow smiley face skin and a simple main app to hold our component. Here it is (view source enabled):

Flash is required. Get it here!

Use the ButtonBar to change the component’s state. Check the source to see the trivial FXG used to draw the smiley face.

ItemRenderer States

All of this has been leading up to my typical case. I have a nice custom component with a nice skin (maybe I built my component and skin in Flash Catalyst or maybe I built them by hand). Then I suddenly realize, Damn!, I need to use my component in an ItemRenderer. So how do I go about hooking up the standard ItemRenderer states (normal, hovered, selected) to my custom component?

Before I get to the answer, let’s rewind a little bit to set the scene. Imagine I have a component that displays a grade, and I want to display a different graphical treatment depending on the grade. The Flex implementation is the standard stateless custom component with a multi-state skin (Button is a good example). A little different that our first example, but our component still need to communicate skin state information down to its skin.

Our grade component:

<?xml version="1.0" encoding="utf-8"?>
<s:SkinnableComponent
        xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        skinClass="skins.GradeSkin">
 
    <fx:Metadata>
        [SkinState("A")]
        [SkinState("B")]
        [SkinState("C")]
    </fx:Metadata>
 
    <fx:Script>
        <![CDATA[
            private var _grade:int = 0;
 
            [Bindable]
            public function get grade():int {
                return _grade;
            }
            public function set grade(value:int):void {
                _grade = (value > 100 ? 100 : value < 0 ? 0 : value);
                invalidateSkinState();
            }
 
            override protected function getCurrentSkinState():String {
                return (grade >= 90 ? 'A' : grade >= 80 ? 'B' : 'C');
            }
        ]]>
    </fx:Script>
</s:SkinnableComponent>

We followed a similar set of three steps to create this component. Step 1 is still “add SkinState metadata”. Step 2 is still “override getCurrentSkinState()” but this time we return a state based on the value of grade. Step 3 is still “force the skin state to change by calling invalidateSkinState()” but this time we call it in the setter method for grade. These three steps give us exactly what we want: a host component without any states that changes its skin state depending on the value of grade.

The code for our three state skin is here:

<?xml version="1.0" encoding="utf-8"?>
<s:Skin ...>
 
    <fx:Metadata>
        [HostComponent("components.Grade")]
    </fx:Metadata>
 
    <s:states>
        <s:State name="A" />
        <s:State name="B" />
        <s:State name="C" />
    </s:states>
 
    <!-- gold star -->
    <s:Path scaleX="0.2655" scaleY="0.2655" includeIn="A">
        ...
    </s:Path>
 
    <!-- pink circle -->
    <s:Ellipse left="0" right="0" top="0" bottom="0" includeIn="B">
        ...
    </s:Ellipse>
 
    <!-- blue square -->
    <s:Rect left="0" right="0" top="0" bottom="0" includeIn="C">
        ...
    </s:Rect>
 
    <s:SimpleText text="{hostComponent.grade}" ... />
</s:Skin>

We define our three states and then use a different FXG shape for each state. A is a gold star, B is a pink circle, and C is a blue square. We also display the number grade via simple binding to hostComponent.grade. Alternately, we could have used a SkinPart to pass the grade information down to the skin (here is a good example), but it costs us complexity so I went with the simple binding instead.

Where I Say, Damn!, ItemRenderer

So, I need to use my nice component in an ItemRenderer. Damn!, I say. As a first attempt, we just dump our custom component inside an inline ItemRenderer, like this:

<?xml version="1.0" encoding="utf-8"?>
<s:Application ... >
 
    <s:List left="10" top="10" width="400" height="95">
        <mx:ArrayCollection source="[95,85,75,5,101,90,80,-1]" />
 
        <s:itemRenderer>
            <fx:Component>
                <s:ItemRenderer>
                    <s:states>
                        <s:State name="normal" />
                        <s:State name="hovered" />
                        <s:State name="selected" />
                    </s:states>
 
                    <comps:Grade grade="{data}" />
                </s:ItemRenderer>
            </fx:Component>
        </s:itemRenderer>
 
        <s:layout>
            <s:HorizontalLayout ... />
        </s:layout>
    </s:List>
</s:Application>

We get a nice app, but it obviously doesn’t respond to any of the ItemRenderer’s states. Here is attempt #1 (view source enabled):

Flash is required. Get it here!

For attempt #2, we can respond to the states directly by adding some code inside the inline ItemRenderer. What code? Well, filters are the prefect fit in this case. They provide a nice visual effect (not easily mimicked by JavaScript), and they are very easy to wrap around a component in a non-invasive manner. Here is the code:

<s:ItemRenderer>
    <s:states>
        <s:State name="normal" />
        <s:State name="hovered" />
        <s:State name="selected" />
    </s:states>
 
    <comps:Grade grade="{data}">
        <comps:filters>
            <s:GlowFilter color="#000000" includeIn="hovered"
                     alpha="0.5" blurX="8" blurY="8" />
            <s:DropShadowFilter includeIn="selected"
                     alpha="0.5" blurX="8" blurY="8" />
        </comps:filters>
    </comps:Grade>
</s:ItemRenderer>

I love filters, and using them can often save the day and avoid some implementation pain. But sometimes, it’s just not enough. Sometimes, you absolutely must have your custom component (and it’s skin) respond to the ItermRenderer’s states. In those cases, you just need get a little dirty…

For attempt #3 (our final attempt), we need to communicate the ItemRenderer’s state down to our custom component. The easiest thing to do is bind our custom component’s state to the ItemRenderer’s state. A simple currentState = "{this.currentState}" does the trick. Here is the refactored inline ItemRenderer code:

<s:ItemRenderer>
    <s:states>
        <s:State name="normal" />
        <s:State name="hovered" />
        <s:State name="selected" />
    </s:states>
 
    <comps:Grade grade="{data}" currentState="{this.currentState}" />
</s:ItemRenderer>

Now we must add three new states to our custom component, and communicate those states down to our skin. Again, we revisit our favorite three steps and refactor our custom component like this:

<?xml version="1.0" encoding="utf-8"?>
<s:SkinnableComponent
        xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        skinClass="skins.GradeSkin">
 
    <fx:Metadata>
        [SkinState("Anormal")]
        [SkinState("Ahovered")]
        [SkinState("Aselected")]
        [SkinState("Bnormal")]
        [SkinState("Bhovered")]
        [SkinState("Bselected")]
        [SkinState("Cnormal")]
        [SkinState("Chovered")]
        [SkinState("Cselected")]
    </fx:Metadata>
 
    <fx:Script>
        <![CDATA[
            ... getter and setter for grade, same as before ...
 
            override protected function getCurrentSkinState():String {
                return (grade >= 90 ? 'A' : grade >= 80 ? 'B' : 'C') + currentState;
            }
        ]]>
    </fx:Script>
 
    <s:states>
        <s:State name="normal" enterState="invalidateSkinState()" />
        <s:State name="hovered" enterState="invalidateSkinState()" />
        <s:State name="selected" enterState="invalidateSkinState()" />
    </s:states>
</s:SkinnableComponent>

In Step 1, we added a bunch more SkinState metadata directives. In Step 2, we simply appended currentState to the string returned by the overridden getCurrentSkinState() method. The result is 3×3 = 9 skin states (added in Step 1). In Step 3, we added three new states (mirroring the ItemRenderer’s states) to our component. And in each enterState event handler, we call invalidateSkinState(). There are, of course, other ways to do it. But to me it makes the most conceptual sense to add the new states to our custom component, and then pass them down to the skin.

Lastly, we refactor our skin to include all 9 states and respond to them graphically:

<?xml version="1.0" encoding="utf-8"?>
<s:Skin ...>
 
    <fx:Metadata>
        [HostComponent("components.Grade")]
    </fx:Metadata>
 
    <s:states>
        <s:State name="Anormal" stateGroups="A, normal" />
        <s:State name="Ahovered" stateGroups="A, hovered" />
        <s:State name="Aselected" stateGroups="A, selected" />
        <s:State name="Bnormal" stateGroups="B, normal" />
        <s:State name="Bhovered" stateGroups="B, hovered" />
        <s:State name="Bselected" stateGroups="B, selected" />
        <s:State name="Cnormal" stateGroups="C, normal" />
        <s:State name="Chovered" stateGroups="C, hovered" />
        <s:State name="Cselected" stateGroups="C, selected" />
    </s:states>
 
    <s:Path scaleX="0.2655" scaleY="0.2655" includeIn="A">
        <s:data>...</s:data>
        <s:stroke>
            <s:SolidColorStroke color="#000000" />
        </s:stroke>
        <s:fill>
            <s:RadialGradient>
                <s:GradientEntry color="#FFCC00" ratio="0" />
                <s:GradientEntry ratio="1"
                    color="#FFCC00"
                    color.Ahovered="#CC0066"
                    color.Aselected="#660033" />
            </s:RadialGradient>
        </s:fill>
    </s:Path>
 
    ...
</s:Skin>

The first thing to note is the use of stateGroups. And all this time you thought they were useless? Here they enable us to split our combined states into their component parts, giving a simpler and more understandable set of states: A, B, C, normal, hovered, selected. In the code, we preserve the original grade to shape mapping (A is still a gold star, etc.), but we’ve added code to respond to the ItemRenderer states via a color change.

It is more interesting to see the result. So, here is attempt #3 (view source enabled):

Flash is required. Get it here!
Conclusion

Remember the three steps to achieve host component to skin state communication and you will live forever in skinning nirvana:

  1. SkinState
  2. getCurrentSkinState()
  3. invalidateSkinState()
Files

NOTE: All code was built with Flash Builder 4 Beta 1.




 6.1

The plan is simple, take the nice Degrafa-skinned components from Part 1 and assemble them into a video player powered by the OvpNetStream class from the Open Video Player project.

Design

I knew right away that the design was not going to have any right angles, but I also didn’t want to go with rounded rectangles everywhere. Modern TVs tend to have a lot of soft rounded edges, so I decided to go with a more vintage look. So I fired up Inkscape and got to work:

tv

Implementing the video player design above in Degrafa, the cabinet mapped to a RoundedRectangle, and the screen & antenna became Paths. You can read more about about translating SVG to Degrafa in my Inkscape SVG to Degrafa Path article. But for now, let’s focus on the resulting Degrafa code:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application
        xmlns:mx="http://www.adobe.com/2006/mxml"
        xmlns:Degrafa="http://www.degrafa.com/2007"
        layout="absolute">
 
    <Degrafa:GeometryComposition graphicsTarget="{[box]}">
        <!-- Antenna -->
        <Degrafa:Path data="...path data...">
            <Degrafa:transform>
                <Degrafa:TranslateTransform x="15" />
            </Degrafa:transform>
        </Degrafa:Path>
 
        <!-- TV cabinet -->
        <Degrafa:RoundedRectangle y="144" width="350" height="300"
                cornerRadius="20" />
 
        <Degrafa:fill>
            <Degrafa:SolidFill color="#333333" />
        </Degrafa:fill>
        <Degrafa:stroke>
            <Degrafa:SolidStroke color="#FF00FF" weight="4" alpha="0.4" />
        </Degrafa:stroke>
    </Degrafa:GeometryComposition>
 
    <!-- TV Screen -->
    <Degrafa:GeometryComposition graphicsTarget="{[tvscreen]}">
        <Degrafa:Path data="...path data...">
            <Degrafa:fill>
                <Degrafa:SolidFill color="#FF99FF" />
            </Degrafa:fill>
            <Degrafa:stroke>
                <Degrafa:SolidStroke color="#FF00FF" weight="2" alpha="0.4" />
            </Degrafa:stroke>
            <Degrafa:filters>
                <mx:GlowFilter color="#EEEEEE" alpha="0.2" blurX="16" blurY="16" />
            </Degrafa:filters>
        </Degrafa:Path>
    </Degrafa:GeometryComposition>
 
    <mx:Canvas width="350" height="444"
            horizontalCenter="0" verticalCenter="0">
        <mx:Canvas id="box" />
        <mx:Canvas x="50" y="174" id="tvscreen" />
    </mx:Canvas>
</mx:Application>

I ended up using a pair of GeometryCompositions to wrap my three shapes to help keep my fills, strokes, and filters organized. It made sense to do it this way, but I won’t claim it’s the best way. Throw the control bar on below the TV screen, and the design is done.

Backend

The backend is build on the OvpNetStream class provided by the Open Video Player project. OvpNetStream extends NetStream and smooths out some of the rough edges as I discussed previously. Basically, it provides a sane interface (no need to construct a dynamic object with function callbacks) and useful events (like metadata and progress events).

For this demo, we simply instantiate OvpNetStream on creationComplete and wire up all the event handlers:

private function complete():void {
    nc = new OvpConnection();
    nc.addEventListener(OvpEvent.ERROR, errorHandler);
    nc.addEventListener(NetStatusEvent.NET_STATUS, connStatusHandler);
    nc.connect(null);
 
    ns = new OvpNetStream(nc);
    ns.addEventListener(OvpEvent.ERROR, errorHandler);
    ns.addEventListener(NetStatusEvent.NET_STATUS, streamStatusHandler);
    ns.addEventListener(OvpEvent.NETSTREAM_METADATA, streamMetadataHandler);
    ns.addEventListener(OvpEvent.PROGRESS, streamProgressHandler);
    ns.addEventListener(OvpEvent.COMPLETE, streamCompleteHandler);
 
    vid = new Video();
    vid.attachNetStream(ns);
    vidContainer.addChild(vid);
}

The most interesting events are the metadata and progress events. The metadata event delivers the duration of the video and its size. The progress event arrives periodically (theoretically every 100ms by default, but in reality I see them come in just a couple of times per second) and delivers the current video time.

Control Bar

The control bar consists of three components: a play-pause button, a scrubber, and a volume slider. They were skinned using Degrafa in Part 1. In order to control video playback, we need to wire the control bar components to the instance of OvpNetStream created above.

Here are the event handlers for the three control bar components:

// PlayPause event handler
private function playPauseClick():void {
    if (first) {
        first = false;
        ns.play(filename);
    } else {
        ns.togglePause();
     }
}
 
// Scrub event handlers
private function scrubPress():void {
    ns.pause();
    playPause.selected = false;
}
private function scrubDrag():void {
    ns.seek(scrub.value);
}
private function scrubRelease():void {
    ns.togglePause();
    playPause.selected = true;
}
 
// Volume event handler
private function volumeChange():void {
    ns.volume = volume.value;
}

In the playPauseHandler(), the initial click calls play() which actually loads the video (and then starts playback), all subsequent clicks just toggle between play or pause. For the scrubber handlers, I chose to break them up into three separate steps: on mouse down pause the video, on mouse up restart playback, and on drag attempt to seek to the to the new time.

That’s it. Here is the resulting Degrafa-skinned video player (view source enabled):

Flash is required. Get it here!

Click Play to start playing Elephants Dream (which is the “world’s first open movie,” and pretty cool too). Right away you’ll notice some visual issues because the dimensions of the video are unknown until the metadata arrives. Also, scrubbing has some problems which I believe are related to cue points in progressive downloads. Lastly, I didn’t implement any indicators for buffering or download progress, so you’ll need to be patient. Since this is just a demo, I’ll have to leave fixing those bugs as an exercise for the reader.

Files

 5.26

I’m going to combine my earlier Degrafa skinning efforts with my more recent video work to create a Degrafa-skinned video player. In this post, I’ll build out all the components required for a video player control bar. And in Part 2, I’ll weld the control bar to an Open Video Player OvpNetStream backend.

Button

To get started, I built out a simple button skin with a slight shine on the top half. I also added rounded corners to give the classic web 2.0 look-and-feel. There’s really not much to look at:

Flash is required. Get it here!
Play-Pause Button

I’m using a toggleable Button component as the base of my Play-Pause button. By leveraging Degrafa’s stateful skins, I simply change the visibility of the Play and Pause geometry depending on the skin’s state. Here’s a snippet from the states block of the skin:

<states>
    <State name="upSkin">
        <SetProperty target="{rect}" name="fill" value="{upFill}"/>
        <SetProperty target="{play}" name="visible" value="true"/>
    </State>
    <State name="selectedUpSkin">
        <SetProperty target="{rect}" name="fill" value="{upFill}"/>
        <SetProperty target="{play}" name="visible" value="false"/>
    </State>
    ...
</states>

Additionally, the play arrow and pause bars are dynamically sized based on the button height, so they readily scale to any size. The result:

Flash is required. Get it here!
Scrubber

Getting total control over a Slider skin is not easy in Flex 3. There are two issue that I find quite annoying: the thumb size is hard-coded into the class and the highlight skin is offset from the track. Fortunately, the web is full of workarounds for the thumb size issue, but for this skin I only needed to hard-code an offset (+6,+6) to bring the thumb’s origin to (0,0) and make all my geometry line up correctly. Similarly, I added a 1 pixel offset to the highlight skin to bring its origin to (0,0) to match the track. The result:

Flash is required. Get it here!
Volume Slider

For consistency, the Volume slider re-uses the Scrubber’s thumb skin, but the track and highlight are wedges (Polygon’s in Degrafa). The only clever piece of code is the highlight skin’s overridden updateDisplayList() function:

override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
    super.updateDisplayList(unscaledWidth, unscaledHeight);
    awidth = unscaledWidth;
    aheight = unscaledHeight;
 
    if (this.hasOwnProperty('parent') &&
             this.parent.hasOwnProperty('parent') &&
             this.parent.parent.hasOwnProperty('width') &&
             this.parent.parent.width > 0) {
        _ratio = awidth / this.parent.parent.width;
    } else {
        _ratio = 0.00001;
    }
}

In order to correctly draw the highlight wedge, we first need to know the dimensions of the track. For simplicity, the Volume track height is hard-coded in both the track skin and highlight skin, but the width of the track is variable. In this case, the width of the track is the same as the width of the HSlider component itself. So in the updateDisplayList() code above, the track width is found and used to compute the ratio of widths. This ratio is later used to draw the highlight geometry. The end result:

Flash is required. Get it here!
The Full Control Bar

Just displaying all the components side-by-side gives us the beginnings of a video player control bar:

Flash is required. Get it here!

Not too shabby. In Part 2, I’ll assemble the final video player frontend including the pretty control bar components above, and weld it to an OvpNetStream-powered backend.

Files

 4.6

Before I dig too deep trying to create a Degrafa Skin Explorer (the idea) as an evolution of the Degrafa Button Explorer, I thought it would be a good idea to spend some more time with some basic Degrafa skinning. I managed to create a few button skins previously, but this time around I wanted something more involved.

ButtonBar

First, I built out a pair of ButtonBar skins (view source enabled):

Flash is required. Get it here!
ToggleButtonBar

Next, I augmented the my basic ButtonBar skins with the various required selected skins (selectedUpSkin, selectedDownSkin, etc.) to create a pair of ToggleButtonBar skins (view source enabled):

Flash is required. Get it here!
iPhone Theme

Lastly, I dissected the iPhone-esque theme sample from the Degrafa Samples page, and created my own bastardized iPhone theme. I factored out a theme color from each component into a CSS style called mainColor to allow the skin to be tweaked. Check it out (view source enabled):

Flash is required. Get it here!
Conclusions

I learned a couple of things during my skinning exercise, but probably the most important came to me in an amazing “A-Duh” moment: Degrafa is really powerful. This is probably the same as saying MXML is a high-level language with a lot of leverage, but the Degrafa team has done a great job bringing this leverage to bear on skinning and graphics in Flex. At this point, I find it hard to imagine creating a style without it.

Secondly, semantic naming is a must inside a stateful skin. So, I replaced any names like darkFill or lightStroke, with nice semantic names like overFill and selectedStroke. So if I look at a chunk of code like this:

<State name="downSkin">
    <SetProperty target="{rect}" name="fill" value="{overFill}" />
</State>

I can immediately know that my downSkin state is using a fill from my overSkin state, and probably implies the states are the same.

Lastly, always resist the temptation to re-use/re-purpose the CSS style from the underlying component in a skin. For example, the iPhone theme above uses a theme color which I intentionally named mainColor. I could easily have used Button’s aptly-named themeColor style (inherited down from UIComponent). But this would dangerously overload the meaning of themeColor, because while it normally controls the Button’s border color it would now control the body color in my iPhone button skin.

Files

 3.27

Recently, I’ve been playing around with Degrafa in preparation for actually using it on an upcoming project at work. For those that aren’t in the know, Degrafa is a badass graphics framework for Flex. One of my favorite parts so far is the amazing leverage it brings to building custom-styled components. It’s terse and powerful, just the way I like it.

Building Buttons

With a very minimal amount of effort, I was able to create a few “web 2.0″ button skins. I just followed one of the millions of glass button photoshop tutorials, but did the work in mxml code using Degrafa’s declarative syntax. Here are my results (view source enabled):

Flash is required. Get it here!
Button Explorer

But then I decided to take it a step further and build out a Degrafa Button Explorer (with a small tip of the hat to the original Flex Style Explorer). The Degrafa Button Explorer allows you to adjust a bunch of properties and colors for all three buttons styles. It goes a step further than the original and provides not only the CSS, but also the full code of the stateful Degrafa-powered button components.

 »  Degrafa Button Explorer

Digging Deeper: I’ve been following the skinning performance issues discovered by the guys at EffectiveUI. The most recent benchmarks appear to show that stateful Degrafa skins (like those produced by the Degrafa Button Explorer) are safe to use.


© 2010 saturnboy.com