////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package spark.components
{
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.events.Event;
import flash.geom.ColorTransform;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
import mx.core.DPIClassification;
import mx.core.FlexGlobals;
import mx.core.ILayoutElement;
import mx.core.IVisualElement;
import mx.core.LayoutDirection;
import mx.core.UIComponent;
import mx.core.mx_internal;
import mx.events.ResizeEvent;
import mx.managers.SystemManager;
import mx.utils.MatrixUtil;
import mx.utils.PopUpUtil;
use namespace mx_internal;
//--------------------------------------
// Styles
//--------------------------------------
/**
* Appearance of the contentGroup
.
* Valid MXML values are inset
,
* flat
, and none
.
*
*
In ActionScript, you can use the following constants
* to set this property:
* ContentBackgroundAppearance.INSET
,
* ContentBackgroundAppearance.FLAT
and
* ContentBackgroundAppearance.NONE
.
arrow
skin part that visually
* displays the direction toward the owner.
*
* The following image shows a Callout container labeled 'Settings':
* ** *
* *You can also use the CalloutButton control to open a callout container. * The CalloutButton control encapsulates in a single control the callout container * and all of the logic necessary to open and close the callout. * The CalloutButton control is then said to the be the owner, or host, * of the callout.
* *Callout uses the horizontalPosition
and
* verticalPosition
properties to determine the position of the
* Callout relative to the owner that is specified by the open()
* method.
* Both properties can be set to CalloutPosition.AUTO
which selects a
* position based on the aspect ratio of the screen for the Callout to fit
* with minimal overlap with the owner and and minimal adjustments at the
* screen bounds.
Once positioned, the Callout positions the arrow on the side adjacent * to the owner, centered as close as possible on the horizontal or vertical * center of the owner as appropriate. The arrow is hidden in cases where * the Callout position is not adjacent to any edge.
* *You do not create a Callout container as part of the normal layout * of its parent container. * Instead, it appears as a pop-up container on top of its parent. * Therefore, you do not create it directly in the MXML code of your application.
* *Instead, you create is as an MXML component, often in a separate MXML file.
* To show the component create an instance of the MXML component, and
* then call the open()
method.
* You can also set the size and position of the component when you open it.
To close the component, call the close()
method.
* If the pop-up needs to return data to a handler, you can add an event listener for
* the PopUp.CLOSE
event, and specify the returned data in
* the close()
method.
The Callout is initially in its closed
skin state.
* When it opens, it adds itself as a pop-up to the PopUpManager,
* and transition to the normal
skin state.
* To define open and close animations, use a custom skin with transitions between
* the closed
and normal
skin states.
Callout changes the default inheritance behavior seen in Flex components * and instead, inherits styles from the top-level application. This prevents * Callout's contents from unintentionally inheriting styles from an owner * (i.e. Button or TextInput) where the default appearance was desired and * expected.
* *The Callout container has the following default characteristics:
*Characteristic | Description |
---|---|
Default size | Large enough to display its children |
Minimum size | 0 pixels |
Maximum size | 10000 pixels wide and 10000 pixels high |
Default skin class | spark.skins.mobile.CalloutSkin |
The <s:Callout>
tag inherits all of the tag
* attributes of its superclass and adds the following tag attributes:
* <s:Callout * Properties * horizontalPosition="auto" * verticalPosition="auto" * * Styles * contentBackgroundAppearance="inset" * /> ** * @see spark.components.CalloutButton * @see spark.skins.mobile.CalloutSkin * @see spark.components.ContentBackgroundAppearance * @see spark.components.CalloutPosition * * @includeExample examples/CalloutExample.mxml -noswf * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public class Callout extends SkinnablePopUpContainer { //-------------------------------------------------------------------------- // // Class constants // //-------------------------------------------------------------------------- private static var decomposition:Vector.
Possible values are "before"
, "start"
,
* "middle"
, "end"
, "after"
,
* and "auto"
(default).
Update this property in commitProperties()
when the
* explicit horizontalPosition
is CalloutPosition.AUTO.
* This property must be updated in updatePopUpPosition()
* when attempting to reposition the Callout.
Subclasses should read this property when computing the arrowDirection
,
* the arrow position in updateSkinDisplayList()
.
Possible values are "before"
, "start"
,
* "middle"
, "end"
, "after"
,
* and "auto"
(default).
Update this property in commitProperties()
when the
* explicit verticalPosition
is CalloutPosition.AUTO.
* This property must be updated in updatePopUpPosition()
* when attempting to reposition the Callout.
Subclasses should read this property when computing the arrowDirection
,
* the arrow position in updateSkinDisplayList()
.
This value is computed based on the callout position given by
* horizontalPosition
and verticalPosition
.
* Exterior and interior positions will point from the callout towards
* the edge of the owner. Corner and absolute center positions are not
* supported and will return a value of "none".
arrow
, whose geometry isn't fully
* specified by the skin's layout.
*
* Subclasses can override this method to update the arrow's size,
* position, and visibility, based on the computed
* arrowDirection
.
By default, this method aligns the arrow on the shorter of either
* the arrow
bounds or the owner
bounds. This
* implementation assumes that the arrow
and the Callout skin
* share the same coordinate space.
margin
so that
* the Callout is not positioned in the margin.
*
* arrowDirection
will change if required for the callout
* to fit.
*
* @see #margin
*/
mx_internal function calculatePopUpPosition():Point
{
// This implementation doesn't handle rotation
var sandboxRoot:DisplayObject = systemManager.getSandboxRoot();
var matrix:Matrix = MatrixUtil.getConcatenatedMatrix(owner, sandboxRoot);
var regPoint:Point = new Point();
if (!matrix)
return regPoint;
var adjustedHorizontalPosition:String;
var adjustedVerticalPosition:String;
var calloutBounds:Rectangle = determinePosition(actualHorizontalPosition,
actualVerticalPosition, matrix, regPoint);
var ownerBounds:Rectangle = owner.getBounds(systemManager.getSandboxRoot());
// Position the callout in the opposite direction if it
// does not fit on the screen.
if (screen)
{
adjustedHorizontalPosition = adjustCalloutPosition(
actualHorizontalPosition, horizontalPosition,
calloutBounds.left, calloutBounds.right,
screen.left, screen.right,
ownerBounds.left, ownerBounds.right);
adjustedVerticalPosition = adjustCalloutPosition(
actualVerticalPosition, verticalPosition,
calloutBounds.top, calloutBounds.bottom,
screen.top, screen.bottom,
ownerBounds.top, ownerBounds.bottom);
}
var oldArrowDirection:String = arrowDirection;
var actualArrowDirection:String = null;
// Reset arrowDirectionAdjusted
arrowDirectionAdjusted = false;
// Get the new registration point based on the adjusted position
if ((adjustedHorizontalPosition != null) || (adjustedVerticalPosition != null))
{
var adjustedRegPoint:Point = new Point();
var tempHorizontalPosition:String = (adjustedHorizontalPosition)
? adjustedHorizontalPosition : actualHorizontalPosition;
var tempVerticalPosition:String = (adjustedVerticalPosition)
? adjustedVerticalPosition : actualVerticalPosition;
// Adjust arrow direction after adjusting position
actualArrowDirection = determineArrowPosition(tempHorizontalPosition,
tempVerticalPosition);
// All position flips gaurantee an arrowDirection change
setArrowDirection(actualArrowDirection);
arrowDirectionAdjusted = true;
if (arrow)
arrow.visible = (arrowDirection != ArrowDirection.NONE);
// Reposition the arrow
updateSkinDisplayList();
var adjustedBounds:Rectangle = determinePosition(tempHorizontalPosition,
tempVerticalPosition, matrix, adjustedRegPoint);
if (screen)
{
// If we adjusted the position but the callout still doesn't fit,
// then revert to the original position.
adjustedHorizontalPosition = adjustCalloutPosition(
adjustedHorizontalPosition, horizontalPosition,
adjustedBounds.left, adjustedBounds.right,
screen.left, screen.right,
ownerBounds.left, ownerBounds.right,
true);
adjustedVerticalPosition = adjustCalloutPosition(
adjustedVerticalPosition, verticalPosition,
adjustedBounds.top, adjustedBounds.bottom,
screen.top, screen.bottom,
ownerBounds.top, ownerBounds.bottom,
true);
}
if ((adjustedHorizontalPosition != null) || (adjustedVerticalPosition != null))
{
regPoint = adjustedRegPoint;
calloutBounds = adjustedBounds;
// Temporarily set actual positions to reposition the arrow
if (adjustedHorizontalPosition)
actualHorizontalPosition = adjustedHorizontalPosition;
if (adjustedVerticalPosition)
actualVerticalPosition = adjustedVerticalPosition;
// Reposition the arrow with the new actual position
updateSkinDisplayList();
}
else
{
// Restore previous arrow direction *before* reversing the
// adjusted positions
setArrowDirection(oldArrowDirection);
arrowDirectionAdjusted = false;
// Reposition the arrow to the original position
updateSkinDisplayList();
}
}
MatrixUtil.decomposeMatrix(decomposition, matrix, 0, 0);
var concatScaleX:Number = decomposition[3];
var concatScaleY:Number = decomposition[4];
// If the callout still doesn't fit, then nudge it
// so it is completely on the screen. Make sure to include scale.
var screenTop:Number = screen.top;
var screenBottom:Number = screen.bottom;
var screenLeft:Number = screen.left;
var screenRight:Number = screen.right;
// Allow zero margin on the the side with the arrow
switch (arrowDirection)
{
case ArrowDirection.UP:
{
screenBottom -= margin;
screenLeft += margin;
screenRight -= margin
break;
}
case ArrowDirection.DOWN:
{
screenTop += margin;
screenLeft += margin;
screenRight -= margin
break;
}
case ArrowDirection.LEFT:
{
screenTop += margin;
screenBottom -= margin;
screenRight -= margin
break;
}
case ArrowDirection.RIGHT:
{
screenTop += margin;
screenBottom -= margin;
screenLeft += margin;
break;
}
default:
{
screenTop += margin;
screenBottom -= margin;
screenLeft += margin;
screenRight -= margin
break;
}
}
regPoint.y += nudgeToFit(calloutBounds.top, calloutBounds.bottom,
screenTop, screenBottom, concatScaleY);
regPoint.x += nudgeToFit(calloutBounds.left, calloutBounds.right,
screenLeft, screenRight, concatScaleX);
// Compute the stage coordinates of the upper,left corner of the PopUp, taking
// the postTransformOffsets - which include mirroring - into account.
// If we're mirroring, then the implicit assumption that x=left will fail,
// so we compensate here.
if (layoutDirection == LayoutDirection.RTL)
regPoint.x += calloutBounds.width;
return MatrixUtil.getConcatenatedComputedMatrix(owner, sandboxRoot).transformPoint(regPoint);
}
/**
* @private
* Computes actualHorizontalPosition
and/or
* actualVerticalPosition
values when using
* CalloutPosition.AUTO
. When implementing subclasses of
* Callout, use actualHorizontalPosition
and
* actualVerticalPosition
to compute
* arrowDirection
and positioning in
* updatePopUpPosition()
and updateSkinDisplayList()
.
*
* The default implementation chooses "outer" positions for the callout * such that the owner is not obscured. Horizontal/Vertical orientation * relative to the owner choosen based on the aspect ratio.
* *When the aspect ratio is landscape, and the callout can fit to the
* left or right of the owner, actualHorizontalPosition
is
* set to CalloutPosition.BEFORE
or
* CalloutPosition.AFTER
as appropriate.
* actualVerticalPosition
is set to
* CalloutPosition.MIDDLE
to have the vertical center of the
* callout align to the vertical center of the owner.
When the aspect ratio is portrait, and the callout can fit
* above or below the owner, actualVerticalPosition
is
* set to CalloutPosition.BEFORE
or
* CalloutPosition.AFTER
as appropriate.
* actualHorizontalPosition
is set to
* CalloutPosition.MIDDLE
to have the horizontal center of the
* callout align to the horizontal center of the owner.
Subclasses may override to modify automatic positioning behavior.
*/ mx_internal function commitAutoPosition():void { if (!screen || ((horizontalPosition != CalloutPosition.AUTO) && (verticalPosition != CalloutPosition.AUTO))) { // Use explicit positions instead of AUTO actualHorizontalPosition = null; actualVerticalPosition = null; return; } var ownerBounds:Rectangle = owner.getBounds(systemManager.getSandboxRoot()); // Use aspect ratio to determine vertical/horizontal preference var isLandscape:Boolean = (screen.width > screen.height); // Exterior space var exteriorSpaceLeft:Number = Math.max(0, ownerBounds.left); var exteriorSpaceRight:Number = Math.max(0, screen.width - ownerBounds.right); var exteriorSpaceTop:Number = Math.max(0, ownerBounds.top); var exteriorSpaceBottom:Number = Math.max(0, screen.height - ownerBounds.bottom); if (verticalPosition != CalloutPosition.AUTO) { // Horizontal auto only switch (verticalPosition) { case CalloutPosition.START: case CalloutPosition.MIDDLE: case CalloutPosition.END: { actualHorizontalPosition = (exteriorSpaceRight > exteriorSpaceLeft) ? CalloutPosition.AFTER : CalloutPosition.BEFORE; break; } default: { actualHorizontalPosition = CalloutPosition.MIDDLE; break; } } actualVerticalPosition = null; } else if (horizontalPosition != CalloutPosition.AUTO) { // Vertical auto only switch (horizontalPosition) { case CalloutPosition.START: case CalloutPosition.MIDDLE: case CalloutPosition.END: { actualVerticalPosition = (exteriorSpaceBottom > exteriorSpaceTop) ? CalloutPosition.AFTER : CalloutPosition.BEFORE; break; } default: { actualVerticalPosition = CalloutPosition.MIDDLE; break; } } actualHorizontalPosition = null; } else // if ((verticalPosition == CalloutPosition.AUTO) && (horizontalPosition == CalloutPosition.AUTO)) { if (!isLandscape) { // Arrow will be vertical when in portrait actualHorizontalPosition = CalloutPosition.MIDDLE; actualVerticalPosition = (exteriorSpaceBottom > exteriorSpaceTop) ? CalloutPosition.AFTER : CalloutPosition.BEFORE; } else { // Arrow will be horizontal when in landscape actualHorizontalPosition = (exteriorSpaceRight > exteriorSpaceLeft) ? CalloutPosition.AFTER : CalloutPosition.BEFORE; actualVerticalPosition = CalloutPosition.MIDDLE; } } } /** * @private * Return true if user-specified max size if set */ mx_internal function get isMaxSizeSet():Boolean { var explicitMaxW:Number = super.explicitMaxWidth; var explicitMaxH:Number = super.explicitMaxHeight; return (!isNaN(explicitMaxW) && !isNaN(explicitMaxH)); } /** * @private * Return the original height if the soft keyboard is active. This height * is used to stabilize AUTO positioning so that the position is based * on the original height of the Callout instead of a possibly shorter * height due to soft keyboard effects. */ mx_internal function get calloutHeight():Number { return (isSoftKeyboardEffectActive) ? softKeyboardEffectCachedHeight : getLayoutBoundsHeight(); } /** * @private * Compute max width and max height. Uses the the owner and screen bounds * as well as preferred positions to determine max width and max height * for all possible exterior and interior positions. */ mx_internal function commitMaxSize():void { var ownerBounds:Rectangle = owner.getBounds(systemManager.getSandboxRoot()); var ownerLeft:Number = ownerBounds.left; var ownerRight:Number = ownerBounds.right; var ownerTop:Number = ownerBounds.top; var ownerBottom:Number = ownerBounds.bottom; var maxW:Number; var maxH:Number; switch (actualHorizontalPosition) { case CalloutPosition.MIDDLE: { // Callout matches screen width maxW = screen.width - (margin * 2); break; } case CalloutPosition.START: case CalloutPosition.END: { // Flip left and right when using inner positions ownerLeft = ownerBounds.right; ownerRight = ownerBounds.left; // Fall through } default: { // Maximum is the larger of the actual position or flipped position maxW = Math.max(ownerLeft, screen.right - ownerRight) - margin; break; } } // If preferred position was AUTO, then allow maxWidth to grow to // fit the interior position if the owner is wide if ((horizontalPosition == CalloutPosition.AUTO) && (ownerBounds.width > maxW)) maxW += ownerBounds.width; switch (actualVerticalPosition) { case CalloutPosition.MIDDLE: { // Callout matches screen height maxH = screen.height - (margin * 2); break; } case CalloutPosition.START: case CalloutPosition.END: { // Flip top and bottom when using inner positions ownerTop = ownerBounds.bottom; ownerBottom = ownerBounds.top; // Fall through } default: { // Maximum is the larger of the actual position or flipped position maxH = Math.max(ownerTop, screen.bottom - ownerBottom) - margin; break; } } // If preferred position was AUTO, then allow maxHeight to grow to // fit the interior position if the owner is tall if ((verticalPosition == CalloutPosition.AUTO) && (ownerBounds.height > maxH)) maxH += ownerBounds.height; calloutMaxWidth = maxW; calloutMaxHeight = maxH; } /** * @private */ mx_internal function determineArrowPosition(horizontalPos:String, verticalPos:String):String { // Determine arrow direction, outer positions get priority. // Corner positions and center show no arrow var direction:String = ArrowDirection.NONE; if (horizontalPos == CalloutPosition.BEFORE) { if ((verticalPos != CalloutPosition.BEFORE) && (verticalPos != CalloutPosition.AFTER)) { direction = ArrowDirection.RIGHT; } } else if (horizontalPos == CalloutPosition.AFTER) { if ((verticalPos != CalloutPosition.BEFORE) && (verticalPos != CalloutPosition.AFTER)) { direction = ArrowDirection.LEFT; } } else if (verticalPos == CalloutPosition.BEFORE) { direction = ArrowDirection.DOWN; } else if (verticalPos == CalloutPosition.AFTER) { direction = ArrowDirection.UP; } else if (horizontalPos == CalloutPosition.START) { direction = ArrowDirection.LEFT; } else if (horizontalPos == CalloutPosition.END) { direction = ArrowDirection.RIGHT; } else if (verticalPos == CalloutPosition.START) { direction = ArrowDirection.UP; } else if (verticalPos == CalloutPosition.END) { direction = ArrowDirection.DOWN; } return direction } /** * @private * * Uses horizontalPosition and verticalPosition to determine the bounds of * the callout. */ mx_internal function determinePosition(horizontalPos:String, verticalPos:String, matrix:Matrix, registrationPoint:Point):Rectangle { var ownerVisualElement:ILayoutElement = owner as ILayoutElement; var ownerWidth:Number = (ownerVisualElement) ? ownerVisualElement.getLayoutBoundsWidth() : owner.width; var ownerHeight:Number = (ownerVisualElement) ? ownerVisualElement.getLayoutBoundsHeight() : owner.height; var calloutWidth:Number = getLayoutBoundsWidth(); var calloutHeight:Number = this.calloutHeight; switch (horizontalPos) { case CalloutPosition.BEFORE: { // The full width of the callout is before the owner // All arrow directions are ArrowDirection.RIGHT x=(width - arrow.width) registrationPoint.x = -calloutWidth; break; } case CalloutPosition.START: { // ArrowDirection.LEFT is at x=0 registrationPoint.x = 0; break; } case CalloutPosition.END: { // The ends of the owner and callout are aligned registrationPoint.x = (ownerWidth - calloutWidth); break; } case CalloutPosition.AFTER: { // The full width of the callout is after the owner // All arrow directions are ArrowDirection.LEFT (x=0) registrationPoint.x = ownerWidth; break; } default: // case CalloutPosition.MIDDLE: { registrationPoint.x = Math.floor((ownerWidth - calloutWidth) / 2); break; } } switch (verticalPos) { case CalloutPosition.BEFORE: { // The full height of the callout is before the owner // All arrow directions are ArrowDirection.DOWN y=(height - arrow.height) registrationPoint.y = -calloutHeight; break; } case CalloutPosition.START: { // ArrowDirection.UP is at y=0 registrationPoint.y = 0; break; } case CalloutPosition.MIDDLE: { registrationPoint.y = Math.floor((ownerHeight - calloutHeight) / 2); break; } case CalloutPosition.END: { // The ends of the owner and callout are aligned registrationPoint.y = (ownerHeight - calloutHeight); break; } default: //case CalloutPosition.AFTER: { // The full height of the callout is after the owner // All arrow directions are ArrowDirection.UP (y=0) registrationPoint.y = ownerHeight; break; } } var topLeft:Point = registrationPoint.clone(); var size:Point = MatrixUtil.transformBounds(calloutWidth, calloutHeight, matrix, topLeft); var bounds:Rectangle = new Rectangle(); bounds.left = topLeft.x; bounds.top = topLeft.y; bounds.width = size.x; bounds.height = size.y; return bounds; } /** * @private */ mx_internal function get isArrowVertical():Boolean { return (arrowDirection == ArrowDirection.UP || arrowDirection == ArrowDirection.DOWN); } //-------------------------------------------------------------------------- // // Event handlers // //-------------------------------------------------------------------------- /** * @private */ private function arrow_resizeHandler(event:Event):void { updateSkinDisplayList(); } /** * @private */ private function systemManager_resizeHandler(event:Event):void { // Remove explicit settings if due to Resize effect softKeyboardEffectResetExplicitSize(); // Screen resize might require a new arrow direction and callout position invalidatePosition(); if (!isSoftKeyboardEffectActive) { // Force validation and use new screen size only if the keyboard // effect is not active. The stage dimensions may be invalid while // the soft keyboard is active. See SDK-31860. validateNow(); } } } }