Saturnboy
 6.1

I needed a good way to have a large settings panel with a minimal visual impact. The obvious answer is to hide or minimize or collapse the settings panel when not in use. I thought about using Flexlib‘s WindowShade component (which I’ve dicussed in detail in Styling Flexlib’s WindowShade), but why reuse something when you can reinvent the wheel? Plus, it always helps to hone my Flex 4 custom component kung-fu. So, I chose to implement a simple sliding drawer component from scratch as a Flex 4 component.

The Drawer component is a vanilla container (it actually extends SkinnableContainer), so it will happily take any spark component for its children. Before I dive into my implementation, let’s check out the finished drawer component in action (view source enabled):

Flash is required. Get it here!

Just click on the handle to open and close the drawer.

Extending SkinnableContainer

The Drawer component itself, is just pure AS3, but the demo above uses a few MXML skins to achieve the desired look-and-feel. This is a pretty standard pattern that I see during Flex 4 development, so expect it when you write your own custom components.

We’ll review the component implementation in two steps. First, we focus on the skin state management aspect of the drawer. Here’s the relevant code (taken from Drawer.as):

[SkinState("opened")]
public class Drawer extends SkinnableContainer {
    private var _opened:Boolean = false;
 
    public function get opened():Boolean {
        return _opened;
    }
 
    public function set opened(value:Boolean):void {
        if (_opened != value) {
            _opened = value;
            invalidateSkinState();
        }
    }
 
    override protected function getCurrentSkinState():String {
        return (opened ? 'opened' : super.getCurrentSkinState());
    }
    ...
}

The Drawer component can either be closed (the default) or opened. To model theses states, we use the normal state from the superclass to represent the closed drawer, and add a new opened SkinState to represent the opened drawer. We just expose a simple opened boolean property with a custom getter and setter, and then override the getCurrentSkinState() method. It’s important to remember the states we are talking about are skin states, and not component states (see Flex 4 Component States vs. Skin States for the difference).

Second, we focus on the action of opening and closing the drawer. Here’s the relevant code (taken from Drawer.as):

public class Drawer extends SkinnableContainer {
    ...
    [SkinPart(required="false")]
    public var openButton:Button;
 
    private function clickHandler(event:MouseEvent):void {
        opened = !opened;
    }
 
    override protected function partAdded(partName:String, instance:Object):void {
        super.partAdded(partName, instance);
        if (instance == openButton) {
            openButton.addEventListener(MouseEvent.CLICK, clickHandler);
        }
    }
 
    override protected function partRemoved(partName:String, instance:Object):void {
        super.partRemoved(partName, instance);
        if (instance == openButton) {
            openButton.removeEventListener(MouseEvent.CLICK, clickHandler);
        }
    }
}

The Drawer component includes a simple spark button, as an optional SkinPart, that is used to initiate the state change. The partAdded() and partRemoved() methods are overridden to manage and adding and removing of the button’s click event handler. And lastly, the clickHandler() method flips between skin states by toggling the opened boolean property.

Usage

Using the Drawer is the same as any container. In MXML, just put any child components you want between the container’s open and close tags:

<containers:Drawer ... skinClass="skins.DrawerSkin">
    <!-- components go here -->
</containers:Drawer>

Here we also apply the DrawerSkin to our container.

Skins

The DrawerSkin is responsible for creating the desired look-and-feel and generally making the Drawer component look cool. Here are the interesting parts of the skin:

<s:Skin ...>
 
    <fx:Metadata>
        [HostComponent("containers.Drawer")]
    </fx:Metadata>
 
    <s:states>
        <s:State name="normal" />
        <s:State name="opened" />
        <s:State name="disabled" />
    </s:states>
 
   ...
 
    <s:Button id="openButton" ...
            skinClass="skins.DrawerOpenButtonSkin"
            skinClass.opened="skins.DrawerCloseButtonSkin" />
 
    <s:Group id="contentGroup" ... includeIn="opened" />
</s:Skin>

The DrawerSkin has three skin states, normal and disabled are inherited from SkinnableContainer, but opened is our custom skin state. The skin also includes the optional openButton SkinPart, which itself uses two custom buttons skins, one for the open drawer and one for the closed drawer. Lastly, note that the container’s content is only displayed when the skin is in the opened state via the newfangled inline state syntax: includeIn="opened".

Files

 3.4

A while back I started digging into the WindowShade component in Flexlib. I really needed a set of cool collapsable buckets for a project at work, and WindowShade was perfect for the task. Alas, I couldn’t find too much info on styling a WindowShade other than Doug McCune’s awesome example of WindowShade and Degrafa. So, here is how I went about achieving the look and feel I needed with WindowShade.

Plain Vanilla

I started with an unstyled, super vanilla WindowShade wrapping a List. And of course, I get this super-vanilla output:

shade 1 (view source enabled)

The only styling magic was to add padding="0" to the WindowShade to get the child List component to suck up to the bottom of the WindowShade button.

My First Attempt

In the next pass, I ditched the lame WindowShade button, and went with Flexlib’s CanvasButton, which contains a simple Label plus a CheckBox skinned with a plus or minus graphic. The code:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application
        xmlns:mx="http://www.adobe.com/2006/mxml"
        xmlns:flexlibContainer="flexlib.containers.*"
        xmlns:flexlibControl="flexlib.controls.*"
        viewSourceURL="srcview/index.html">
 
    <mx:Script>
    <![CDATA[
        [Bindable]
        private var goodies:Array = [
            { header:'Ice Cream', items:['Vanilla', 'Chocolate', 'Strawberry', 'Cookies & Cream'] },
            { header:'Candy', items:['Twix', 'Snickers', 'Fire Balls', 'Hot Tamales', 'Mike & Ikes', 'Pez'] },
            { header:'Cookies', items:['Chewy Chips Ahoy', 'Mint Milano', 'Oreo', 'Nutter Butter', 'Fig Newtons'] }];
    ]]>
    </mx:Script>
 
    <mx:Style source="style.css" />
 
    <mx:VBox width="140" styleName="container">
        <mx:Repeater id="r" dataProvider="{goodies}">
            <flexlibContainer:WindowShade width="100%" styleName="shade"
                    data="{r.currentItem.header}"
                    opened="{r.currentIndex == 0}">
                <flexlibContainer:headerRenderer>
                    <mx:Component>
                        <flexlibControl:CanvasButton width="100%" height="30" styleName="shadeBtn">
                            <mx:Script>
                            <![CDATA[
                                import flexlib.containers.WindowShade;
                            ]]>
                            </mx:Script>
 
                            <mx:Label id="header" top="3" left="4" text="{WindowShade(parent).data}" styleName="shadeHead"/>
                            <mx:CheckBox id="chk" top="9" right="6" selected="{WindowShade(parent).opened}" styleName="shadeChk"/>
                        </flexlibControl:CanvasButton>
                    </mx:Component>
                </flexlibContainer:headerRenderer>
 
                <mx:List width="100%" dataProvider="{r.currentItem.items}" rowCount="{r.currentItem.items.length}" styleName="shadeList"/>
            </flexlibContainer:WindowShade>
        </mx:Repeater>
    </mx:VBox>
</mx:Application>

After throwing in some colors from a sweet Kuler theme and embedding Helvetica, we get this:

shade 2 (view source enabled)

At this point, I was really happy with look and feel, but there we still a few minor issues with the CanvasButton header that needed to be fixed before declaring victory.

Fixups

First, I needed the rollover event to flow down to the checkbox, so it would correctly show the overSkin on mouse over. Second, I wanted a color change on the Label on rollover to provide better visual feedback to the user. And lastly, I wanted the entire CanvasButton header to be clickable, not just the label text or the checkbox graphic.

Focusing just on the modified CanvasButton code, used in the headerRendered:

<flexlibControl:CanvasButton width="100%" height="30" styleName="shadeBtn"
        rollOver="header.setStyle('color', 0xffffff); chk.dispatchEvent(event);"
        rollOut="header.setStyle('color', 0xcccccc); chk.dispatchEvent(event);">
    <mx:Script>
    <![CDATA[
        import flexlib.containers.WindowShade;
    ]]>
    </mx:Script>
 
    <mx:Label id="header" top="3" left="4" text="{WindowShade(parent).data}" styleName="shadeHead"/>
    <mx:CheckBox id="chk" top="9" right="6" selected="{WindowShade(parent).opened}" styleName="shadeChk"/>
    <mx:Canvas width="100%" height="100%" backgroundColor="#000000" backgroundAlpha="0" />
</flexlibControl:CanvasButton>

The final result, a nice styled WindowShade:

shade 3 (view source enabled)

I used the parent CanvasButton’s rollover event to set the child Label color and forward the event to the child CheckBox. To make the entire button clickable, I used one of my favorite Flex UI hacks: I added a space-filling transparent Canvas.

Comparison Shopping

And finally, a side-by-side comparison of all three WindowShade skins:

Flash is required. Get it here!

Update: See my Drawer Component in Flex 4 post for a custom collapsible drawer component written from scratch in Flex 4.


© 2014 saturnboy.com