• Spark Skinning (including SkinnableComponent) Functional and Design Specification


    Spark Skinning (including SkinnableComponent) - Functional and Design Specification



    Glossary


    Skinning - Changing the look and feel of a component

    Summary and Background


    This document is a high-level overview of the new skinning architecture for Gumbo. Unlike MX, where "skinning" was a simple, graphic-only operation, Gumbo skins are much more comprehensive and can contain multiple elements, like graphical elements, text, images, transitions, etc... Highlights include:

    • All visual aspects, including layout, are controlled by the skin.
    • Single, consistent mechanism for all skins - skinning a Button is the same as skinning a List item.
    • Formal mechanism for skinning composite components through skin parts.
    • Declarative control of all visual aspects of components so it can be easily created and manipulated by tools

    Usage Scenarios


    The most prevalent scenario: A user is creating an application and wants to change the look and feel of an Button. Without having to write any ActionScript code, he creates a new ButtonSkin to use within his application. Because this skin file is MXML, a tool can be used to create/modify it; also, it's easy for a designer to come up with one or modify an existing one to suit their needs.

    Other scenarios:

    • A developer is creating a custom component that he wants to sell to other customers. However, he wants the user to be able to modify the look and feel of the component. By extending SkinnableComponent and hooking into the SkinnableComponent lifecycle, the developer ensures skinning will be easy for the users.
    • A designer is looking to pocket some extra money on the side. He decides to create a set of skins for Flex components and packages them up in a SWC that he sells to other Flex application developers. The application developers change some CSS and start using the new skins.

    Example Skin: ButtonSkin.mxml

    <?xml version="1.0" encoding="utf-8"?>
    <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark">
    
        <Metadata>
            [HostComponent("spark.components.Button")]
        </Metadata> 
        
        <s:states>
            <s:State name="up" />
            <s:State name="over" />
            <s:State name="down" />
            <s:State name="disabled" />
        </s:states>
            
        <!-- border -->
        <s:Rect left="0" right="0" top="0" bottom="0" minWidth="70" minHeight="24">
            <s:stroke>            
                <s:SolidColorStroke color="0x808080" color.disabled="0xC0C0C0" />
            </s:stroke>
        </s:Rect>
    
        <!-- fill -->
        <s:Rect left="1" right="1" top="1" bottom="1" minWidth="68" minHeight="22">
            <s:stroke>
                <s:SolidColorStroke color="0xFFFFFF" color.over="0xFAFAFA" color.down="0xEFEFEF" />
            </s:stroke>
            <s:fill>
                <s:SolidColor color="0xFFFFFF" color.over="0xF2F2F2" color.down="0xD8D8D8" />
            </s:fill>
        </s:Rect>
    
        <!-- label -->
        <s:Label text="{hostComponent.label}"
             color="0x444444" color.disabled="0xC0C0C0"
             horizontalCenter="0" verticalCenter="0"
             left="10" right="10" top="4" bottom="2"
             textAlign="center" verticalAlign="middle" />
    
    </s:Skin>

    In this example, all the visuals for the button's skin are defined in this MXML skin file. It defines two rectangles and a Label for the text. All the information about the layout of these items is also contained in the skin. Note the[HostComponent("spark.components.Button")] metadata that defines what component this skin can be attached to. Because of this metadata, a typed hostComponent property is created on the skin. The Label uses this to bind its text tohostComponent.label.

    Detailed Description


    Each component contains four conceptual pieces: model, skin, controller, and view-specific logic.

    The model contains the properties and business logic for the component. For example, a Range model has properties forminimumValuemaximumValuestepSize, etc., and methods for stepping by a line or a page. The model does not contain any visual or behavioral information.

    The skin defines the visual elements of the component.

    The controller is the interface between the skin and the model. It has several key functions:

    • Defining the component behavior. For example, the Button controller has all of the mouse event handling logic.
    • Defining the visual states of the component.
    • Defining the parts of a component. For example, a scrollbar controller has 4 parts: up arrow, down arrow, thumb, and track.

    The view-specific logic helps position and size the different parts of a skin. For example, HScrollBar and VScrollBar have different view-specific logic which determine the position of the thumb. To make a circular scrollbar one would have to override this logic.

    skinnable component is one whose skin can be changed during authoring or at runtime.

    Putting the pieces together

    • The model and controller are typically written in ActionScript
    • Inheritance should be used to separate the model from the controller. In the framework, we try to do this; however, the distinction between model and controller can sometimes be unclear, especially when the model is a UI element. This is not a hard rule that one always needs to follow, but it is a recommendation when possible.
    • The skin is typically written in MXML.
    • The view-specific logic could be written in the skin file; however, most of the time the logic should be put into the component itself, or a subclass of the component, because lots of people need that logic and it makes skinning easier. An example of this is ScrollBarBase->HScrollBar or ScrollBarBase->VScrollBar.
    • For skinnable components, skins are associated through CSS

    Here is the block layout for a skinnable component. Green arrows denote inheritance. Red arrows denote CSS association.

      

    Code in skins vs. code in components

    In general, one should try to put all "visual" logic in the skin, especially if you want it to be customizable. However, it's not always clear-cut. Code for view-specific logic can live in the skin or in the component. The general rule is any code that is used by multiple skins belongs in the component class, and any code that is specific to a particular skin implementation lives in the skin.

    For example, scrollbars have code to position and size the thumb control. Since this is common to all scrollbars, this code belongs in the component. Because multiple skins will utilize the view-specific logic to position a scrollbar horizontally, there's a class called HScrollBar to do just that.

    We anticipate most framework components will follow this pattern. However, if a developer is creating a one-off component, they may put view-specific logic in the skin file rather than in a separate subclass of the component.

    States

    Some components, like Button, utilize states in their skins. The component defines these states through SkinStatemetadata. This corresponds to states defined on the Skin.

    On the component:

    /**
     *  Up State of the Button
     */
    [SkinState("up")]
    
    /**
     *  Over State of the Button
     */
    [SkinState("over")]
    
    /**
     *  Down State of the Button
     */
    [SkinState("down")]
    
    /**
     *  Disabled State of the Button
     */
    [SkinState("disabled")]
    
    public class Button extends SkinnableComponent {
        ...
    }

    On the skin:

    <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark">
    
        <s:states>
            <s:State name="up" />
            <s:State name="over" />
            <s:State name="down" />
            <s:State name="disabled" />
        </s:states>
    
        ...
    </s:Skin>

    The controller code in the component determines the state of the skin, and sets the currentState property on the skin.

    SkinState metadata is the way a component defines required states for the skin. This is part of the contract between a component and its skin. If a skin doesn't define a required state, the compiler will throw an error, but only theHostComponent metadata is defined on that Skin.

    Subclasses inherit the SkinState}}s defined on their parent. For example, {{Button declares: "up", "over", "down", "disabled" as the skin states. ToggleButton declares: "upAndSelected", "overAndSelected", "downAndSelected", "disabledAndSelected" as the skin states (and inherits all of the skin states from Button). As a B-feature, we'd like to support the Exclude metadata for skin states, but it's not recommended because it does break the "is-a" relationship of a subclass.

    Note that a skin's states aren't necessarily the same as the component's states. For example, an Application has anApplicationSkin associated with it. It may have some states associated with how the skin is configured; however, theApplication may have a different set of "application states." The component is driving the skin's state, which is separate from its own currentState property. So the Button's currentState property is not just a proxy to ButtonSkin's currentStateproperty. The button drives the skin's state, and a user can affect the skin's state by interacting with the component or setting properties on the component, like selected=true/false for a toggle button.

    As a developer, to change the skin's state, you should use invalidateSkinState() and getCurrentSkinState() within the component. invalidateSkinState() invalidates the skin state and eventually sets the skin's state to whatevergetCurrentSkinState() returns. getCurrentSkinState() keeps track of any internal properties on the component and figures out what state the skin should be in.

    Example (Button.as):

    package spark.components
    {
    
    /**
     *  Up State of the Button
     */
    [SkinState("up")]
    
    /**
     *  Over State of the Button
     */
    [SkinState("over")]
    
    /**
     *  Down State of the Button
     */
    [SkinState("down")]
    
    /**
     *  Disabled State of the Button
     */
    [SkinState("disabled")]
     
    public class Button extends SkinnableComponent implements IFocusManagerComponent
    {
    
        ...    
    
        /**
         *  @return Returns true when the mouse cursor is over the button. 
         */    
        public function get isHoveredOver():Boolean
        {
            return flags.isSet(isHoveredOverFlag);
        }
        
        /**
         *  Sets the flag indicating whether the mouse cursor
         *  is over the button.
         */    
        protected function setHoveredOver(value:Boolean):void
        {
            if (!flags.update(isHoveredOverFlag, value))
                return;
    
            invalidateSkinState();
        }
    
        ...
    
        // GetState returns a string representation of the component's state as
        // a combination of some of its public properties    
        protected override function getUpdatedSkinState():String
        {
            if (!isEnabled)
                return "disabled";
    
            if (isDown())
                return "down";
                
            if (isHoveredOver || isMouseCaptured )
                return "over";
                
            return "up";
        }
    
        ...
    
        //--------------------------------------------------------------------------
        //
        //  Event handling
        //
        //--------------------------------------------------------------------------
        
        protected function mouseEventHandler(event:Event):void
        {
            var mouseEvent:MouseEvent = event as MouseEvent;
            switch (event.type)
            {
                case MouseEvent.ROLL_OVER:
                {
                    // if the user rolls over while holding the mouse button
                    if (mouseEvent.buttonDown && !isMouseCaptured)
                        return;
                        setHoveredOver(true);
                    break;
                }
    
                case MouseEvent.ROLL_OUT:
                {
                    setHoveredOver(false);
                    break;
                }
                
                ...
            }
        }
    }
    
    }

    Data

    Sometimes the skin needs to get a reference back to the component that is hosting it. Skins contain a reference back to the host component if the [HostComponent] metadata is defined. The metadata is optional, but when defined it produces a strongly-typed reference back to the component through the hostComponent property. Also, if this metadata is defined, we'll also do some compile-time checking on the skin to make sure it has all the necessary skin parts and skin states are present.

    In the component, to get a reference back to the skin instance, use the skin property.

    There are different ways to pass data back and forth between the skin and the component. One is the push method; the other is the pull method. The push method means taking a property from the component and stuffing it into the appropriate skin slot. On the other hand, if the skin asked the host component what the property value was, that would be the pull method.

    The pull method would use binding in the skin to grab the content from the component. In the push method, the component pushes the value down into the skin. For example, in Button in the label setter, we push this value down to the labelDisplay skin part. Though there's no hard rule when choosing between the two different methods, in the framework, if the functionality is optional in the skin, we use the pull method. However, if the functionality is required in the skin, we use the push method.

    Component/Skin Parts

    Besides skin states, the other major piece of the component/skin contract is skin parts.

    Some components are composed of individual parts. For example, a NumericStepper component contains an up button, a down button and text. These parts are declared by the component, and defined by the skin. Parts defined by the component can be optional or required.

    There are two types of parts: static and dynamic. Static parts are instantiated automatically by the skin. There may only be one instance per static part. Dynamic parts are instantiated when needed, and there may be more than one instance of it. For a Scrollbar, you have 4 different static parts: track, thumb, upArrow, downArrow. For an Accordion, you'd define a dynamic part for the header and then instantiate that dynamic part multiple times.

    Declaring Skin Parts on the Component

    The [SkinPart] metadata is used to declare parts. This metadata can be used on any public property or accessor (getter/setter) on the component. The name of the property becomes the name of the part. The type of the part must be assignable to the type specified in the property.

    Here is an example that defines a skin part named "thumb" that is typed as an Button.

    [SkinPart(required="true")]
    public var thumb:Button;

    The [SkinPart] metadata for static parts has one optional argument: required. By default, all skin parts are optional. You may want skin parts to be required, in which case you can set required="true". If your component includes optional parts, you must make sure that the part is present (non-null) before coding against it.

    Here is an example of a non-required skin part named "upButton".

    [SkinPart(required="false")]
    public var upButton:Button;

    There are times when a component may need one or more instances of a part. For example, an Accordion needs to create a header for each segment. These dynamic parts are declared with [SkinPart] metadata, but the type of the variable is IFactory, and the metadata may optionally include the type that must be created by the factory.

    Here is an example of a dynamic part named "headerFactory", which is expecting the factory to create an instance assignable to the type "spark.components.Button". Dynamic parts can be required or optional.

    [SkinPart(type="spark.components.Button")]
    public var headerFactory:IFactory;

    Defining Skin Parts on the Skin

    Within a skin definition, parts are identified by their id attribute.

    Here is an example of a skin that contains a "thumb" part.

    <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:local="*">
        <!-- other stuff here... -->
    
        <!-- The "thumb" part is defined here. This is an instance of "MyCustomThumb", which
             is a subclass of Button. The id attribute is used to bind this element to the
             component part. -->
        <local:MyCustomThumb id="thumb" ... />
    
        <!-- other stuff here... -->
    </s:Skin>

    Skin parts can be defined anywhere within the skin file. They do not need to be top-level nodes in the MXML file, though the component may bake-in certain assumptions, like the thumb and the track being in the same coordinate space.

    If a required part is not defined by the skin, a runtime error is thrown when the skin is attached to the component, or if the HostComponent metadata is defined on the skin, a compiler error will be thrown.

    Dynamic parts, by convention, live in the Declarations section of the skin file. Declarations is part of the new MXML spec and was need to support top-level default properties. Please read the MXML spec for more details.

    Here is an example of a skin that contains a dynamic part named "headerFactory".

    <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:local="*">
        <fx:Declarations>
            <fx:Component id="headerFactory">
                <local:MyCustomHeaderControl />
            </fx:Component >
    
            <!-- More dynamic part definitions can go here... -->
        </fx:Declarations>
    
        <!-- Static skin elements and parts go here... -->
    </s:Skin>

    Working with static parts

    When loading a skin with static parts, the SkinnableComponent base class is responsible for assigning all static parts from the skin to the declared part variables on the component. Once this assignment is done, the component can simply reference the instance variable directly.

    The partAdded() method is called when the variable is assigned. This is where behavior is added to the part. ThepartRemoved() method is called before the skin is unloaded. This is where behavior is removed from the part.

    Here is a simple example:

    override protected function partAdded(partName:String, instance:*):void
    {
        super.partAdded(partName, instance);
        
        // For static parts we can just compare the instance with the property
        if (instance == thumb)
            thumb.addEventListener(...);
    }
    
    override protected function partRemoved(partName:String, instance:*):void
    {
        super.partRemoved(partName, instance);
        
        if (instance == thumb)
            thumb.removeEventListener(...);
    }

    You should always call super.partAdded() and super.partRemoved() so your base class gets to hook up behavior to the parts appropriately.

    Working with dynamic parts

    Dynamic parts are instantiated by the component using the createDynamicPartInstance() method and removed using theremoveDynamicPartInstance() method. The partAdded() and partRemoved() methods are called whenever a dynamic part is added/removed. This is where behavior is added and removed.

    Here is an example of a component that uses a dynamic part named "separator".

    public class MyComponent extends SkinnableComponent
    {
        /**
         *  Declare the "separator" part
         */
        [SkinPart(type="flash.display.DisplayObject")]
        public var separator:IFactory;
    
        protected const SEPARATOR_NAME:String = "separator";
    
        ...
    
        protected function createSeparator():void
        {
            var separator:DisplayObject = createPartInstance(SEPARATOR_NAME);
            
            separator.alpha = Math.random();
    
            separators[i] = separator;
            skin.addElement(separator);
        }
      
        override protected function partAdded(partName:String, instance:*):void
        {
            super.partAdded(partName, instance);
    
            if (partName == SEPARATOR_NAME)
            {
                // A new separator has been created. 
                
                // Add event listeners
                instance.addEventListener(...);
            }
        }
    
        override protected function partRemoved(partName:String, instance:*):void
        {
            super.partRemoved(partName, instance);
    
            if (partName == SEPARATOR_NAME)
            {
                // A separator is being removed
    
    
                // Remove event listeners
                instance.removeEventListener(...);
            }
        }
        }
    }

    There can be many instances of a dynamic part, and it's up to the developer to call createDynamicPartInstance() as needed. In the example above, createSeparator() creates a separator object from the dynamic part defined in the skin and assigns a random alpha value to the separator. The developer is the one who calls the createSeparator() method. By calling createDynamicPartInstance(), this automatically calls partAdded(). To remove a dynamic part instance, the developer can call removeDynamicPartInstance() directly. Notice that the developer is responsible for adding (and laying out) the dynamic part on the skin object's display object. Similarly, removeDynamicPartInstance() doesn't remove the part from the display list. Just like the developer needs to call addElement(), he must also call removeElement() to remove it from the display list. When a skin is changed at runtime, removeDynamicPartInstance() will be called automatically on all the dynamic parts (as well as the static ones).

    However, if you want the dynamic parts to exist in the new skin, you must create them again once the new skin is loaded. The reason we can't do this automatically for the developer is because each dynamic part has state associated with it (header text, color, alpha, etc...), and we're not privy to this information and what state needs to be transferred. We could create a contract around dynamic part instances to get and retrieve their state information, similar to list item renderers today and the get/set data contract, but it'd only be to support this use case (dynamic parts and skins changing at runtime), which we don't think will occur often enough to merit this extra overhead. In the example below, when a skin is unloading, we copy all the info from the dynamic parts (alpha in this example). Then when the skin is loaded, we use that info to recreate the dynamic parts needed. attachSkin() is a skinning lifecycle method called to load up a skin. If you want to do anything to the newly loaded skin, do it after calling super.attachSkin()detachSkin() is a skinning lifecycle method called to unload a skin. If you want to do anything to the skin that's getting unloaded, do it before calling super.detachSkin().

    public class MyComponent extends SkinnableComponent
    {
        /**
         *  Declare the "separator" part
         */
        [SkinPart(type="flash.display.DisplayObject")]
        public var separator:IFactory;
    
        protected const SEPARATOR_NAME:String = "separator";
    
        ...
    
        protected function createSeparator():void
        {
            var separator:DisplayObject = createPartInstance(SEPARATOR_NAME);
            
            separator.alpha = Math.random();
    
            separators.push(separator);
            skin.addElement(separator);
        }
      
        override protected function partAdded(partName:String, instance:*):void
        {
            super.partAdded(partName, instance);
    
            if (partName == SEPARATOR_NAME)
            {
                // A new separator has been created. 
                
                // Add event listeners
                instance.addEventListener(...);
            }
        }
    
        override protected function partRemoved(partName:String, instance:*):void
        {
            super.partRemoved(partName, instance);
    
            if (partName == SEPARATOR_NAME)
            {
                // A separator is being removed
    
    
                // Remove event listeners
                instance.removeEventListener(...);
            }
        }
        
        override protected function detachSkin():void
        {
            for (var i:int = 0; i < separators.length; i++)
    	{
    	    separatorAlphas[i] = separators[i].alpha;
            }
    
            separators = [];
    
            super.detachSkin();
        }
    
        override protected function attachSkin():void
        {
            super.attachSkin();
            
            // create separators from last time
            
            for (var i:int = 0; i < separatorAlphas.length; i++)
    
    	{
    	   var separator:DisplayObject = createPartInstance(SEPARATOR_NAME);
    	   
    	   separator.alpha = separatorAlphas[i];
    	   
    	   separators.push(separator);
    	   skin.addElement(separator);
            }
        }
        }
    }

    Working with deferred parts

    Complex components may need to work with parts that are created at some indeterminate time after the skin has been created. Components need to be aware that the partAdded() method may be called long after the skin has been instantiated. An example of a deferred part is one in a TabNavigator that isn't created until the tab is clicked on by the user. When the user clicks on the tab, the part will be created, and through Binding, the SkinnableComponent will become aware of the new part and call partAdded() on it.

    public class MyComponent extends SkinnableComponent
    {
        // Declare the "details" part
        [SkinPart]
        public var details:DetailsPane;
    
    
        override protected function partAdded(partName:String, instance:*):void
        {
            super.partAdded(partName, instance);
    
            if (partName == "details")
            {
                // Details part has been created. Attach behaviors here.
                details.addEventListener(...);
                details.doSomethingElse();
            }
        }
    }

    Associating Components with Skins

    Skins are associated to a component via the skinClass style property defined on SkinnableComponent.

    An example of doing this via CSS:

    Panel
    {
    skinClass: ClassReference("mx.skins.spark.PanelSkin")
    }

    You can also do it inline with MXML:

    <comps:MySkinnableComponent skinClass="com.mycompany.skins.MyHungrySkin" />

    The skinClass style is typed to take Class objects.

    Skin Parts Lifecycle

    Here's a diagram that explains the skin parts lifecycle, how the different parts are instantiated, and when partAdded() is called in each case. Note that Flex calls attachSkin() during the first commitProperties() phase or whenever a skin is changed at runtime.


    (Note: In the diagram above, loadSkin() has been renamed to attachSkin() 

    Unloading a skin is basically done automatically. detachSkin() is called whenever the skin changes at runtime.partRemoved() will be called on all skin parts (static, dynamic, and deferred) automatically. That's where the developer should remove the event listeners. Note that if the component is removed from the display list, detachSkin() won't be called--it is only called when the skin is explicitly removed or changed.

    Styles

    In MX, each of the components had lots of styles for you to tweak. In Spark, we've shied away from styling in favor of skinning, which gives you more flexibility. However, this approach does make some simple tweaks a little harder.

    The Spark components do have some styles that you can set, though. We've created some global styles that apply to all of our skins. This makes styling the Spark components to completely change the look fairly easy. See the Styling Gumbo Components Specification. Your skins can be affected by these global styles by using SparkSkin instead of Skin as the root class for your skin. After using SparkSkin, you will have to override certain properties, like symbolItems andcolorizeExclusion.

    The Spark components also have some other styles that can be set. For instance, the Button component has a cornerRadius style declared on it. In order to accomplish this, there's some work that needs to be done in both the Skinand in the SkinnableComponent.

    The Skin, which defines the visual aspects of the component, is the one who must react to this style. There are two recommended approaches in order to deal with this. The first is through binding, and the second is by overriding updateDisplayList().

    Binding Example:

    <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" 
                 minWidth="21" minHeight="21" alpha.disabled="0.5">
        
        <fx:Metadata>[HostComponent("spark.components.Button")]</fx:Metadata>
        
        <!-- states -->
        <s:states>
            <s:State name="up" />
            <s:State name="over" />
            <s:State name="down" />
            <s:State name="disabled" />
        </s:states>
        
        <s:Rect id="fill" left="1" right="1" top="1" bottom="1" radiusX="{getStyle('cornerRadius')}">
            <s:fill>
                <s:LinearGradient rotation="90">
                    <s:GradientEntry color="0xFFFFFF" 
                                     color.over="0xBBBDBD" 
                                     color.down="0xAAAAAA" 
                                     alpha="0.85" />
                    <s:GradientEntry color="0xD8D8D8" 
                                     color.over="0x9FA0A1" 
                                     color.down="0x929496" 
                                     alpha="0.85" />
                </s:LinearGradient>
            </s:fill>
        </s:Rect>
    
        
        <s:Label id="labelDisplay"
                 textAlign="center"
                 verticalAlign="middle"
                 maxDisplayedLines="1"
                 horizontalCenter="0" verticalCenter="1"
                 left="10" right="10" top="2" bottom="2" />
        
    </s:Skin>

    Overriding updateDisplayList() example:

    <?xml version="1.0" encoding="utf-8"?>
    <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" 
                 minWidth="21" minHeight="21" alpha.disabled="0.5">
        
        <fx:Metadata>[HostComponent("spark.components.Button")]</fx:Metadata>
        
        <fx:Script>
            <![CDATA[
                override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number) : void
                {
                    fill.radiusX = getStyle('cornerRadius');
                    
                    super.updateDisplayList(unscaledWidth, unscaledHeight);
                }
            ]]>
        </fx:Script>
        
        <!-- states -->
        <s:states>
            <s:State name="up" />
            <s:State name="over" />
            <s:State name="down" />
            <s:State name="disabled" />
        </s:states>
        
        <s:Rect id="fill" left="1" right="1" top="1" bottom="1">
            <s:fill>
                <s:LinearGradient rotation="90">
                    <s:GradientEntry color="0xFFFFFF" 
                                     color.over="0xBBBDBD" 
                                     color.down="0xAAAAAA" 
                                     alpha="0.85" />
                    <s:GradientEntry color="0xD8D8D8" 
                                     color.over="0x9FA0A1" 
                                     color.down="0x929496" 
                                     alpha="0.85" />
                </s:LinearGradient>
            </s:fill>
        </s:Rect>
    
        
        <s:Label id="labelDisplay"
                 textAlign="center"
                 verticalAlign="middle"
                 maxDisplayedLines="1"
                 horizontalCenter="0" verticalCenter="1"
                 left="10" right="10" top="2" bottom="2" />
        
    </s:Skin>

    Both of these approaches work fine--one uses binding and one overrides a method. In picking an approach, you should do whichever you are comfortable with, though, in general, binding is usually easier but also a bit slower performance wise.

    Now that our skin respects the style, we need to be able to set the style on the button. You can use CSS, like:

    <?xml version="1.0" encoding="utf-8"?>
    <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark">
    	<fx:Style>
            @namespace s "library://ns.adobe.com/flex/spark";
            
            s|Button
            {
                cornerRadius: 10;
            }
        </fx:Style>
        <s:Button skinClass="GradientSkin" label="hello" />
    </s:Application>

    However, in order to set the style inline with MXML, you need to declare the style metadata on the component directly. For example:

    [Style(name="cornerRadius", type="Number", format="Length", inherit="no", minValue="0.0")]
    public class Button extends SkinnableComponent
    {
    ...
    }

    If you want to add a style to one of the Spark components, if you don't care about inline MXML styling, then you can just add the style to the Skin. However, if you want inline MXML styling capabilities, then you'll have to subclass the Spark component and add the style metadata. For example:

    [Style(name="myWackyNewStyle", type="Number", inherit="no"")]
    public class MyButton extends Button
    {
    ...
    }

    Note that even though you are setting the style on the component, in the skin, you aren't callinghostComponent.getStyle('blah'), but you are using getStyle('blah') directly. This is because a component's styles are automatically pushed down in to the skin.

    API Description


    SkinnableComponent:

    package spark.components.supportClasses
    {
    
    /**
     *  Name of the skin to use for this component. The skin must be a class that extends
     *  the flex.core.Skin class. 
     */
    [Style(name="skinClass", type="Class")]
    
    public class SkinnableComponent extends UIComponent
    {
    
        //--------------------------------------------------------------------------
        //
        //  Properties
        //
        //--------------------------------------------------------------------------
    
        /**
         *  The instance of the skin class for this component instance. 
         *  This is a read-only property that gets set automomatically when Flex 
         *  calls the <code>attachSkin()</code> method.
         */
        public function get skin():UIComponent;
    
    
        //--------------------------------------------------------------------------
        //
        //  Methods - Skin/Behavior lifecycle
        //
        //--------------------------------------------------------------------------
    
        /**
         *  Create the skin for the component. 
         *  You do not call this method directly. 
         *  Flex calls it automatically when it calls <code>createChildren()</code> or  
         *  the <code>UIComponent.commitProperties()</code> method.
         *  Typically, a subclass of SkinnableComponent does not override this method.
         * 
         *  <p>This method instantiates the skin for the component, 
         *  adds the skin as a child of the component, and 
         *  resolves all part associations for the skin</p>
         */
        protected function attachSkin():void;
    
        /**
         *  Destroys and removes the skin for this component. 
         *  You do not call this method directly. 
         *  Flex calls it automatically when a skin is changed at runtime.
         *
         *  This method removes the skin and clears all part associations.
         *
         *  <p>Typically, subclasses of SkinnableComponent do not override this method.</p>
         */
        protected function detachSkin():void;
    
        /**
         *  Find the skin parts in the skin class and assign them to the properties of the component.
         *  You do not call this method directly. 
         *  Flex calls it automatically when it calls the <code>attachSkin()</code> method.
         *  Typically, a subclass of SkinnableComponent does not override this method.
         */
        protected function findSkinParts():void;
    
        /**
         *  Clear out references to skin parts. 
         *  You do not call this method directly. 
         *  Flex calls it automatically when it calls the <code>detachSkin()</code> method.
         *
         *  <p>Typically, subclasses of SkinnableComponent do not override this method.</p>
         */
        protected function clearSkinParts():void;
    
        //--------------------------------------------------------------------------
        //
        //  Methods - Parts
        //
        //--------------------------------------------------------------------------
    
        /**
         *  Called when a skin part is added. 
         *  You do not call this method directly. 
         *  For static parts, Flex calls it automatically when it calls the <code>attachSkin()</code> method. 
         *  For dynamic parts, Flex calls it automatically when it calls 
         *  the <code>createDynamicPartInstance()</code> method. 
         *
         *  <p>Override this function to attach behavior to the part. 
         *  If you want to override behavior on a skin part that is inherited from a base class, 
         *  do not call the <code>super.partAdded()</code> method. 
         *  Otherwise, you should always call the <code>super.partAdded()</code> method.</p>
         *
         *  @param partname The name of the part.
         *
         *  @param instance The instance of the part.
         */
        protected function partAdded(partName:String, instance:Object):void;
    
        /**
         *  Called when an instance of a skin part is being removed. 
         *  You do not call this method directly. 
         *  For static parts, Flex calls it automatically when it calls the <code>detachSkin()</code> method. 
         *  For dynamic parts, Flex calls it automatically when it calls 
         *  the <code>removeDynamicPartInstance()</code> method. 
         *
         *  <p>Override this function to remove behavior from the part.</p>
         *
         *  @param partname The name of the part.
         *
         *  @param instance The instance of the part.
         */
        protected function partRemoved(partName:String, instance:Object):void;
    
    
        //--------------------------------------------------------------------------
        //
        //  Methods - Dynamic Parts
        //
        //--------------------------------------------------------------------------
        /**
         *  Create an instance of a dynamic skin part. 
         *  Dynamic skin parts should always be instantiated by this method, 
         *  rather than directly by calling the <code>newInstance()</code> method on the factory.
         *  This method creates the part, but does not add it to the display list.
         *  The component must call the <code>Group.addElement()</code> method, or the appropriate 
         *  method to add the skin part to the display list. 
         *
         *  @param partName The name of the part.
         *
         *  @return The instance of the part, or null if it cannot create the part.
         */
        protected function createDynamicPartInstance(partName:String):Object;
    
        /**
         *  Remove an instance of a dynamic part. 
         *  You must call this method  before a dynamic part is deleted.
         *  This method does not remove the part from the display list.
         *
         *  @param partname The name of the part.
         *
         *  @param instance The instance of the part.
         */
        protected function removeDynamicPartInstance(partName:String, instance:Object):void;
    
        /**
         *  Returns the number of instances of a dynamic part.
         *
         *  @param partName The name of the dynamic part.
         *
         *  @return The number of instances of the dynamic part.
         */
        protected function numDynamicParts(partName:String):int;
    
        /**
         *  Returns a specific instance of a dynamic part.
         *
         *  @param partName The name of the dynamic part.
         *
         *  @param index The index of the dynamic part.
         *
         *  @return The instance of the part, or null if it the part does not exist.
         */
        protected function getDynamicPartAt(partName:String, index:int):Object;
    
    
        //--------------------------------------------------------------------------
        //
        // Skin states support
        //
        //--------------------------------------------------------------------------
    
        /**
         *  Returns the name of the state to be applied to the skin. For example, a
         *  Button component could return the String "up", "down", "over", or "disabled" 
         *  to specify the state.
         * 
         *  <p>A subclass of SkinnableComponent must override this method to return a value.</p>
         * 
         *  @return A string specifying the name of the state to apply to the skin.
         */
        protected function getCurrentSkinState():String;
    
        /**
         *  Marks the component so that the new state of the skin is set
         *  during a later screen update.
         */
        public function invalidateSkinState():void;
    }
    }

    Gumbo components have the same lifecycle as MX components, with a few additions for skinning. Most Gumbo components should not override the createChildren() method because the attachSkin() method loads the visuals by attaching the skin object. The ones developers will override most often are partAdded() and partRemoved().

    Skin:

    package spark.components
    {
    public class Skin extends Group
    {
    
    }
    }

    Not many new properties or methods are added to the Skin class. Most of its behavior comes from it's base class, Group.

    B Features


    We should add support for compile-time checking of the skin-component contract based on the HostComponent metadata. We should also add Exclude metadata support to the tools and ASDoc for {{SkinState}s

    Examples and Usage


    Examples were located throughout the document.

    Additional Implementation Details


    SkinPart metadata

    The SkinPart metadata is compiled into static information that's defined on each class. This static construct includes theSkinPart}}s defined on that class as well as any defined on any base classes. {{SkinnableComponent defines a protected getter, skinParts, that gets overridden by subclasses and returns all the skinparts for this component. The compiler automatically does this work for you, and at runtime, when a component skin is loaded, the SkinnableComponent base class looks at the skinParts getter to find all the skin parts, assigns parts that were found, and throws a runtime error if any required parts are not defined by the skin. Note that deferred parts will be found (because they are defined properties of the skin), but will have a value of null at this time.

    Notification for id assignment

    In order to support deferred part notification, an event will be sent whenever an id slot on a document is assigned.SkinnableComponent will listen for a "propertyChange" event on the skin object to see when the object has been updated. If you're producing a skin file in MXML, you won't have to do anything because ids are already bindable and dispatch a "propertyChange" event when updated. If you're producing a skin file in ActionScript only, you can just make your parts bindable through the [Bindable] metadata. If you decide to dispatch your own events, make sure to dispatch a "propertyChange" event of class type PropertyChangeEvent. SkinnableComponent listens for this event, and if the assigned id is a static part, calls the partAdded() method. For dynamic parts, the partAdded() function is only called when an instance of the part is created. It is not called when the factory itself is assigned.

    Keeping track of instantiated dynamic parts

    The createDynamicPartInstance() method keeps a cache of the dynamically instantiated parts. The numDynamicParts() andgetDynamicPartAt() methods use this cache for quick reference.

    Prototype Work


    Most of this design has already been implemented in the Gumbo codebase.

    Compiler Work


    Compiler work has already been done to support the extra metadata and any generated code out of that.

    We should add support for compile-time checking of the skin based on the HostComponent metadata. This will be a B-feature though.

    Web Tier Compiler Impact


    Same as compiler work above.

    Flex Feature Dependencies


    -

    Backwards Compatibility


    Syntax changes

    N/A

    Behavior

    N/A

    Warnings/Deprecation

    N/A

    Accessibility


    Screen readers should be able to access the skin in order to read any text.

    Performance


    Skinning will add some additional overhead in terms of the number of display objects since it's not just a simple graphical element anymore. However, not all GraphicElements will be DisplayObjects; there will be code in Group to handle this. Currently a ButtonSkin is a DisplayObject and is a child of Button. Essentially every SkinnableComponent has atleast two DisplayObjects associated with it, but there really only needs to be one. So we might change Skin so it's not a display object and more of an orchestrator of what it's host component should be drawing.

    Globalization


    N/A

    Localization


    Compiler Features

    All command-line and run-time messages will be added to the property configuration files.

    Framework Features

    RTE strings that will require localization:

    • Error message when a required skin part is not defined in the skin

    Issues and Recommendations


    Parsing the SkinPart metadata at runtime requires calling describeType(), which is very expensive. The results are cached, and describeType() is called a maximum of one time per Class. If this is still too expensive we'll need to come up with a compiler/code-gen method for storing SkinPart metadata.

    We are adding a lot of new lifecycle methods around Skins. This could be confusing to users. Instead of partAdded() andpartRemoved(), you could put this logic into a getter/setter for the individual part. The downside to this approach is that the user may forget to handle the removing of handlers, where as before it's explicitly called out. However, this way all the controller code for that part exists with the definition of that part, rather than throughout the file in separate methods, partAdded() and partRemoved(). It's also building on top of existing Flex-isms, rather than introducing these new lifecycle methods one has to learn.

    Documentation


    New classes will need to be documented. The whole skinning architecture will probably need a separate section in the docs with several examples.

    QA


    QA will need to test all the different types of skin parts and all the different skin lifecycle methods.

     From: http://opensource.adobe.com/wiki/display/flexsdk/Spark+Skinning

  • 相关阅读:
    PHP抓取页面的几种方式
    MySQL性能优化的最佳20+条经验
    linux下导入、导出mysql数据库命令
    8个必备的PHP功能开发
    jquery中的children()和contents()的区别
    centos 通用开发工具及库安装 有了它不用愁了
    将bat批处理文件注册成windows服务
    squid隐藏squid的版本号
    squid如何屏蔽User-Agent为空的请求
    RHEL/CentOS 6.x使用EPEL6与remi的yum源安装MySQL 5.5.x
  • 原文地址:https://www.cnblogs.com/fxair/p/1743638.html
Copyright © 2020-2023  润新知