Menu

Callout and CalloutButton

SourceForge Editorial Staff

Callout and CalloutButton - Functional and Design Specification


Glossary

dropDown component - general term referring to a component that contains an anchor button and a dropDown

DropDownController - class that contains the user interaction logic for a dropDown component

dropDown - component that is displayed when the user presses the button of a dropDown component.

Summary and Background

The Callout subclasses SkinnablePopUpContainer. It is used to provide a container that shows an arrow (a.k.a. tail) that is positioned relative to it's owner. #CalloutButton combines #Callout and DropDownController into a convenient control that uses a button to open and close the #Callout. In addition to controlling when the #Callout is opened, the DropDownController also fires close events when the user taps outside the hit area of the #CalloutButton.

Usage Scenarios

Andy is creating a tablet application for DJs to mix sound tracks and effects. To quickly access equalizer controls, he wants to add a "EQ" button to the interface that shows a pop up with multiple sliders. To do this he uses a #CalloutButton and sets the label to "EQ". In the pop up container, he adds VSliders for each equalizer setting.

Sergey creating a calendar application and wants to make it easy to add an appointment without leaving the main calendar UI. When the user clicked on a day of the month, a #Callout is opened with the arrow pointing exactly at the x/y position where the user clicked.

Detailed Description

Callout

The #Callout is a subclass of SkinnablePopUpContainer. It adds an optional arrow:``UIComponent skin part to visually indicate a direction toward the #Callout's owner.

#Callout is opened using the open() method inherited from SkinnablePopUpContainer. A new updatePopUpPosition() method is added as a hook to SkinnablePopUpContainer to allow subclasses to position the component before any transitions occur. This method may also be called at any time to reposition the pop-up for external changes such as size or position changes in the owner or it's ancestors.

2 properties for horizontalPosition and verticalPosition define the placement of #Callout relative to it's owner. The properties determining the arrowDirection (up, down, left, right or none) based on the the position of the pop-up relative to the owner.

The #Callout can automatically adjust the horizontalPosition and verticalPosition values in order to fit the pop up on screen. For any adjustment, the size and display list of the #Callout skin is invalidated in order to update the size based on the new arrowDirection.

When the #Callout updates, it positions the arrow to align with the owner. Similar to how a ScrollBar positions a thumb along a vertical or horizontal track, the #Callout positions the arrow along the rectangular bounds of the skin.

Arrow skin part

The arrow skin part is an ILayoutElement used to show the direction toward the owner. The skin is responsible for drawing this skin part with the desired arrowDirection. Note that corner alignments are not supported because the arrow is always positioned outside the rectangular bounds of the skin.

Arrow Positioning and Availability

The table below shows the arrowDirection value based on verticalPosition and horizontalPosition. The center and corner positions use ArrowDirection.NONE.

verticalPosition / horizontalPosition
Before
Start
Middle
End
After

Before
none
down
down
down
none

Start
right
left
up
right
left

Middle
right
left
none
right
left

End
right
left
down
right
left

After
none
up
up
up
none

Repositioning on Orientation Change

When opened, Callout lists for RESIZE events on the system manager. This allows the Callout to reposition itself and the arrow when the system manager resizes due to a StageOrientationEvent.

CalloutButton

[#CalloutButton] extends Button and acts as a "drop down component" (see Spark DropDownList for other examples). This new component adds an optional skin part dropDown:IFactory (a ClassFactory that creates a Callout). Similarly, it uses a DropDownController to manage the logic for opening and closing the Callout.

#CalloutButton solves the common use case for opening a pop-up with arbitrary content from a button press. The #CalloutButton itself is used as the owner for the #Callout.

When a [#CalloutButton] is first opened, it instantiates a Callout instance (from the IFactory or a default instance if the dropDown skin part is not defined. When the [#CalloutButton] is closed, users can control the destruction of the callout instance via the calloutDestructionPolicy property. By default, [#CalloutButton] uses ContainerDestructionPolicy``.AUTO which will remove and null-out the callout instance. The contentGroup contents are still cached by the [SkinnableContainer].

Animation

#CalloutButton itself has only the states that it inherits from Button. Animation of the Callout is handled by #Callout's skin.

To support the mobile use case for orientation changes, this feature adds a new closeOnResize property to DropDownController. When true (default), the DropDownController closes the drop down. This behavior is consistent with Flex 4.x. CalloutButton sets this property to false. This allows the Callout to stay visible and reposition as necessary.

API Description

package spark.components
{

//--------------------------------------
//  Styles
//--------------------------------------

/**
 *  Appearance of the contentGroup. Valid MXML values are inset
 *  flat and transparent.
 *
 *  In ActionScript, you can use the following constants
 *  to set this property:
 *  ContentBackgroundAppearance.INSET,
 *  ContentBackgroundAppearance.FLAT and
 *  ContentBackgroundAppearance.NONE.
 */
<a href="Style%28name%3D%26quot%3BcontentBackgroundAppearance%26quot%3B%2C%20type%3D%26quot%3BString%26quot%3B%2C%20enumeration%3D%26quot%3Binset%2Cflat%2Cnone%26quot%3B%2C%20inherit%3D%26quot%3Bno%26quot%3B%29">Style(name="contentBackgroundAppearance", type="String", enumeration="inset,flat,none", inherit="no")</a>

public class Callout extends SkinnablePopUpContainer
{

<a href="Bindable">Bindable</a>
<a href="SkinPart%28required%3D%26quot%3Bfalse%26quot%3B%29">SkinPart(required="false")</a>

/**
 *  An optional skin part that visually connects the owner to the
 *  component.
 */
public var arrow:UIComponent;

<a href="Inspectable%28category%3D%26quot%3BGeneral%26quot%3B%2C%20enumeration%3D%26quot%3Bauto%2Cbefore%2Cstart%2Cmiddle%2Cend%2Cafter%26quot%3B%2C%20defaultValue%3D%26quot%3Bauto%26quot%3B%29">Inspectable(category="General", enumeration="auto,before,start,middle,end,after", defaultValue="auto")</a>

/**
 * Horizontal position of the pop-over relative to the owner point.
 *
 * @see spark.components.CalloutPosition
 */
public function get horizontalPosition():String
public function set horizontalPosition(value:String):void

/**
 * Resolved horizontal position when using CalloutPosition.AUTO or opposite
 * horizontal position (when using BEFORE/AFTER or START/END) when the callout
 * does not fit in on screen using the specified horizontalPosition.
 *
 * @see spark.components.CalloutPosition
 */
protected function get actualHorizontalPosition():String
protected function set actualHorizontalPosition(value:String):void

<a href="Inspectable%28category%3D%26quot%3BGeneral%26quot%3B%2C%20enumeration%3D%26quot%3Bauto%2Cbefore%2Cstart%2Cmiddle%2Cend%2Cafter%26quot%3B%2C%20defaultValue%3D%26quot%3Bauto%26quot%3B%29">Inspectable(category="General", enumeration="auto,before,start,middle,end,after", defaultValue="auto")</a>

/**
 * Vertical position of the pop-over relative to the owner point.
 *
 * @see spark.components.CalloutPosition
 */
public function get verticalPosition():String
public function set verticalPosition(value:String):void

/**
 * Resolved vertical position when using CalloutPosition.AUTO or opposite
 * vertical position (when using BEFORE/AFTER or START/END) when the callout
 * does not fit in on screen using the specified verticalPosition.
 *
 * @see spark.components.CalloutPosition
 */
protected function get actualVerticalPosition():String
protected function set actualVerticalPosition(value:String):void

/**
 * Sets the bounds of arrow, whose geometry isn't fully
 * specified by the skin's layout.
 */
protected function updateSkinDisplayList():void

/**
 * @see spark.components.SkinnablePopUpContainer#updatePopUpPosition()
 */
override public function updatePopUpPosition():void

}

package spark.components
{
public class SkinnablePopUpContainer extends SkinnableContainer
{

/**
 *  Positions the pop-up after the pop-up is added to PopUpManager but
 *  before any state transitions occur. The base implementation of open()
 *  calls updatePopUpPosition() immediately after the pop-up is added.
 *
 *  This method may also be called at any time to update the pop-up's
 *  position. Pop-ups that are positioned relative to their owner should
 *  call this method after position or size changes occur on the owner or
 *  it's ancestors.
 */
protected function updatePopUpPosition():void

}
}

package spark.components
{
/**
 * Enumeration of arrow directions for the Callout component.
 */
public class ArrowDirection
{
//--------------------------------------------------------------------------
//
//  Class constants
//
//--------------------------------------------------------------------------

/**
 * Point right towards the Callout owner
 */
public static const RIGHT:String = "right";

/**
 * Point up towards the Callout owner
 */
public static const UP:String = "up";

/**
 * Point left towards the Callout owner
 */
public static const LEFT:String = "left";

/**
 * Point down towards the Callout owner
 */
public static const DOWN:String = "down";

/**
 * Arrow is not visible due to corner or center placement relative to the
 * Callout owner
 */
public static const NONE:String = "none";
}
}

package spark.components
{
/**
 * The ContentBackgroundAppearance class defines the constants for the allowed values of the contentBackgroundAppearance style of Callout.
 */
public class ContentBackgroundAppearance
{
//--------------------------------------------------------------------------
//
//  Class constants
//
//--------------------------------------------------------------------------

/**
 *  Applies a shadow and mask to the contentGroup.
 */
public static const INSET:String = "inset";

/**
 *  Applies mask to the contentGroup.
 */
public static const FLAT:String = "flat";

/**
 *  Disables both the contentBackgroundColor style and
 *  contentGroup masking. Use this value when Callout's contents should
 *  appear directly on top of the backgroundColor or when
 *  contents provide their own masking.
 */
public static const NONE:String = "none";
}

package spark.components
{

/**
 * Position of the Callout relative to the owner.
 */
public final class CalloutPosition
{

/**
 * Position the trailing edge of the popUp before the leading edge of the owner
 */
public static const BEFORE:String = "before";

/**
 * Position the leading edge of the popUp at the leading edge of the owner
 *
 */
public static const START:String = "start";

/**
 * Position the horizontalCenter of the popUp to the horizontalCenter of the owner
 */
public static const MIDDLE:String = "middle";

/**
 * Position the trailing edge of the popUp at the trailing edge of the owner
 */
public static const END:String = "end";

/**
 * Position the leading edge of the popUp after the trailing edge of the owner
 */
public static const AFTER:String = "after";

/**
 * Position the popUp on the exterior of the owner where the popUp
 * requires the least amount of resizing to fit.
 */
public static const AUTO:String = "auto";

}

}

package spark.components
{
public class DropDownController extends EventDispatcher
{

/**
 * When true, resizing the system manager will close the the drop down.
 *
 * @default true
 */
public function get closeOnResize():Boolean
public function set closeOnResize(value:Boolean):void

}
}

package spark.components
{

/**
 *  Dispatched when the drop-down closes for any reason, such when
 *  the user:
 *  <ul>
 *      <li>The drop-down is programmatically closed.</li>
 *      <li>Clicks outside of the drop-down.</li>
 *      <li>Clicks the open button while the drop-down is
 *  displayed.</li>
 *  </ul>
 *
 *  @eventType spark.events.DropDownEvent.CLOSE
 */
<a href="Event%28name%3D%26quot%3Bclose%26quot%3B%2C%20type%3D%26quot%3Bspark.events.DropDownEvent%26quot%3B%29">Event(name="close", type="spark.events.DropDownEvent")</a>

/**
 *  Dispatched when the user clicks the open button
 *  to display the drop-down.
 *
 *  @eventType spark.events.DropDownEvent.OPEN
 */
<a href="Event%28name%3D%26quot%3Bopen%26quot%3B%2C%20type%3D%26quot%3Bspark.events.DropDownEvent%26quot%3B%29">Event(name="open", type="spark.events.DropDownEvent")</a>

<a href="DefaultProperty%28%26quot%3BcalloutContent%26quot%3B%29">DefaultProperty("calloutContent")</a>

public class CalloutButton extends Button
{

<a href="SkinPart%28required%3D%26quot%3Bfalse%26quot%3B%29">SkinPart(required="false")</a>

/**
 *  A skin part that defines the drop-down area. When the CalloutButton is open,
 *  clicking anywhere outside of the dropDown skin part closes the drop-down.
 * If dropDown is not defined on the skin, a default Callout instance is created.
 */
public var dropDown:IFactory;

/**
 * The set of components to include in the Callout's content.
 */
public function get calloutContent():Array
public function set calloutContent(value:Array):void

/**
 * Defines the layout of the Callout.
 */
public function get calloutLayout():LayoutBase
public function set calloutLayout(value:LayoutBase):void

/**
 *  The Callout instance created after the <code>DropDownEvent.OPEN</code> is
 *  triggered. The instance is created using the <code>dropDown</code>
 *  <code>IFactory</code> skin part.
 */
public function get callout():Callout

<a href="Inspectable%28category%3D%26quot%3BGeneral%26quot%3B%2C%20enumeration%3D%26quot%3Bauto%2Cnever%26quot%3B%2C%20defaultValue%3D%26quot%3Bauto%26quot%3B%29">Inspectable(category="General", enumeration="auto,never", defaultValue="auto")</a>

/**
 *  Defines the destruction policy the callout button should use
 *  when the callout is closed. If set to "auto", the button will
 *  destroy the callout when it is closed.  If set to "never", the
 *  callout will be cached in memory.
 */
public function get calloutDestructionPolicy():String
public function set calloutDestructionPolicy(value:String):void

<a href="Inspectable%28category%3D%26quot%3BGeneral%26quot%3B%2C%20enumeration%3D%26quot%3Bauto%2Cbefore%2Cstart%2Cmiddle%2Cend%2Cafter%26quot%3B%2C%20defaultValue%3D%26quot%3Bauto%26quot%3B%29">Inspectable(category="General", enumeration="auto,before,start,middle,end,after", defaultValue="auto")</a>

/**
 * Horizontal position of the pop-over relative to the owner point.
 *
 * @see spark.components.CalloutPosition
 */
public function get horizontalPosition():String
public function set horizontalPosition(value:String):void

<a href="Inspectable%28category%3D%26quot%3BGeneral%26quot%3B%2C%20enumeration%3D%26quot%3Bauto%2Cbefore%2Cstart%2Cmiddle%2Cend%2Cafter%26quot%3B%2C%20defaultValue%3D%26quot%3Bauto%26quot%3B%29">Inspectable(category="General", enumeration="auto,before,start,middle,end,after", defaultValue="auto")</a>

/**
 * Vertical position of the pop-over relative to the owner point.
 *
 * @see spark.components.CalloutPosition
 */
public function get verticalPosition():String
public function set verticalPosition(value:String):void

/**
 *  Instance of the DropDownController class that handles all of the mouse, keyboard
 *  and focus user interactions.
 */
protected function get dropDownController():DropDownController;
protected function set dropDownController(value:DropDownController):void;

/**
 *  Open the drop-down and dispatch a DropDownEvent.OPEN event.
 */
public function openDropDown():void;

/**
 *  Close the drop-down and dispatch a DropDownEvent.CLOSE event.
 */
public function closeDropDown():void;

}

}

package spark.skins.mobile
{
/**
 *  The default skin class for the Spark Callout component in mobile
 *  applications.
 *
 *  <p>The <code>contentGroup</code> lies above a <code>backgroundColor</code> fill
 *  which frames the <code>contentGroup</code>. The position and size of the frame
 *  adjust based on the host component <code>arrowDirection</code>, leaving
 *  space for the <code>arrow</code> to appear on the outside edge of the
 *  frame.</p>
 *
 *  <p>The <code>arrow</code> skin part is not positioned by the skin. Instead,
 *  the Callout component positions the arrow relative to the owner in
 *  <code>updateSkinDisplayList()</code>. This method assumes that Callout skin
 *  and the <code>arrow</code> use the same coordinate space.</p>
 */
public class CalloutSkin extends MobileSkin
{

/**
 *  @copy spark.skins.spark.ApplicationSkin#hostComponent
 */
public var hostComponent:Callout;

/**
 *  @copy spark.components.SkinnableContainer#contentGroup
 */
public var contentGroup:Group;

/**
 * @copy spark.components.Callout#arrow
 */
public var arrow:UIComponent;

/**
 *  Enables a RectangularDropShadow behind the <code>backgroundColor</code> frame.
 */
protected var dropShadowVisible = true;

/**
 *  Enables a vertical linear gradient in the <code>backgroundColor</code> frame. This
 *  gradient fill is drawn across both the arrow and the frame. By default,
 *  the gradient brightens the background color by 15% and darkens it by 60%.
 *
 *  @default true
 */
protected var useBackgroundGradient:Boolean = true;

/**
 *  Corner radius used for the <code>contentBackgroundColor</code> fill.
 */
protected var contentCornerRadius:uint;

/**
 *  Corner radius of the <code>backgroundColor</code> "frame". Also controls the inset
 *  of the <code>contentGroup</code>.
 */
protected var backgroundCornerRadius:Number;

/**
 *  The thickness of the <code>backgroundColor</code> "frame" that surrounds the
 *  <code>contentGroup</code>.
 */
protected var frameThickness:Number;

/**
 *  Color of the border stroke around the <code>backgroundColor</code> "frame".
 */
protected var borderColor:Number = 0;

/**
 *  Thickness of the border stroke around the <code>backgroundColor</code>
 *  "frame".
 */
protected var borderThickness:Number = NaN;

/**
 *  Width of the arrow in vertical directions. This property also controls
 *  the height of the arrow in horizontal directions.
 */
protected var arrowWidth:Number;

/**
 *  Height of the arrow in vertical directions. This property also controls
 *  the width of the arrow in horizontal directions.
 */
protected var arrowHeight:Number;
}
}

package spark.skins.mobile.supportClasses
{
/**
 *  The arrow skin part for CalloutSkin.
 */
public class CalloutArrow extends UIComponent
{

/**
 *  A gap on the frame-adjacent side of the arrow graphic to avoid
 *  drawing past the CalloutSkin backgroundCornerRadius.
 *
 *  @see spark.skins.mobile.CalloutSkin#backgroundCornerRadius
 */
protected var gap:Number;

/**
 *  @copy spark.skins.mobile.CalloutSkin#useBackgroundGradient
 */
protected var useBackgroundGradient:Boolean;

/**
 *  @copy spark.skins.mobile.CalloutSkin#borderColor
 */
protected var borderColor:Number;

/**
 *  @copy spark.skins.mobile.CalloutSkin#borderThickness
 */
protected var borderThickness:Number = NaN;
}
}

B Features

None

Examples and Usage

<s:HGroup top="20" right="20">

  <s:Button label="Mute" click="handleMute(event)"/>

  <!-- "EQ" button at the top-right of an application with a popUp appearing below it -->
  <s:CalloutButton
    verticalPosition="after"
    horizontalPosition="end"
    icon="@Embed('equalizer.png')"
    label="EQ"
    close="handleEqualizerClosed(event)">

    <s:calloutLayout>
      <s:VerticalLayout/>
    </s:calloutLayout>

    <custom:SliderControls/>
    <custom:GraphicEqualizer/>

  </s:CalloutButton>

</s:HGroup>

<mobile:ButtonSkin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mobile="spark.skins.mobile.*">
    <fx:Declarations>
        <fx:Component id="dropDown">
            <s:Callout skinClass="skins.SimpleCalloutSkin" backgroundColor="0xFFFFFF"/>
        </fx:Component>
    </fx:Declarations>
</mobile:ButtonSkin>

private function button_clickHandler(event:MouseEvent):void
{
  // close triggers for the pop over
  systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_DOWN,
      systemManager_mouseDownHandler);
  systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_DOWN_SOMEWHERE,
      systemManager_mouseDownHandler);

  // open a Callout in response to a mouse click
  popUp = new MyToolTip(); /* MyToolTip extends Callout */

  // position the popUp to the right if the click is on the left side of the stage
  popUp.horizontalPosition = (event.stageX < (stage.stageWidth / 2))
      ? CalloutPosition.AFTER : CalloutPosition.BEFORE;
  popUp.verticalPosition = CalloutPosition.MIDDLE;

  popUp.open(button, false);
}

Additional Implementation Details

N/A

Compiler Work

N/A

Cross-Form-Factor Considerations

The scope of this feature targets mobile projects only, primarily tablet form factors. Phone form factor usage is possible, but is better addressed by a generalized Dialog feature (in a future version of Flex) or built through other means such as SkinnablePopUpContainer. Addition of desktop support should consider usage with modules.

Cross-Platform Considerations

With respect to tablets, PopOvers are widely used on the iPad. Android Honeycomb has a similar drop down container opened via a button on the ActionBar. The native Android Honeycomb pop over does not show a tail.

Backwards Compatibility

Syntax changes

None

Behavior

None

Warnings/Deprecation

None

Accessibility

None

Performance

None

Globalization

None

Localization

None

Issues and Recommendations

  1. With #Callout, how do you deal with margin/padding affordances at the edges of the stage?
    1. Use built-in padding in your #Callout skins.

Related

Wiki: Flex 4.6
Wiki: SkinnablePopUpContainer
Wiki: SplitViewNavigator

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.