Simple Spark Button Skin Flex

A skin is the overall layout of a control [according to me], which is the composition of basic Geometric shapes and text. For example, a button is a filled rectangle with some text in the middle [a basic button]. This combination can be assumed as the default skin for the control. And of course, who would like to go with the default Always? 

Flex provides mechanism to change the default skin of controls. This post describes the code for changing the default skin of spark.components.Button control. Lets not complicate too much and stick on to only few necessary things to get the job done. And here, the discussion is around applying a simple skin to a button using only mxml [not using a single line of ActionScript].

The layout of the control changes at situations in order to create some illusion. For example a button when clicking pretends to be actually pushed and pulled back, but really only the physical element changes such as the border color or the fill color etc., These situations are very limited, few of them are unique to controls, called as the states of a control. So, our example, the button has the following states

Up - normal state [when the button is enabled]
Over - mouse pointer is over the control
Down - the button is clicked
Disabled - disabled state [cannot click, mouse over does not have any effect]

So the first step is to make Flex understand a Skin. A skin is written between the tags <s:SparkSkin></s:SparkSkin>. And also, the component for which the skin has to applied, which is enclosed in the <fx:Metadata> tag. In our case it is the spark.components.Button. Of course the states too. Putting it all together, the code looks like

<?xml version="1.0" encoding="utf-8"?>
<s:SparkSkin 
    xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark"
    xmlns:mx="library://ns.adobe.com/flex/mx">

    <fx:Metadata>
        [HostComponent("spark.components.Button")]
    </fx:Metadata>

    <s:states>

        <s:State name="up"/>
        <s:State name="down"/>
        <s:State name="over"/>
        <s:State name="disabled"/>
    </s:states>

    <!-- Rest of the code in this post goes here -->

</s:SparkSkin>

The next step is to draw the basic layout of the button using our own skin. A rectangle, of course a filled one, can be used instead of a button control. The <s:Rect> tag allows us to draw one such rectangle. The below code and other code in the post has to come after the comment in the above mxml.

A simple Rectangle has the following code,

<s:Rect x="100" y="100" width="100" height="25">
    <s:fill>
        <s:SolidColor color="0xdddddd"/>
    </s:fill>
</s:Rect>

The same rectangle, but with a black border

<s:Rect x="100" y="100" width="100" height="25">
    <s:stroke>
        <s:SolidColorStroke color="0x000000"/>
    </s:stroke>
    <s:fill>
        <s:SolidColor color="0xdddddd"/>
    </s:fill>
</s:Rect>

Buttons are not always a sharp rectangle, instead would be a rounded rectangle, adding border radius to the above rectangles

<s:Rect x="100" y="100" width="100" radiusX="4" radiusY="4" height="25">
    <s:fill>
        <s:SolidColor color="0xdddddd"/>
    </s:fill>
</s:Rect>

<
s:Rect x="100" y="100" width="100" radiusX="4" radiusY="4" height="25">
    <s:stroke>
        <s:SolidColorStroke color="0x000000"/>
    </s:stroke>
    <s:fill>
        <s:SolidColor color="0xdddddd"/>
    </s:fill>
</s:Rect>

The output of the above 4 rectangles are shown in the image below, from left to right



But a button would often create some illusions as if it has a third dimension. It can be achieved using Gradient fill instead of a Solid fill, and shadows. Below code is example for Gradient fill

<s:Rect x="100" y="100" radiusX="4" radiusY="4" width="100" height="25">
    <s:fill>
        <s:LinearGradient rotation="90">
            <s:GradientEntry color="0x000055" ratio=".1" />
            <s:GradientEntry color="0x0000aa" ratio=".4" />
            <s:GradientEntry color="0x000055" />
        </s:LinearGradient>
    </s:fill>
</s:Rect>

This code corresponds to the last blue colored rectangle in the below image. Others are few examples with Gradients.You can play with gradients to master it.



Applying shadow to a shape can be done drawing one rectangle (for the shadow) and placing one more over that. Note that that shadow has to be drawn first, not the actual shape, so that the shape overlaps the shadow, allowing parts of the below shape to be visible, creating an illusion of a shadow. The below code can be used for a shadow.

<!-- This is for shadow -->
<s:Rect x="103" y="103" radiusX="4" radiusY="4" width="100" height="25">
    <s:fill>
        <s:SolidColor color="0x909090"/>
    </s:fill>
</s:Rect>

<!-- This is for the control -->

<s:Rect x="100" y="100" radiusX="4" radiusY="4" width="100" height="25">
    <s:fill>
        <s:LinearGradient rotation="90">
            <s:GradientEntry color="0x606060" ratio=".5"/>
            <s:GradientEntry color="0x303030"/>
        </s:LinearGradient>
    </s:fill>
</s:Rect>

Note that x and y are incremented by the same value in the shadow so as to provide shadow at an angle of 45 degrees. You can change the x and y of shadow based on your needs. The below shown image has the controls with shadow, 1, 2,..5 px displacement.


The basic layout of the control has been decided and the next step is to make the user believe that the created shape is an actual button. The states are meant for this. The below section explains about the states and the control behavior based on the states.

The UP state
The default state of the button just when it is created, enabled and of course the cursor is not over the control. The above layout [including the shadow], can be used for the UP state. The shadow can be used also with over state. So, the above code would have one and only modification

<s:Rect x="100" y="100" radiusX="4" radiusY="4" width="100" height="25" includeIn="up">

Rest of the code is same. The includeIn attribute defines the layout of the particular state. Say, with the above example, in the up state, the shadow along with the Gradient filled rectangle will be there.

The OVER state
The state when the mouse pointer is over the control. This state may or may no have some changes in layout. If the layout changes it would be more pleasant to see, and it adds liveliness to the control. My idea is to darken the Gradient fill and let the shadow remains the same.

<s:Rect x="100" y="100" radiusX="4" radiusY="4" width="100" height="25" includeIn="over">
    <s:fill>
        <s:LinearGradient rotation="90">
            <s:GradientEntry color="0x404040" ratio=".5" />
            <s:GradientEntry color="0x101010"/>
        </s:LinearGradient>
    </s:fill>
</s:Rect>

Now as the mouse rolls in the control, the fill gets darker and lighter as it rolls out. No wonder. Isn't it?

The DOWN state
The state when a user clicks on the control. The change of state is so fast sometimes cannot see the actual change. There are two cases, the state can change from over to down and down to over, this is when the user clicks on the control using Mouse and can be from up to down and down to up, when a Keyboard shortcut is there for the button and the user prefers Keyboard. So, for down, we dont have the shadow [it is hidden:)]. In the place of shadow, we place the top rectangle, creating the illusion of being clicked that is went it and came back. The code for down state is

<s:Rect x="103" y="103" radiusX="4" radiusY="4" width="100" height="25" includeIn="down">
    <s:fill>
        <s:LinearGradient rotation="90">
            <s:GradientEntry color="0x303030" ratio=".5" />
            <s:GradientEntry color="0x090909"/>
        </s:LinearGradient>
    </s:fill>
</s:Rect>

Note that we still have the shadow there, because it does not have any includeIn attribute and hence it will be included in all states. But, the shadow is overwritten by our new rectangle, as it has the same location and dimensions as the shadow.

The DISABLED state
The state when the button is disabled, that is cannot be clicked. We have darken the color for over state, still darker for down and lighter for disabled. I think that does make sense.

<s:Rect x="100" y="100" radiusX="4" radiusY="4" width="100" height="25" includeIn="disabled">
    <s:fill>
        <s:LinearGradient rotation="90">
            <s:GradientEntry color="0x909090" ratio=".5" />
            <s:GradientEntry color="0x606060"/>
        </s:LinearGradient>
    </s:fill>
</s:Rect>

The below image shows the control in all four states. Would be great seeing in Flash Player than as a series of images.



So far we have created our own styled button. The control is still not completed as a whole. Yep! The button does not say still what it is for. The next step is to make the control convey something to the user, so that the user clicks it whenever needed. A label would do that.

<s:Label 
    color="0xD0D0D0" 
    text="Click Me!" 
    x="100" 
    y="100" 
    width="100" 
    height="25" 
    verticalAlign="middle" 
    textAlign="center" 
    includeIn="up" 
/>

So this label is positioned exactly as same as the rectangle. And of course, some change can be done in the label for over state.

<s:Label 
    color="0xFFFFFF"
    text="Click Me!"
    x="100"
    y="100"
    width="100"
    height="25"
    verticalAlign="middle" 
    textAlign="center" 
    includeIn="over"
/>

Just darkened the Label color. And for down state, since the layout moves to a different location, the label has to move also.

<s:Label
    color="0xFFFFFF"
    text="Click Me!"
    x="103"
    y="103"
    width="100"
    height="25"
    verticalAlign="middle"
    textAlign="center"
    includeIn="down" 
/>

Don't forget the disabled state too:)

<s:Label
    color="0xA0A0A0"
    text="Click Me!"
    x="100"
    y="100"
    width="100"
    height="25"
    verticalAlign="middle"
    textAlign="center"
    includeIn="disabled" 
/>

The four states now with Label, is shown below.


And at this stage, still the skin is not complete and it would locate the control always at the same location (100, 100) and of same height and width. The final step is cutomizing these parameters so as the Skin becomes generic and can be used across various application with less effort. Lets see how.

Using the hostComponent we can get the values from the actual declaration. Thus, in our case we use the following, one example for Rect and one for Label

<s:Rect 
    x="{hostComponent.x + 3}"
    y="{hostComponent.y + 3}"
    radiusX="4" 
    radiusY="4"
    width="{hostComponent.width}"
    height="{hostComponent.height}"
    includeIn="down"
>
    <s:fill>
        <s:LinearGradient rotation="90">
            <s:GradientEntry color="0x303030" ratio=".5" />
            <s:GradientEntry color="0x090909"/>
        </s:LinearGradient>
    </s:fill>
</s:Rect>

The above one is for shadow 3px displacement. For the label

<s:Label
    color="0xD0D0D0" 
    text="{hostComponent.label}" 
    x="{hostComponent.x}"
    y="{hostComponent.y}"
    width="{hostComponent.width}"
    height="{hostComponent.height}"
    verticalAlign="middle"
    textAlign="center"
/>

With this, now the designer can located the button on any desired place, of any desired dimensions and of any desired Label as follows.

<s:Button 
    skinClass="Skin1" 
    x="100" 
    y="100" 
    height="40" 
    width="100" 
    id="btn3" 
    label="Hello" 
    enabled="true"
/>

where Skin1 is the name of the file which has the defintion of the skin, I mean the tag <s:SparkSkin>, and if you the skin is under a package [named Hello] then,

<s:Button 
    skinClass="Hello.Skin1" 
    x="100" 
    y="100" 
    height="40" 
    width="100" 
    id="btn3" 
    label="Hello" 
    enabled="false"
/>

3 comments:

  1. following statements are not working and shows blank output screen.
    x="{hostComponent.x + 3}"
    y="{hostComponent.y + 3}"

    but when I change to this
    x="{hostComponent.x}"
    y="{hostComponent.y}"
    the result appears but not desirable. so actually +3 is not working.

    ReplyDelete
    Replies
    1. The output shown here is corresponding to the code here. If you can post some more code or a screen shot of the output that you are getting, it would be helpful to find the actual issue.

      Delete
  2. nice blog
    great information
    Cosmetology Course
    Advance Diploma in Cosmetology,Grand Master in Cosmetology,
    Professional Diploma in Cosmetology,International Diploma in Cosmetology

    ReplyDelete