Skinning Callout and CalloutButton

Skinning Callout and CalloutButton introduce a few challenges. Let’s start with Callout first.

The default Callout skin is a little more complex than the typical mobile skin. A few issues to highlight:

  • The arrow skin part is implemented as a subclass named CalloutArrow
  • The backgroundColor gradient fill also fills the arrow for all callout and arrow position combinations
  • The arrow is not given a position in the skin. Instead, Callout manages the arrow position based on the Callout’s position relative to the owner.
  • The skin implements open/close state transition effects in ActionScript
To make Callout skinning easier, spark.skins.mobile.CalloutSkin exposes protected properties that subclasses can modify in order to tweak the existing visuals. For example, the screenshot and code below show how I’ve squared up the Callout’s corners, removed the background gradient, and specified new default sizes for the arrow.
package skins
{
import mx.core.DPIClassification;

import spark.skins.mobile.CalloutSkin;

public class BoxyCalloutSkin extends CalloutSkin
{
    public function BoxyCalloutSkin()
    {
        super();
        
        // this skin does not implement contentBackgroundAppearance=inset
        contentBackgroundInsetClass = null;
        
        // disable drop shadow
        dropShadowVisible = false;
        
        // disable backgroundColor gradient fill
        useBackgroundGradient = false; 
        
        // square up the corners
        contentCornerRadius = 0; 
        backgroundCornerRadius = 0; 
        
        // backgroundColor frame is not visible
        frameThickness = 0; 
        
        // add border properties (by default this is NaN, no border)
        borderThickness = 1; 
        borderColor = 0x333333; 
        
        // new arrow proportions
        switch (applicationDPI)
        {
            case DPIClassification.DPI_320:
            {
                arrowWidth = 60; 
                arrowHeight = 30; 
                break;
            }
            case DPIClassification.DPI_240:
            {
                arrowWidth = 45; 
                arrowHeight = 15; 
                break;
            }
            default:
            {
                arrowWidth = 30; 
                arrowHeight = 10; 
                break;
            }
        }
    }
    
    override protected function createChildren():void
    {
        // BoxyCalloutArrow subclasses CalloutArrow
        
        // create arrow first, super will skip default arrow creation 
        arrow = new BoxyCalloutArrow();
        arrow.id = "arrow";
        arrow.styleName = this;
        
        // call super
        super.createChildren();
        
        // add arrow above all other children
        addChild(arrow);
    }
}
}
package skins
{
import spark.skins.mobile.supportClasses.CalloutArrow;

public class BoxyCalloutArrow extends CalloutArrow
{
    public function BoxyCalloutArrow()
    {
        super();
        
        borderThickness = 1; 
        borderColor = 0x333333; 
        gap = 0;
        useBackgroundGradient = false;
    }
}
}

CalloutButton is simply a subclass of Button. Because of this, wherever a CalloutButton is used, it typically takes on the skinClass value inherited from Button style rules.

To create a Callout from it’s skin, CalloutButton looks for a dropDown property in the skin. If it’s not present, it creates a default Callout. In order to change any properties of CalloutButton’s generated Callout, you have to change CalloutButton’s skin. This isn’t quite intuitive at first.

To use the same BoxyCalloutSkin from the previous example in a CalloutButton, I’ve defined a subclass of the ActionBar’s BeveledActionButtonSkin.

<?xml version="1.0" encoding="utf-8"?>
<!-- 
CalloutButton is a Button and uses the standard Button skins by default. 
For this example, I explicitly use the beveled ActionBar button skin.
-->
<mobile:BeveledActionButtonSkin xmlns:fx="http://ns.adobe.com/mxml/2009" 
                                xmlns:s="library://ns.adobe.com/flex/spark" 
                                xmlns:mobile="spark.skins.mobile.*">
    <fx:Declarations>
        <!-- 
        An optional dropDown skin part defines the Callout generated after 
        (1) pressing a CalloutButton or (2) calling calloutButton.openDropDown().
        Button skins don't contain a dropDown skin part by default. In this case,
        CalloutButton creates a default Callout.
        -->
        <fx:Component id="dropDown">
            <s:Callout skinClass="skins.BoxyCalloutSkin"
                       backgroundColor="0x999999"
                       contentBackgroundAlpha="0"/>
        </fx:Component>
    </fx:Declarations>
</mobile:BeveledActionButtonSkin>

Finally, here’s the code for the View shown in the screen shot above. It shows usages of both an independent Callout and a CalloutButton, both using the same custom BoxyCalloutSkin. The full project can also be downloaded here.

<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:s="library://ns.adobe.com/flex/spark" title="HomeView">
    <fx:Declarations>
        <!-- boxyCallout uses BoxyCalloutSkin -->
        <s:Callout id="boxyCallout" 
                   width="{Math.floor(this.width * .3)}"
                   height="{Math.floor(this.height * .3)}"
                   skinClass="skins.BoxyCalloutSkin"
                   mouseDownOutside="boxyCallout.close()"
                   backgroundColor="0x999999"
                   contentBackgroundAlpha="0"/>
    </fx:Declarations>
    <s:actionContent>
        <!-- use a standard button to open boxyCallout -->
        <s:Button id="button1" 
                  label="BoxyCalloutSkin" 
                  click="boxyCallout.open(button1)"/>
        <!--
        Use a CalloutButton with a custom CalloutButtonSkin.
        The new skin uses BoxyCalloutSkin on the dynamically created Callout.
        -->
        <s:CalloutButton id="calloutButton"
                         label="CalloutButton" 
                         skinClass="skins.CalloutButtonSkin">
            <!-- 
            Filler to give the Callout some size.
            The callout instanace can be accessed via calloutButton.callout
            once the open event has fired.
            -->
            <s:Group width="{Math.floor(this.width * .3)}"
                     height="{Math.floor(this.height * .3)}"/>
        </s:CalloutButton>
    </s:actionContent>
    <s:actionLayout>
        <s:HorizontalLayout verticalAlign="middle" 
                            gap="5"/>
    </s:actionLayout>
</s:View>

Download: CalloutSkinning.fxp

Share on Facebook