Saturnboy
 9.20

Flex 4 Component States vs. Skin States

,

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.




Comments

11.3.2009

1

Great post! Thanks, Justin!

Now I just have to figure out how to get enterState syntax on an AS only component.

11.3.2009

2

I love MXML, especially with the advances made in Flex 4. But if you have to go with AS3, just listen for the FlexEvent.ENTER_STATE event:

var s:State = new State();
s.name = 'foo';
s.addEventListener(FlexEvent.ENTER_STATE, enterStateHandler);

11.16.2009

3

This is great post! I used this technique on article about different solutions for skinning in Flex 4 here: http://blog.paveljacko.com/?p=29

6.23.2010

4

Yes, thank you Justin especially for that last step, where you compound the states.

I do all components in AS3 and only the skins in MXML. Extend SkinnableComponent and override at most seven methods:

  • set – set property and call invalidateProperties()
  • invalidateProperties() – say properties have changed
  • commitProperties() – act on combination of properties
  • invalidateDisplay() – if property change should change display
  • updateDisplayList() – only if you must intrude on layout…
  • getCurrentSkinState() – when skin needs to know what to show…
  • partAdded() – watch for parts you need to initialize (addEventListener, etc)
  • partRemoved() – clean up whatever partAdded() did

John Hicks

6.24.2010

5

@John – yes, nowadays, I also tend to write my custom components in AS3 rather than MXML for most cases. But I’ll still use MXML when my custom component contains a bunch of other components (thereby leveraging the power of MXML to add and layout components).

And yes, compound skin states are cool. I just wrote a custom button with:

return super.getCurrentSkinState() + (alerted ? 'AndAlerted' : '');

So my states where: up, over, down, disabled and upAndAlerted, overAndAlerted, downAndAlerted, disabledAndAlerted.

Jake

8.15.2010

6

Great article. Clear and comprehensive, thanks man!

Joe Konczal

9.23.2010

7

Thanks for all the clear, concise, and useful articles. There is one thing that bothers me, though, in the code for this one. In the smileys example, all the states are enumerated in four different places. In the grades examples there are even more lists of states. Is there a way to reduce the number of places where the states need to be listed?

9.23.2010

8

@Joe: Alas, because we have both “component” states and “skin” states, it can look like we are repeating ourselves, but you are seeing the minimum.

In Smiley we have 3 SkinStates and 3 States, I just made the names the same. It would be nice if State had some attribute to promote the component state to a skin state. Then I could just say isSkinStateToo="true".

Boris

10.8.2010

9

Hi, great article! Opened my eyes for some possibilities. However when I use the approach described by you, the flex compiler complains that required states ‘normal’ and ‘disabled’ are missing from the host component. Any ideas how to fix this issue?

zvi

10.27.2010

10

you really helped me SO much!

Dave

11.8.2010

11

In your step #3, instead of repeating invalidateSkinState() on every State, I used the CURRENT_STATE_CHANGE event to invalidate the skin state.

This can be baked into the underlying component, like this:

public function MyComponent() extends SkinnableComponent {
 
    public function MyComponent() {
        addEventListener(StateChangeEvent.CURRENT_STATE_CHANGE, onCurrentStateChange);
    }
 
    private function onCurrentStateChange(event:StateChangeEvent):void {
        invalidateSkinState();
    }
}

Now if you just use MyComponent everywhere, its SkinState will be invalidated every time its State changes. Easy.

11.8.2010

12

@Dave: Perfect. Good suggestion!

Rakesh

6.23.2012

13

thanks…now i understood easily..

12.19.2012

14

really helpful article over understanding Flex component completely.

6.11.2013

15

Do you have any example for horizontal scroll bar example ?

I am using Flex 4.6 and my application is in AIR.

© 2017 saturnboy.com