blob: b526294b5d757fb233499b42884c328d72ddfb01 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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.layouts
{
import flash.events.Event;
import flash.geom.Point;
import flash.geom.Rectangle;
import mx.containers.utilityClasses.Flex;
import mx.core.FlexVersion;
import mx.core.ILayoutElement;
import mx.core.IVisualElement;
import mx.core.mx_internal;
import mx.events.PropertyChangeEvent;
import spark.components.DataGroup;
import spark.components.supportClasses.GroupBase;
import spark.core.NavigationUnit;
import spark.layouts.supportClasses.DropLocation;
import spark.layouts.supportClasses.LayoutBase;
import spark.layouts.supportClasses.LayoutElementHelper;
import spark.layouts.supportClasses.LinearLayoutVector;
use namespace mx_internal;
/**
* The HorizontalLayout class arranges the layout elements in a horizontal sequence,
* left to right, with optional gaps between the elements and optional padding
* around the elements.
*
* <p>The horizontal position of the elements is determined by arranging them
* in a horizontal sequence, left to right, taking into account the padding
* before the first element and the gaps between the elements.</p>
*
* <p>The vertical position of the elements is determined by the layout's
* <code>verticalAlign</code> property.</p>
*
* <p>During the execution of the <code>measure()</code> method,
* the default size of the container is calculated by
* accumulating the preferred sizes of the elements, including gaps and padding.
* When the <code>requestedColumnCount</code> property is set to a value other than -1,
* only the space for that many elements
* is measured, starting from the first element.</p>
*
* <p>During the execution of the <code>updateDisplayList()</code> method,
* the width of each element is calculated
* according to the following rules, listed in their respective order of
* precedence (element's minimum width and maximum width are always respected):</p>
* <ul>
* <li>If <code>variableColumnWidth</code> is <code>false</code>,
* then set the element's width to the
* value of the <code>columnWidth</code> property.</li>
*
* <li>If the element's <code>percentWidth</code> is set, then calculate the element's
* width by distributing the available container width between all
* elements with <code>percentWidth</code> setting.
* The available container width
* is equal to the container width minus the gaps, the padding and the
* space occupied by the rest of the elements. The element's <code>precentWidth</code>
* property is ignored when the layout is virtualized.</li>
*
* <li>Set the element's width to its preferred width.</li>
* </ul>
*
* <p>The height of each element is calculated according to the following rules,
* listed in their respective order of precedence (element's minimum height and
* maximum height are always respected):</p>
* <ul>
* <li>If the <code>verticalAlign</code> property is <code>"justify"</code>,
* then set the element's height to the container height.</li>
*
* <li>If the <code>verticalAlign</code> property is <code>"contentJustify"</code>,
* then set the element's height to the maximum between the container's height
* and all elements' preferred height.</li>
*
* <li>If the element's <code>percentHeight</code> property is set,
* then calculate the element's height as a percentage of the container's height.</li>
*
* <li>Set the element's height to its preferred height.</li>
* </ul>
*
* @mxml
* <p>The <code>&lt;s:HorizontalLayout&gt;</code> tag inherits all of the tag
* attributes of its superclass and adds the following tag attributes:</p>
*
* <pre>
* &lt;s:HorizontalLayout
* <strong>Properties</strong>
* columnWidth="<i>calculated</i>"
* gap="6"
* paddingBottom="0"
* paddingLeft="0"
* paddingRight="0"
* paddingTop="0"
* requestedColumnCount="-1"
* requestedMaxColumnCount="-1"
* requestedMinColumnCount="-1"
* variableColumnWidth="true"
* verticalAlign="top"
* /&gt;
* </pre>
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class HorizontalLayout extends LayoutBase
{
include "../core/Version.as";
/**
* @private
* Cached column widths, max row height for virtual layout. Not used unless
* useVirtualLayout=true. See updateLLV(), resetCachedVirtualLayoutState(),
* etc.
*/
private var llv:LinearLayoutVector;
//--------------------------------------------------------------------------
//
// Class methods
//
//--------------------------------------------------------------------------
private static function calculatePercentHeight(layoutElement:ILayoutElement, height:Number):Number
{
var percentHeight:Number;
if (FlexVersion.compatibilityVersion < FlexVersion.VERSION_4_6)
{
percentHeight = LayoutElementHelper.pinBetween(Math.round(layoutElement.percentHeight * 0.01 * height),
layoutElement.getMinBoundsHeight(),
layoutElement.getMaxBoundsHeight() );
return percentHeight < height ? percentHeight : height;
}
else
{
percentHeight = LayoutElementHelper.pinBetween(Math.min(Math.round(layoutElement.percentHeight * 0.01 * height), height),
layoutElement.getMinBoundsHeight(),
layoutElement.getMaxBoundsHeight() );
return percentHeight;
}
}
private static function sizeLayoutElement(layoutElement:ILayoutElement, height:Number,
verticalAlign:String, restrictedHeight:Number,
width:Number, variableColumnWidth:Boolean,
columnWidth:Number):void
{
var newHeight:Number = NaN;
// if verticalAlign is "justify" or "contentJustify",
// restrict the height to restrictedHeight. Otherwise,
// size it normally
if (verticalAlign == VerticalAlign.JUSTIFY ||
verticalAlign == VerticalAlign.CONTENT_JUSTIFY)
{
newHeight = restrictedHeight;
}
else
{
if (!isNaN(layoutElement.percentHeight))
newHeight = calculatePercentHeight(layoutElement, height);
}
if (variableColumnWidth)
layoutElement.setLayoutBoundsSize(width, newHeight);
else
layoutElement.setLayoutBoundsSize(columnWidth, newHeight);
}
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function HorizontalLayout():void
{
super();
// Don't drag-scroll in the vertical direction
dragScrollRegionSizeVertical = 0;
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// alignmentBaseline
//----------------------------------
/**
* @private
* Storage for the alignmentBaseline property.
*/
private var _alignmentBaseline:Object = "maxAscent:0";
/**
* @private
*
* The base line of the layout, in pixels.
*
* The base line is the virtual horizontal line to which layout elements' text is aligned,
* it is relative to the top edge of the container plus any <code>paddingTop</code>.
*
* The base line can be specified as either an explicit number, or as a numberical offset from
* the computed maximum of the elements' text ascent by using the "maxAscent:offset" syntax.
*
* Note that <code>alignmentBaseline</code> has an effect only when <code>verticalAlign</code>
* is set to "baseline".
*
* @default "maxAscent:0"
* @see #verticalAlign
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
mx_internal function get alignmentBaseline():Object
{
return _alignmentBaseline;
}
/**
* @private
*/
mx_internal function set alignmentBaseline(value:Object):void
{
if (_alignmentBaseline == value)
return;
_alignmentBaseline = value;
invalidateTargetSizeAndDisplayList();
}
//----------------------------------
// gap
//----------------------------------
private var _gap:int = 6;
[Inspectable(category="General")]
/**
* The horizontal space between layout elements, in pixels.
*
* Note that the gap is only applied between layout elements, so if there's
* just one element, the gap has no effect on the layout.
*
* @default 6
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get gap():int
{
return _gap;
}
/**
* @private
*/
public function set gap(value:int):void
{
if (_gap == value)
return;
_gap = value;
invalidateTargetSizeAndDisplayList();
}
//----------------------------------
// columnCount
//----------------------------------
private var _columnCount:int = -1;
[Bindable("propertyChange")]
[Inspectable(category="General")]
/**
* Returns the current number of elements in view.
*
* @default -1
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get columnCount():int
{
return _columnCount;
}
/**
* @private
*
* Sets the <code>columnCount</code> property and dispatches
* a PropertyChangeEvent.
*/
private function setColumnCount(value:int):void
{
if (_columnCount == value)
return;
var oldValue:int = _columnCount;
_columnCount = value;
dispatchEvent(PropertyChangeEvent.createUpdateEvent(this, "columnCount", oldValue, value));
}
//----------------------------------
// paddingLeft
//----------------------------------
private var _paddingLeft:Number = 0;
[Inspectable(category="General")]
/**
* Number of pixels between the container's left edge
* and the left edge of the first layout element.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get paddingLeft():Number
{
return _paddingLeft;
}
/**
* @private
*/
public function set paddingLeft(value:Number):void
{
if (_paddingLeft == value)
return;
_paddingLeft = value;
invalidateTargetSizeAndDisplayList();
}
//----------------------------------
// paddingRight
//----------------------------------
private var _paddingRight:Number = 0;
[Inspectable(category="General")]
/**
* Number of pixels between the container's right edge
* and the right edge of the last layout element.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get paddingRight():Number
{
return _paddingRight;
}
/**
* @private
*/
public function set paddingRight(value:Number):void
{
if (_paddingRight == value)
return;
_paddingRight = value;
invalidateTargetSizeAndDisplayList();
}
//----------------------------------
// paddingTop
//----------------------------------
private var _paddingTop:Number = 0;
[Inspectable(category="General")]
/**
* The minimum number of pixels between the container's top edge and
* the top of all the container's layout elements.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get paddingTop():Number
{
return _paddingTop;
}
/**
* @private
*/
public function set paddingTop(value:Number):void
{
if (_paddingTop == value)
return;
_paddingTop = value;
invalidateTargetSizeAndDisplayList();
}
//----------------------------------
// paddingBottom
//----------------------------------
private var _paddingBottom:Number = 0;
[Inspectable(category="General")]
/**
* The minimum number of pixels between the container's bottom edge and
* the bottom of all the container's layout elements.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get paddingBottom():Number
{
return _paddingBottom;
}
/**
* @private
*/
public function set paddingBottom(value:Number):void
{
if (_paddingBottom == value)
return;
_paddingBottom = value;
invalidateTargetSizeAndDisplayList();
}
//----------------------------------
// requestedMaxColumnCount
//----------------------------------
private var _requestedMaxColumnCount:int = -1;
[Inspectable(category="General", minValue="-1")]
/**
* The measured width of this layout is large enough to display
* at most <code>requestedMaxColumnCount</code> layout elements.
*
* <p>If <code>requestedColumnCount</code> is set, then
* this property has no effect.</p>
*
* <p>If the actual size of the container using this layout has been explicitly set,
* then this property has no effect.</p>
*
* @default -1
* @see #requestedColumnCount
* @see #requestedMinColumnCount
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get requestedMaxColumnCount():int
{
return _requestedMaxColumnCount;
}
/**
* @private
*/
public function set requestedMaxColumnCount(value:int):void
{
if (_requestedMaxColumnCount == value)
return;
_requestedMaxColumnCount = value;
if (target)
target.invalidateSize();
}
//----------------------------------
// requestedMinColumnCount
//----------------------------------
private var _requestedMinColumnCount:int = -1;
[Inspectable(category="General", minValue="-1")]
/**
* The measured width of this layout is large enough to display
* at least <code>requestedMinColumnCount</code> layout elements.
*
* <p>If <code>requestedColumnCount</code> is set, then
* this property has no effect.</p>
*
* <p>If the actual size of the container using this layout has been explicitly set,
* then this property has no effect.</p>
*
* @default -1
* @see #requestedColumnCount
* @see #requestedMaxColumnCount
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get requestedMinColumnCount():int
{
return _requestedMinColumnCount;
}
/**
* @private
*/
public function set requestedMinColumnCount(value:int):void
{
if (_requestedMinColumnCount == value)
return;
_requestedMinColumnCount = value;
if (target)
target.invalidateSize();
}
//----------------------------------
// requestedColumnCount
//----------------------------------
private var _requestedColumnCount:int = -1;
[Inspectable(category="General", minValue="-1")]
/**
* The measured size of this layout is wide enough to display
* the first <code>requestedColumnCount</code> layout elements.
* If <code>requestedColumnCount</code> is -1, then the measured
* size will be big enough for all of the layout elements.
*
* <p>If the actual size of the container using this layout has been explicitly set,
* then this property has no effect.</p>
*
* @default -1
* @see #requestedMinColumnCount
* @see #requestedMaxColumnCount
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get requestedColumnCount():int
{
return _requestedColumnCount;
}
/**
* @private
*/
public function set requestedColumnCount(value:int):void
{
if (_requestedColumnCount == value)
return;
_requestedColumnCount = value;
if (target)
target.invalidateSize();
}
//----------------------------------
// columnWidth
//----------------------------------
private var _columnWidth:Number;
[Inspectable(category="General", minValue="0.0")]
/**
* If the <code>variableColumnWidth</code> property is <code>false</code>,
* then this property specifies the actual width of each layout element, in pixels.
*
* <p>If the <code>variableColumnWidth</code> property is <code>true</code>,
* the default, then this property has no effect.</p>
*
* <p>The default value of this property is the preferred width
* of the item specified by the <code>typicalLayoutElement</code> property.</p>
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get columnWidth():Number
{
if (!isNaN(_columnWidth))
return _columnWidth;
else
{
var elt:ILayoutElement = typicalLayoutElement
return (elt) ? elt.getPreferredBoundsWidth() : 0;
}
}
/**
* @private
*/
public function set columnWidth(value:Number):void
{
if (_columnWidth == value)
return;
_columnWidth = value;
invalidateTargetSizeAndDisplayList();
}
//----------------------------------
// variableColumnWidth
//----------------------------------
/**
* @private
*/
private var _variableColumnWidth:Boolean = true;
[Inspectable(category="General", enumeration="true,false")]
/**
* If <code>true</code>, specifies that layout elements are to be allocated their
* preferred width.
*
* <p>Setting this property to <code>false</code> specifies fixed width columns.
* The actual width of each layout element is
* the value of the <code>columnWidth</code> property, and the layout ignores
* a layout elements' <code>percentWidth</code> property.</p>
*
* @default true
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get variableColumnWidth():Boolean
{
return _variableColumnWidth;
}
/**
* @private
*/
public function set variableColumnWidth(value:Boolean):void
{
if (value == _variableColumnWidth) return;
_variableColumnWidth = value;
invalidateTargetSizeAndDisplayList();
}
//----------------------------------
// firstIndexInView
//----------------------------------
/**
* @private
*/
private var _firstIndexInView:int = -1;
[Inspectable(category="General")]
[Bindable("indexInViewChanged")]
/**
* The index of the first column that is part of the layout and within
* the layout target's scroll rectangle, or -1 if nothing has been displayed yet.
*
* Note that the column may only be partially in view.
*
* @see lastIndexInView
* @see fractionOfElementInView
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get firstIndexInView():int
{
return _firstIndexInView;
}
//----------------------------------
// lastIndexInView
//----------------------------------
/**
* @private
*/
private var _lastIndexInView:int = -1;
[Inspectable(category="General")]
[Bindable("indexInViewChanged")]
/**
* The index of the last column that is part of the layout and within
* the layout target's scroll rectangle, or -1 if nothing has been displayed yet.
*
* Note that the column may only be partially in view.
*
* @see firstIndexInView
* @see fractionOfElementInView
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get lastIndexInView():int
{
return _lastIndexInView;
}
//----------------------------------
// horizontalAlign
//----------------------------------
/**
* @private
*/
private var _horizontalAlign:String = HorizontalAlign.LEFT;
[Inspectable(category="General", enumeration="left,right,center", defaultValue="left")]
/**
* The horizontal alignment of the content relative to the container's width.
* If the value is <code>"left"</code>, <code>"right"</code>, or <code>"center"</code> then the
* layout element is aligned relative to the container's <code>contentWidth</code> property.
*
* <p>This property has no effect when <code>clipAndEnableScrolling</code> is true
* and the <code>contentWidth</code> is greater than the container's width.</p>
*
* <p>This property does not affect the layout's measured size.</p>
*
* @default "left"
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get horizontalAlign():String
{
return _horizontalAlign;
}
/**
* @private
*/
public function set horizontalAlign(value:String):void
{
if (value == _horizontalAlign)
return;
_horizontalAlign = value;
var layoutTarget:GroupBase = target;
if (layoutTarget)
layoutTarget.invalidateDisplayList();
}
//----------------------------------
// verticalAlign
//----------------------------------
/**
* @private
*/
private var _verticalAlign:String = VerticalAlign.TOP;
[Inspectable(category="General", enumeration="top,bottom,middle,justify,contentJustify,baseline", defaultValue="top")]
/**
* The vertical alignment of layout elements.
*
* <p>If the value is <code>"bottom"</code>, <code>"middle"</code>,
* or <code>"top"</code> then the layout elements are aligned relative
* to the container's <code>contentHeight</code> property.</p>
*
* <p>If the value is <code>"contentJustify"</code> then the actual
* height of the layout element is set to
* the container's <code>contentHeight</code> property.
* The content height of the container is the height of the largest layout element.
* If all layout elements are smaller than the height of the container,
* then set the height of all the layout elements to the height of the container.</p>
*
* <p>If the value is <code>"justify"</code> then the actual height
* of the layout elements is set to the container's height.</p>
*
* <p>If the value is <code>"baseline"</code> then the elements are positioned
* such that their text is aligned to the maximum of the elements' text ascent.</p>
*
* @default "top"
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get verticalAlign():String
{
return _verticalAlign;
}
/**
* @private
*/
public function set verticalAlign(value:String):void
{
if (value == _verticalAlign)
return;
var oldValue:String = _verticalAlign;
_verticalAlign = value;
// "baseline" affects the measured size
if (oldValue == VerticalAlign.BASELINE ||
value == VerticalAlign.BASELINE)
{
invalidateTargetSizeAndDisplayList();
}
else
{
var layoutTarget:GroupBase = target;
if (layoutTarget)
layoutTarget.invalidateDisplayList();
}
}
/**
* @private
*
* Sets the <code>firstIndexInView</code> and <code>lastIndexInView</code>
* properties and dispatches a <code>"indexInViewChanged"</code>
* event.
*
* @param firstIndex The new value for firstIndexInView.
* @param lastIndex The new value for lastIndexInView.
*
* @see firstIndexInView
* @see lastIndexInview
*/
private function setIndexInView(firstIndex:int, lastIndex:int):void
{
if ((_firstIndexInView == firstIndex) && (_lastIndexInView == lastIndex))
return;
_firstIndexInView = firstIndex;
_lastIndexInView = lastIndex;
dispatchEvent(new Event("indexInViewChanged"));
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
override public function set clipAndEnableScrolling(value:Boolean):void
{
super.clipAndEnableScrolling = value;
var hAlign:String = horizontalAlign;
if (hAlign == HorizontalAlign.CENTER || hAlign == HorizontalAlign.RIGHT)
{
var g:GroupBase = target;
if (g)
g.invalidateDisplayList();
}
}
/**
* @private
*/
override public function clearVirtualLayoutCache():void
{
llv = null;
var g:GroupBase = GroupBase(target);
if (!g)
return;
target.invalidateSize();
target.invalidateDisplayList();
}
/**
* @private
*/
override public function getElementBounds(index:int):Rectangle
{
if (!useVirtualLayout)
return super.getElementBounds(index);
var g:GroupBase = GroupBase(target);
if (!g || (index < 0) || (index >= g.numElements) || !llv)
return null;
// We need a valid LLV for this function
updateLLV(g);
return llv.getBounds(index);
}
/**
* Returns 1.0 if the specified index is completely in view, 0.0 if
* it's not, or a value between 0.0 and 1.0 that represents the percentage
* of the if the index that is partially in view.
*
* <p>An index is "in view" if the corresponding non-null layout element is
* within the horizontal limits of the container's <code>scrollRect</code>
* and included in the layout.</p>
*
* <p>If the specified index is partially within the view, the
* returned value is the percentage of the corresponding
* layout element that's visible.</p>
*
* @param index The index of the column.
*
* @return The percentage of the specified element that's in view.
* Returns 0.0 if the specified index is invalid or if it corresponds to
* null element, or a ILayoutElement for which
* the <code>includeInLayout</code> property is <code>false</code>.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function fractionOfElementInView(index:int):Number
{
var g:GroupBase = target;
if (!g)
return 0.0;
if ((index < 0) || (index >= g.numElements))
return 0.0;
if (!clipAndEnableScrolling)
return 1.0;
var r0:int = firstIndexInView;
var r1:int = lastIndexInView;
// index is outside the "in view" or visible range
if ((r0 == -1) || (r1 == -1) || (index < r0) || (index > r1))
return 0.0;
// within the visible index range, but not first or last
if ((index > r0) && (index < r1))
return 1.0;
// get the layout element's X and Width
var eltX:Number;
var eltWidth:Number;
if (useVirtualLayout)
{
if (!llv)
return 0.0;
eltX = llv.start(index);
eltWidth = llv.getMajorSize(index);
}
else
{
var elt:ILayoutElement = g.getElementAt(index);
if (!elt || !elt.includeInLayout)
return 0.0;
eltX = elt.getLayoutBoundsX();
eltWidth = elt.getLayoutBoundsWidth();
}
// index is either the first or last column in the scrollRect
// and potentially partially visible.
// x0,x1 - scrollRect left,right edges
// ix0, ix1 - layout element left,right edges
var x0:Number = g.horizontalScrollPosition;
var x1:Number = x0 + g.width;
var ix0:Number = eltX;
var ix1:Number = ix0 + eltWidth;
if (ix0 >= ix1) // element has 0 or negative height
return 1.0;
if ((ix0 >= x0) && (ix1 <= x1))
return 1.0;
return (Math.min(x1, ix1) - Math.max(x0, ix0)) / (ix1 - ix0);
}
/**
* @private
*
* Binary search for the first layout element that contains y.
*
* This function considers both the element's actual bounds and
* the gap that follows it to be part of the element. The search
* covers index i0 through i1 (inclusive).
*
* This function is intended for variable height elements.
*
* Returns the index of the element that contains x, or -1.
*/
private static function findIndexAt(x:Number, gap:int, g:GroupBase, i0:int, i1:int):int
{
var index:int = (i0 + i1) / 2;
var element:ILayoutElement = g.getElementAt(index);
var elementX:Number = element.getLayoutBoundsX();
// TBD: deal with null element, includeInLayout false.
if ((x >= elementX) && (x < elementX + element.getLayoutBoundsWidth() + gap))
return index;
else if (i0 == i1)
return -1;
else if (x < elementX)
return findIndexAt(x, gap, g, i0, Math.max(i0, index-1));
else
return findIndexAt(x, gap, g, Math.min(index+1, i1), i1);
}
/**
* @private
*
* Returns the index of the first non-null includeInLayout element,
* beginning with the element at index i.
*
* Returns -1 if no such element can be found.
*/
private static function findLayoutElementIndex(g:GroupBase, i:int, dir:int):int
{
var n:int = g.numElements;
while((i >= 0) && (i < n))
{
var element:ILayoutElement = g.getElementAt(i);
if (element && element.includeInLayout)
{
return i;
}
i += dir;
}
return -1;
}
/**
* @private
*
* Updates the first,lastIndexInView properties per the new
* scroll position.
*
* @see setIndexInView
*/
override protected function scrollPositionChanged():void
{
super.scrollPositionChanged();
var g:GroupBase = target;
if (!g)
return;
var n:int = g.numElements - 1;
if (n < 0)
{
setIndexInView(-1, -1);
return;
}
var scrollR:Rectangle = getScrollRect();
if (!scrollR)
{
setIndexInView(0, n);
return;
}
// We're going to use findIndexAt to find the index of
// the elements that overlap the left and right edges of the scrollRect.
// Values that are exactly equal to scrollRect.right aren't actually
// rendered, since the left,right interval is only half open.
// To account for that we back away from the right edge by a
// hopefully infinitesimal amount.
var x0:Number = scrollR.left;
var x1:Number = scrollR.right - .0001;
if (x1 <= x0)
{
setIndexInView(-1, -1);
return;
}
if (useVirtualLayout && !llv)
{
setIndexInView(-1, -1);
return;
}
var i0:int;
var i1:int;
if (useVirtualLayout)
{
i0 = llv.indexOf(x0);
i1 = llv.indexOf(x1);
}
else
{
i0 = findIndexAt(x0 + gap, gap, g, 0, n);
i1 = findIndexAt(x1, gap, g, 0, n);
}
// Special case: no element overlaps x0, is index 0 visible?
if (i0 == -1)
{
var index0:int = findLayoutElementIndex(g, 0, +1);
if (index0 != -1)
{
var element0:ILayoutElement = g.getElementAt(index0);
var element0X:Number = element0.getLayoutBoundsX();
var element0Width:Number = element0.getLayoutBoundsWidth();
if ((element0X < x1) && ((element0X + element0Width) > x0))
i0 = index0;
}
}
// Special case: no element overlaps y1, is index n visible?
if (i1 == -1)
{
var index1:int = findLayoutElementIndex(g, n, -1);
if (index1 != -1)
{
var element1:ILayoutElement = g.getElementAt(index1);
var element1X:Number = element1.getLayoutBoundsX();
var element1Width:Number = element1.getLayoutBoundsWidth();
if ((element1X < x1) && ((element1X + element1Width) > x0))
i1 = index1;
}
}
if (useVirtualLayout)
{
var firstElement:ILayoutElement = g.getElementAt(_firstIndexInView);
var lastElement:ILayoutElement = g.getElementAt(_lastIndexInView);
var scrollRect:Rectangle = getScrollRect();
/* If the scrollRect is within the bounds of the elements, we do
not need to call invalidateDisplayList(). This considerably speeds
up small scrolls. */
if (!firstElement || !lastElement ||
scrollRect.left < firstElement.getLayoutBoundsX() ||
scrollRect.right >= (lastElement.getLayoutBoundsX() + lastElement.getLayoutBoundsWidth()))
{
g.invalidateDisplayList();
}
}
setIndexInView(i0, i1);
}
/**
* @private
*
* Returns the actual position/size Rectangle of the first partially
* visible or not-visible, non-null includeInLayout element, beginning
* with the element at index i, searching in direction dir (dir must
* be +1 or -1). The last argument is the GroupBase scrollRect, it's
* guaranteed to be non-null.
*
* Returns null if no such element can be found.
*/
private function findLayoutElementBounds(g:GroupBase, i:int, dir:int, r:Rectangle):Rectangle
{
var n:int = g.numElements;
if (fractionOfElementInView(i) >= 1)
{
// Special case: if we hit the first/last element,
// then return the area of the padding so that we
// can scroll all the way to the start/end.
i += dir;
if (i < 0)
return new Rectangle(0, 0, paddingLeft, 0);
if (i >= n)
return new Rectangle(getElementBounds(n-1).right, 0, paddingRight, 0);
}
while((i >= 0) && (i < n))
{
var elementR:Rectangle = getElementBounds(i);
// Special case: if the scrollRect r _only_ contains
// elementR, then if we're searching left (dir == -1),
// and elementR's left edge is visible, then try again
// with i-1. Likewise for dir == +1.
if (elementR)
{
var overlapsLeft:Boolean = (dir == -1) && (elementR.left == r.left) && (elementR.right >= r.right);
var overlapsRight:Boolean = (dir == +1) && (elementR.right == r.right) && (elementR.left <= r.left);
if (!(overlapsLeft || overlapsRight))
return elementR;
}
i += dir;
}
return null;
}
/**
* @private
*/
override protected function getElementBoundsLeftOfScrollRect(scrollRect:Rectangle):Rectangle
{
return findLayoutElementBounds(target, firstIndexInView, -1, scrollRect);
}
/**
* @private
*/
override protected function getElementBoundsRightOfScrollRect(scrollRect:Rectangle):Rectangle
{
return findLayoutElementBounds(target, lastIndexInView, +1, scrollRect);
}
/**
* @private
* Fills in the result with preferred and min sizes of the element.
*/
private function getElementWidth(element:ILayoutElement, fixedColumnWidth:Number, result:SizesAndLimit):void
{
// Calculate preferred width first, as it's being used to calculate min width
var elementPreferredWidth:Number = isNaN(fixedColumnWidth) ? Math.ceil(element.getPreferredBoundsWidth()) :
fixedColumnWidth;
// Calculate min width
var flexibleWidth:Boolean = !isNaN(element.percentWidth);
var elementMinWidth:Number = flexibleWidth ? Math.ceil(element.getMinBoundsWidth()) :
elementPreferredWidth;
result.preferredSize = elementPreferredWidth;
result.minSize = elementMinWidth;
}
/**
* @private
* Fills in the result with preferred and min sizes of the element.
*/
private function getElementHeight(element:ILayoutElement, justify:Boolean, result:SizesAndLimit):void
{
// Calculate preferred height first, as it's being used to calculate min height below
var elementPreferredHeight:Number = Math.ceil(element.getPreferredBoundsHeight());
// Calculate min height
var flexibleHeight:Boolean = !isNaN(element.percentHeight) || justify;
var elementMinHeight:Number = flexibleHeight ? Math.ceil(element.getMinBoundsHeight()) :
elementPreferredHeight;
result.preferredSize = elementPreferredHeight;
result.minSize = elementMinHeight;
}
/**
* @private
* Returns [baselineTop, baselineBottom, baselineBottomMin],
* where baselineTop is the portion of the elements above the common baseline
* and the baselineBottom is the portion of the elements below the common baseline.
*/
private function calculateBaselineTopBottom(calculateBottom:Boolean):Array
{
var baselineOffset:Number = 0;
var baselineTop:Number = 0;
var baselineBottom:Number = 0;
var baselineBottomMin:Number = 0;
var calculateTop:Boolean;
var temp:Array = LayoutElementHelper.parseConstraintExp(alignmentBaseline);
if (temp.length == 2 && temp[1] == "maxAscent")
{
baselineOffset = Number(temp[0]);
calculateTop = true;
}
else
{
calculateTop = false;
baselineTop = Number(temp[0]);
// If someone sets the explicit baseline to NaN, then
// still calculate from the elements
if (isNaN(baselineTop))
{
baselineTop = 0;
calculateTop = true;
}
}
var count:int = target.numElements;
for (var i:int = 0; i < count; i++)
{
var element:ILayoutElement = target.getElementAt(i);
if (!element || !element.includeInLayout)
continue;
var elementBaseline:Number = element.baseline as Number;
if (isNaN(elementBaseline))
elementBaseline = 0;
var baselinePosition:Number = element.baselinePosition;
// The portion of the current element that's above the baseline
var elementBaselineTop:Number = baselinePosition - elementBaseline;
if (calculateTop)
baselineTop = Math.max(elementBaselineTop, baselineTop);
if (calculateBottom)
{
// The portion of the current element that's below the baseline
var elementHeight:Number = element.getPreferredBoundsHeight();
var elementBaselineBottom:Number = elementHeight - elementBaselineTop;
// Calculate bottom based on min height if the element has flexible height
var elementBaselineBottomMin:Number = elementBaselineBottom;
if (!isNaN(element.percentHeight))
elementBaselineBottomMin = element.getMinBoundsHeight() - elementBaselineTop;
baselineBottom = Math.max(elementBaselineBottom, baselineBottom);
}
}
// If ascent was specified, add the offset
if (calculateTop)
baselineTop += baselineOffset;
return [baselineTop, baselineBottom, baselineBottomMin];
}
/**
* @private
* @return columns to measure based on elements in layout and any requested/min/max rowCount settings.
*/
private function getColumsToMeasure(numElementsInLayout:int):int
{
var columnsToMeasure:int;
if (requestedColumnCount != -1)
columnsToMeasure = requestedColumnCount;
else
{
columnsToMeasure = numElementsInLayout;
if (requestedMaxColumnCount != -1)
columnsToMeasure = Math.min(requestedMaxColumnCount, columnsToMeasure);
if (requestedMinColumnCount != -1)
columnsToMeasure = Math.max(requestedMinColumnCount, columnsToMeasure);
}
return columnsToMeasure;
}
/**
* @private
*
* Compute exact values for measuredWidth,Height and measuredMinWidth,Height.
*
* Measure each of the layout elements. If requestedColumnCount >= 0 we
* consider the height and width of as many layout elements, padding with
* typicalLayoutElement if needed, starting with index 0. We then only
* consider the height of the elements remaining.
*
* If requestedColumnCount is -1, we consider width/height of each element.
*/
private function measureReal(layoutTarget:GroupBase):void
{
var size:SizesAndLimit = new SizesAndLimit();
var alignToBaseline:Boolean = verticalAlign == VerticalAlign.BASELINE;
var justify:Boolean = verticalAlign == VerticalAlign.JUSTIFY;
var numElements:int = layoutTarget.numElements; // How many elements there are in the target
var numElementsInLayout:int = numElements; // How many elements have includeInLayout == true, start off with numElements.
var requestedColumnCount:int = this.requestedColumnCount;
var columnsMeasured:int = 0; // How many columns have been measured
var preferredHeight:Number = 0; // sum of the elt preferred heights
var preferredWidth:Number = 0; // max of the elt preferred widths
var minHeight:Number = 0; // sum of the elt minimum heights
var minWidth:Number = 0; // max of the elt minimum widths
var fixedColumnWidth:Number = NaN;
if (!variableColumnWidth)
fixedColumnWidth = columnWidth; // may query typicalLayoutElement, elt at index=0
// Get the numElementsInLayout clamped to requested min/max
var columnsToMeasure:int = getColumsToMeasure(numElementsInLayout);
var element:ILayoutElement;
for (var i:int = 0; i < numElements; i++)
{
element = layoutTarget.getElementAt(i);
if (!element || !element.includeInLayout)
{
numElementsInLayout--;
continue;
}
if (!alignToBaseline)
{
// Consider the height of each element, inclusive of those outside
// the requestedColumnCount range.
getElementHeight(element, justify, size);
preferredHeight = Math.max(preferredHeight, size.preferredSize);
minHeight = Math.max(minHeight, size.minSize);
}
// Can we measure the width of this column?
if (columnsMeasured < columnsToMeasure)
{
getElementWidth(element, fixedColumnWidth, size);
preferredWidth += size.preferredSize;
minWidth += size.minSize;
columnsMeasured++;
}
}
// Calculate the total number of columns to measure again, since numElementsInLayout may have changed
columnsToMeasure = getColumsToMeasure(numElementsInLayout);
// Do we need to measure more columns?
if (columnsMeasured < columnsToMeasure)
{
// Use the typical element
element = typicalLayoutElement;
if (element)
{
if (!alignToBaseline)
{
// Height
getElementHeight(element, justify, size);
preferredHeight = Math.max(preferredHeight, size.preferredSize);
minHeight = Math.max(minHeight, size.minSize);
}
// Width
getElementWidth(element, fixedColumnWidth, size);
preferredWidth += size.preferredSize * (columnsToMeasure - columnsMeasured);
minWidth += size.minSize * (columnsToMeasure - columnsMeasured);
columnsMeasured = columnsToMeasure;
}
}
if (alignToBaseline)
{
var result:Array = calculateBaselineTopBottom(true /*calculateBottom*/);
var top:Number = result[0];
var bottom:Number = result[1];
var bottomMin:Number = result[2];
preferredHeight = Math.ceil(top + bottom);
minHeight = Math.ceil(top + bottomMin);
}
// Add gaps
if (columnsMeasured > 1)
{
var hgap:Number = gap * (columnsMeasured - 1);
preferredWidth += hgap;
minWidth += hgap;
}
var hPadding:Number = paddingLeft + paddingRight;
var vPadding:Number = paddingTop + paddingBottom;
layoutTarget.measuredHeight = preferredHeight + vPadding;
layoutTarget.measuredWidth = preferredWidth + hPadding;
layoutTarget.measuredMinHeight = minHeight + vPadding;
layoutTarget.measuredMinWidth = minWidth + hPadding;
}
/**
* @private
*
* Syncs the LinearLayoutVector llv with typicalLayoutElement and
* the target's numElements. Calling this function accounts
* for the possibility that the typicalLayoutElement has changed, or
* something that its preferred size depends on has changed.
*/
private function updateLLV(layoutTarget:GroupBase):void
{
if (!llv)
{
llv = new LinearLayoutVector(LinearLayoutVector.HORIZONTAL)
// Virtualization defaults for cases
// where there are no items and no typical item.
// The llv defaults are the width/height of a Spark Button skin.
llv.defaultMinorSize = 22;
llv.defaultMajorSize = 71;
}
var typicalElt:ILayoutElement = typicalLayoutElement;
if (typicalElt)
{
var typicalWidth:Number = typicalElt.getPreferredBoundsWidth();
var typicalHeight:Number = typicalElt.getPreferredBoundsHeight();
llv.defaultMinorSize = typicalHeight;
llv.defaultMajorSize = typicalWidth;
}
if (!isNaN(_columnWidth))
llv.defaultMajorSize = _columnWidth;
if (layoutTarget)
llv.length = layoutTarget.numElements;
llv.gap = gap;
llv.majorAxisOffset = paddingLeft;
}
/**
* @private
*/
override public function elementAdded(index:int):void
{
if ((index >= 0) && useVirtualLayout && llv)
llv.insert(index); // insert index parameter is uint
}
/**
* @private
*/
override public function elementRemoved(index:int):void
{
if ((index >= 0) && useVirtualLayout && llv)
llv.remove(index); // remove index parameter is uint
}
/**
* @private
*
* Compute potentially approximate values for measuredWidth,Height and
* measuredMinWidth,Height.
*
* This method does not get layout elements from the target except
* as a side effect of calling typicalLayoutElement.
*
* If variableColumnWidth="false" then all dimensions are based on
* typicalLayoutElement and the sizes already cached in llv. The
* llv's defaultMajorSize, minorSize, and minMinorSize
* are based on typicalLayoutElement.
*/
private function measureVirtual(layoutTarget:GroupBase):void
{
var eltCount:int = layoutTarget.numElements;
var measuredEltCount:int = getColumsToMeasure(eltCount);
var hPadding:Number = paddingLeft + paddingRight;
var vPadding:Number = paddingTop + paddingBottom;
if (measuredEltCount <= 0)
{
layoutTarget.measuredWidth = layoutTarget.measuredMinWidth = hPadding;
layoutTarget.measuredHeight = layoutTarget.measuredMinHeight = vPadding;
return;
}
updateLLV(layoutTarget);
if (variableColumnWidth)
{
// Special case: fewer elements than requestedColumnCount, so temporarily
// make llv.length == requestedColumnCount.
var oldLength:int = -1;
if (measuredEltCount > llv.length)
{
oldLength = llv.length;
llv.length = measuredEltCount;
}
// paddingRight is already taken into account as the majorAxisOffset of the llv
// Measured size according to the cached actual size:
var measuredWidth:Number = llv.end(measuredEltCount - 1) + paddingRight;
// For the live ItemRenderers use the preferred size
// instead of the cached actual size:
var dataGroupTarget:DataGroup = layoutTarget as DataGroup;
if (dataGroupTarget)
{
var indices:Vector.<int> = dataGroupTarget.getItemIndicesInView();
for each (var i:int in indices)
{
var element:ILayoutElement = dataGroupTarget.getElementAt(i);
if (element)
{
measuredWidth -= llv.getMajorSize(i);
measuredWidth += element.getPreferredBoundsWidth();
}
}
}
layoutTarget.measuredWidth = measuredWidth;
if (oldLength != -1)
llv.length = oldLength;
}
else
{
var hgap:Number = (measuredEltCount > 1) ? (measuredEltCount - 1) * gap : 0;
layoutTarget.measuredWidth = (measuredEltCount * columnWidth) + hgap + hPadding;
}
layoutTarget.measuredHeight = llv.minorSize + vPadding;
layoutTarget.measuredMinWidth = layoutTarget.measuredWidth;
layoutTarget.measuredMinHeight = (verticalAlign == VerticalAlign.JUSTIFY) ?
llv.minMinorSize + vPadding : layoutTarget.measuredHeight;
}
/**
* @private
*
* If requestedColumnCount is specified then as many layout elements
* or "columns" are measured, starting with element 0, otherwise all of the
* layout elements are measured.
*
* If requestedColumnCount is specified and is greater than the
* number of layout elements, then the typicalLayoutElement is used
* in place of the missing layout elements.
*
* If variableColumnWidth="true", then the layoutTarget's measuredWidth
* is the sum of preferred widths of the layout elements, plus the sum of the
* gaps between elements, and its measuredHeight is the max of the elements'
* preferred heights.
*
* If variableColumnWidth="false", then the layoutTarget's measuredWidth
* is columnWidth multiplied by the number or layout elements, plus the
* sum of the gaps between elements.
*
* The layoutTarget's measuredMinWidth is the sum of the minWidths of
* layout elements that have specified a value for the percentWidth
* property, and the preferredWidth of the elements that have not,
* plus the sum of the gaps between elements.
*
* The difference reflects the fact that elements which specify
* percentWidth are considered to be "flexible" and updateDisplayList
* will give flexible components at least their minWidth.
*
* Layout elements that aren't flexible always get their preferred width.
*
* The layoutTarget's measuredMinHeight is the max of the minHeights for
* elements that have specified percentHeight (that are "flexible") and the
* preferredHeight of the elements that have not.
*
* As before the difference is due to the fact that flexible items are only
* guaranteed their minHeight.
*/
override public function measure():void
{
var layoutTarget:GroupBase = target;
if (!layoutTarget)
return;
if (useVirtualLayout)
measureVirtual(layoutTarget);
else
measureReal(layoutTarget);
// Use Math.ceil() to make sure that if the content partially occupies
// the last pixel, we'll count it as if the whole pixel is occupied.
layoutTarget.measuredWidth = Math.ceil(layoutTarget.measuredWidth);
layoutTarget.measuredHeight = Math.ceil(layoutTarget.measuredHeight);
layoutTarget.measuredMinWidth = Math.ceil(layoutTarget.measuredMinWidth);
layoutTarget.measuredMinHeight = Math.ceil(layoutTarget.measuredMinHeight);
}
/**
* @private
*/
override public function getNavigationDestinationIndex(currentIndex:int, navigationUnit:uint, arrowKeysWrapFocus:Boolean):int
{
if (!target || target.numElements < 1)
return -1;
var maxIndex:int = target.numElements - 1;
// Special case when nothing was previously selected
if (currentIndex == -1)
{
if (navigationUnit == NavigationUnit.LEFT)
return arrowKeysWrapFocus ? maxIndex : -1;
if (navigationUnit == NavigationUnit.RIGHT)
return 0;
}
// Make sure currentIndex is within range
currentIndex = Math.max(0, Math.min(maxIndex, currentIndex));
var newIndex:int;
var bounds:Rectangle;
var x:Number;
switch (navigationUnit)
{
case NavigationUnit.LEFT:
{
if (arrowKeysWrapFocus && currentIndex == 0)
newIndex = maxIndex;
else
newIndex = currentIndex - 1;
break;
}
case NavigationUnit.RIGHT:
{
if (arrowKeysWrapFocus && currentIndex == maxIndex)
newIndex = 0;
else
newIndex = currentIndex + 1;
break;
}
case NavigationUnit.PAGE_UP:
case NavigationUnit.PAGE_LEFT:
{
// Find the first fully visible element
var firstVisible:int = firstIndexInView;
var firstFullyVisible:int = firstVisible;
if (fractionOfElementInView(firstFullyVisible) < 1)
firstFullyVisible += 1;
// Is the current element in the middle of the viewport?
if (firstFullyVisible < currentIndex && currentIndex <= lastIndexInView)
newIndex = firstFullyVisible;
else
{
// Find an element that's one page left
if (currentIndex == firstFullyVisible || currentIndex == firstVisible)
{
// currentIndex is visible, we can calculate where the scrollRect top
// would end up if we scroll by a page
x = getHorizontalScrollPositionDelta(NavigationUnit.PAGE_LEFT) + getScrollRect().left;
}
else
{
// currentIndex is not visible, just find an element a page left from currentIndex
x = getElementBounds(currentIndex).right - getScrollRect().width;
}
// Find the element after the last element that spans left of the x position
newIndex = currentIndex - 1;
while (0 <= newIndex)
{
bounds = getElementBounds(newIndex);
if (bounds && bounds.left < x)
{
// This element spans the y position, so return the next one
newIndex = Math.min(currentIndex - 1, newIndex + 1);
break;
}
newIndex--;
}
}
break;
}
case NavigationUnit.PAGE_DOWN:
case NavigationUnit.PAGE_RIGHT:
{
// Find the last fully visible element:
var lastVisible:int = lastIndexInView;
var lastFullyVisible:int = lastVisible;
if (fractionOfElementInView(lastFullyVisible) < 1)
lastFullyVisible -= 1;
// Is the current element in the middle of the viewport?
if (firstIndexInView <= currentIndex && currentIndex < lastFullyVisible)
newIndex = lastFullyVisible;
else
{
// Find an element that's one page right
if (currentIndex == lastFullyVisible || currentIndex == lastVisible)
{
// currentIndex is visible, we can calculate where the scrollRect bottom
// would end up if we scroll by a page
x = getHorizontalScrollPositionDelta(NavigationUnit.PAGE_RIGHT) + getScrollRect().right;
}
else
{
// currentIndex is not visible, just find an element a page right from currentIndex
x = getElementBounds(currentIndex).left + getScrollRect().width;
}
// Find the element before the first element that spans right of the y position
newIndex = currentIndex + 1;
while (newIndex <= maxIndex)
{
bounds = getElementBounds(newIndex);
if (bounds && bounds.right > x)
{
// This element spans the y position, so return the previous one
newIndex = Math.max(currentIndex + 1, newIndex - 1);
break;
}
newIndex++;
}
}
break;
}
default: return super.getNavigationDestinationIndex(currentIndex, navigationUnit, arrowKeysWrapFocus);
}
return Math.max(0, Math.min(maxIndex, newIndex));
}
/**
* @private
*
* Used only for virtual layout.
*/
private function calculateElementHeight(elt:ILayoutElement, targetHeight:Number, containerHeight:Number):Number
{
// If percentHeight is specified then the element's height is the percentage
// of targetHeight clipped to min/maxHeight and to (upper limit) targetHeight.
var percentHeight:Number = elt.percentHeight;
if (!isNaN(percentHeight))
{
var height:Number = percentHeight * 0.01 * targetHeight;
return Math.min(targetHeight, Math.min(elt.getMaxBoundsHeight(), Math.max(elt.getMinBoundsHeight(), height)));
}
switch(verticalAlign)
{
case VerticalAlign.JUSTIFY:
return targetHeight;
case VerticalAlign.CONTENT_JUSTIFY:
return Math.max(elt.getPreferredBoundsHeight(), containerHeight);
}
return NaN; // not constrained
}
/**
* @private
*
* Used only for virtual layout.
*/
private function calculateElementY(elt:ILayoutElement, eltHeight:Number, containerHeight:Number):Number
{
switch(verticalAlign)
{
case VerticalAlign.MIDDLE:
return Math.round((containerHeight - eltHeight) * 0.5);
case VerticalAlign.BOTTOM:
return containerHeight - eltHeight;
}
return 0; // VerticalAlign.TOP
}
/**
* @private
*
* Update the layout of the virtualized elements that overlap
* the scrollRect's horizontal extent.
*
* The width of each layout element will be its preferred width, and its
* x will be the right edge of the previous item, plus the gap.
*
* No support for percentWidth, includeInLayout=false, or null layoutElements,
*
* The height of each layout element will be set to its preferred height, unless
* one of the following is true:
*
* - If percentHeight is specified for this element, then its height will be the
* specified percentage of the target's actual (unscaled) height, clipped
* the layout element's minimum and maximum height.
*
* - If verticalAlign is "justify", then the element's height will
* be set to the target's actual (unscaled) height.
*
* - If verticalAlign is "contentJustify", then the element's height
* will be set to the target's content height.
*
* The Y coordinate of each layout element will be set to 0 unless one of the
* following is true:
*
* - If verticalAlign is "middle" then y is set so that the element's preferred
* height is centered within the larget of the contentHeight and the target's height:
* y = (Math.max(contentHeight, target.height) - layoutElementHeight) * 0.5
*
* - If verticalAlign is "bottom" the y is set so that the element's bottom
* edge is aligned with the the bottom edge of the content:
* y = (Math.max(contentHeight, target.height) - layoutElementHeight)
*
* Implementation note: unless verticalAlign is either "justify" or
* "top", the layout elements' y or height depends on the contentHeight.
* The contentHeight is a maximum and although it may be updated to
* different value after all (viewable) elements have been laid out, it
* often does not change. For that reason we use the current contentHeight
* for the initial layout and then, if it has changed, we loop through
* the layout items again and fix up the y/height values.
*/
private function updateDisplayListVirtual():void
{
var layoutTarget:GroupBase = target;
var eltCount:int = layoutTarget.numElements;
var targetHeight:Number = Math.max(0, layoutTarget.height - paddingTop - paddingBottom);
var minVisibleX:Number = layoutTarget.horizontalScrollPosition;
var maxVisibleX:Number = minVisibleX + layoutTarget.width;
var contentWidth:Number;
var paddedContentWidth:Number;
updateLLV(layoutTarget);
// Find the index of the first visible item. Since the item's bounds includes the gap
// that follows it, we want to avoid looking at an item that has only a portion of
// its gap intersecting with the visible region.
// We have to also be careful, as gap could be negative and in that case, we should
// simply start from minVisibleX - SDK-22497.
var startIndex:int = llv.indexOf(Math.max(0, minVisibleX + gap));
if (startIndex == -1)
{
// No items are visible. Just set the content size.
contentWidth = llv.end(llv.length - 1) - paddingLeft;
paddedContentWidth = Math.ceil(contentWidth + paddingLeft + paddingRight);
layoutTarget.setContentSize(paddedContentWidth, layoutTarget.contentHeight);
return;
}
var fixedColumnWidth:Number = NaN;
if (!variableColumnWidth)
fixedColumnWidth = columnWidth; // may query typicalLayoutElement, elt at index=0
var justifyHeights:Boolean = verticalAlign == VerticalAlign.JUSTIFY;
var eltWidth:Number = NaN;
var eltHeight:Number = (justifyHeights) ? Math.max(llv.minMinorSize, targetHeight) : llv.minorSize;
var contentHeight:Number = (justifyHeights) ? Math.max(llv.minMinorSize, targetHeight) : llv.minorSize;
var containerHeight:Number = Math.max(contentHeight, targetHeight);
var x:Number = llv.start(startIndex);
var index:int = startIndex;
var y0:Number = paddingTop;
// First pass: compute element x,y,width,height based on
// current contentHeight; cache computed widths/heights in llv.
for (; (x < maxVisibleX) && (index < eltCount); index++)
{
// TODO (hmuller): should pass in eltWidth, eltHeight
var elt:ILayoutElement = layoutTarget.getVirtualElementAt(index);
var w:Number = fixedColumnWidth; // NaN for variable width columns
var h:Number = calculateElementHeight(elt, targetHeight, containerHeight); // can be NaN
elt.setLayoutBoundsSize(w, h);
w = elt.getLayoutBoundsWidth();
h = elt.getLayoutBoundsHeight();
var y:Number = y0 + calculateElementY(elt, h, containerHeight);
elt.setLayoutBoundsPosition(x, y);
llv.cacheDimensions(index, elt);
x += w + gap;
}
var endIndex:int = index - 1;
// Second pass: if neccessary, fix up y and height values based
// on the updated contentHeight
if (!justifyHeights && (llv.minorSize != contentHeight))
{
contentHeight = llv.minorSize;
containerHeight = Math.max(contentHeight, targetHeight);
if ((verticalAlign != VerticalAlign.TOP) && (verticalAlign != VerticalAlign.JUSTIFY))
{
for (index = startIndex; index <= endIndex; index++)
{
elt = layoutTarget.getElementAt(index);
h = calculateElementHeight(elt, targetHeight, containerHeight); // can be NaN
elt.setLayoutBoundsSize(elt.getLayoutBoundsWidth(), h);
h = elt.getLayoutBoundsHeight();
y = y0 + calculateElementY(elt, h, containerHeight);
elt.setLayoutBoundsPosition(elt.getLayoutBoundsX(), y);
}
}
}
// Third pass: if neccessary, fix up x based on updated contentWidth
contentWidth = llv.end(llv.length - 1) - paddingLeft;
var targetWidth:Number = Math.max(0, layoutTarget.width - paddingLeft - paddingRight);
if (contentWidth < targetWidth)
{
var excessWidth:Number = targetWidth - contentWidth;
var dx:Number = 0;
var hAlign:String = horizontalAlign;
if (hAlign == HorizontalAlign.CENTER)
{
dx = Math.round(excessWidth / 2);
}
else if (hAlign == HorizontalAlign.RIGHT)
{
dx = excessWidth;
}
if (dx > 0)
{
for (index = startIndex; index <= endIndex; index++)
{
elt = layoutTarget.getElementAt(index);
elt.setLayoutBoundsPosition(dx + elt.getLayoutBoundsX(), elt.getLayoutBoundsY());
}
contentWidth += dx;
}
}
setColumnCount(index - startIndex);
setIndexInView(startIndex, endIndex);
// Make sure that if the content spans partially over a pixel to the right/bottom,
// the content size includes the whole pixel.
paddedContentWidth = Math.ceil(contentWidth + paddingLeft + paddingRight);
var paddedContentHeight:Number = Math.ceil(contentHeight + paddingTop + paddingBottom);
layoutTarget.setContentSize(paddedContentWidth, paddedContentHeight);
}
/**
* @private
*/
private function updateDisplayListReal():void
{
var layoutTarget:GroupBase = target;
var targetWidth:Number = Math.max(0, layoutTarget.width - paddingLeft - paddingRight);
var targetHeight:Number = Math.max(0, layoutTarget.height - paddingTop - paddingBottom);
var layoutElement:ILayoutElement;
var count:int = layoutTarget.numElements;
// If verticalAlign is top, we don't need to figure out the contentHeight.
// Otherwise the contentHeight is used to position the element and even size
// the element if it's "contentJustify" or "justify".
var containerHeight:Number = targetHeight;
if (verticalAlign == VerticalAlign.CONTENT_JUSTIFY ||
(clipAndEnableScrolling && (verticalAlign == VerticalAlign.MIDDLE ||
verticalAlign == VerticalAlign.BOTTOM)))
{
for (var i:int = 0; i < count; i++)
{
layoutElement = layoutTarget.getElementAt(i);
if (!layoutElement || !layoutElement.includeInLayout)
continue;
var layoutElementHeight:Number;
if (!isNaN(layoutElement.percentHeight))
layoutElementHeight = calculatePercentHeight(layoutElement, targetHeight);
else
layoutElementHeight = layoutElement.getPreferredBoundsHeight();
containerHeight = Math.max(containerHeight, Math.ceil(layoutElementHeight));
}
}
var excessWidth:Number = distributeWidth(targetWidth, targetHeight, containerHeight);
// default to top (0)
var vAlign:Number = 0;
if (verticalAlign == VerticalAlign.MIDDLE)
vAlign = .5;
else if (verticalAlign == VerticalAlign.BOTTOM)
vAlign = 1;
var actualBaseline:Number = 0;
var alignToBaseline:Boolean = verticalAlign == VerticalAlign.BASELINE;
if (alignToBaseline)
{
var result:Array = calculateBaselineTopBottom(false /*calculateBottom*/);
actualBaseline = result[0];
}
// If columnCount wasn't set, then as the LayoutElements are positioned
// we'll count how many columns fall within the layoutTarget's scrollRect
var visibleColumns:uint = 0;
var minVisibleX:Number = layoutTarget.horizontalScrollPosition;
var maxVisibleX:Number = minVisibleX + targetWidth
// Finally, position the LayoutElements and find the first/last
// visible indices, the content size, and the number of
// visible elements.
var x:Number = paddingLeft;
var y0:Number = paddingTop;
var maxX:Number = paddingLeft;
var maxY:Number = paddingTop;
var firstColInView:int = -1;
var lastColInView:int = -1;
// Take horizontalAlign into account
if (excessWidth > 0 || !clipAndEnableScrolling)
{
var hAlign:String = horizontalAlign;
if (hAlign == HorizontalAlign.CENTER)
{
x = paddingLeft + Math.round(excessWidth / 2);
}
else if (hAlign == HorizontalAlign.RIGHT)
{
x = paddingLeft + excessWidth;
}
}
for (var index:int = 0; index < count; index++)
{
layoutElement = layoutTarget.getElementAt(index);
if (!layoutElement || !layoutElement.includeInLayout)
continue;
// Set the layout element's position
var dx:Number = Math.ceil(layoutElement.getLayoutBoundsWidth());
var dy:Number = Math.ceil(layoutElement.getLayoutBoundsHeight());
var y:Number;
if (alignToBaseline)
{
var elementBaseline:Number = layoutElement.baseline as Number;
if (isNaN(elementBaseline))
elementBaseline = 0;
// Note: don't round the position. Rounding will case the text line to shift by
// a pixel and won't look aligned with the other element's text.
var baselinePosition:Number = layoutElement.baselinePosition;
y = y0 + actualBaseline + elementBaseline - baselinePosition;
}
else
{
y = y0 + (containerHeight - dy) * vAlign;
// In case we have VerticalAlign.MIDDLE we have to round
if (vAlign == 0.5)
y = Math.round(y);
}
layoutElement.setLayoutBoundsPosition(x, y);
// Update maxX,Y, first,lastVisibleIndex, and x
maxX = Math.max(maxX, x + dx);
maxY = Math.max(maxY, y + dy);
if (!clipAndEnableScrolling ||
((x < maxVisibleX) && ((x + dx) > minVisibleX)) ||
((dx <= 0) && ((x == maxVisibleX) || (x == minVisibleX))))
{
visibleColumns += 1;
if (firstColInView == -1)
firstColInView = lastColInView = index;
else
lastColInView = index;
}
x += dx + gap;
}
setColumnCount(visibleColumns);
setIndexInView(firstColInView, lastColInView);
// Make sure that if the content spans partially over a pixel to the right/bottom,
// the content size includes the whole pixel.
layoutTarget.setContentSize(Math.ceil(maxX + paddingRight),
Math.ceil(maxY + paddingBottom));
}
/**
* @private
*
* This function sets the width of each child
* so that the widths add up to <code>width</code>.
* Each child is set to its preferred width
* if its percentWidth is zero.
* If its percentWidth is a positive number,
* the child grows (or shrinks) to consume its
* share of extra space.
*
* The return value is any extra space that's left over
* after growing all children to their maxWidth.
*/
private function distributeWidth(width:Number,
height:Number,
restrictedHeight:Number):Number
{
var spaceToDistribute:Number = width;
var totalPercentWidth:Number = 0;
var childInfoArray:Array = [];
var childInfo:HLayoutElementFlexChildInfo;
var newHeight:Number;
var layoutElement:ILayoutElement;
// columnWidth can be expensive to compute
var cw:Number = (variableColumnWidth) ? 0 : Math.ceil(columnWidth);
var count:int = target.numElements;
var totalCount:int = count; // number of elements to use in gap calculation
// If the child is flexible, store information about it in the
// childInfoArray. For non-flexible children, just set the child's
// width and height immediately.
for (var index:int = 0; index < count; index++)
{
layoutElement = target.getElementAt(index);
if (!layoutElement || !layoutElement.includeInLayout)
{
totalCount--;
continue;
}
if (!isNaN(layoutElement.percentWidth) && variableColumnWidth)
{
totalPercentWidth += layoutElement.percentWidth;
childInfo = new HLayoutElementFlexChildInfo();
childInfo.layoutElement = layoutElement;
childInfo.percent = layoutElement.percentWidth;
childInfo.min = layoutElement.getMinBoundsWidth();
childInfo.max = layoutElement.getMaxBoundsWidth();
childInfoArray.push(childInfo);
}
else
{
sizeLayoutElement(layoutElement, height, verticalAlign,
restrictedHeight, NaN, variableColumnWidth, cw);
spaceToDistribute -= Math.ceil(layoutElement.getLayoutBoundsWidth());
}
}
if (totalCount > 1)
spaceToDistribute -= (totalCount-1) * gap;
// Distribute the extra space among the flexible children
if (totalPercentWidth)
{
Flex.flexChildrenProportionally(width,
spaceToDistribute,
totalPercentWidth,
childInfoArray);
var roundOff:Number = 0;
for each (childInfo in childInfoArray)
{
// Make sure the calculated percentages are rounded to pixel boundaries
var childSize:int = Math.round(childInfo.size + roundOff);
roundOff += childInfo.size - childSize;
sizeLayoutElement(childInfo.layoutElement, height, verticalAlign,
restrictedHeight, childSize,
variableColumnWidth, cw);
spaceToDistribute -= childSize;
}
}
return spaceToDistribute;
}
/**
* @private
*/
override public function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
var layoutTarget:GroupBase = target;
if (!layoutTarget)
return;
if ((layoutTarget.numElements == 0) || (unscaledWidth == 0) || (unscaledHeight == 0))
{
setColumnCount(0);
setIndexInView(-1, -1);
if (layoutTarget.numElements == 0)
layoutTarget.setContentSize(Math.ceil(paddingLeft + paddingRight),
Math.ceil(paddingTop + paddingBottom));
return;
}
if (useVirtualLayout)
updateDisplayListVirtual();
else
updateDisplayListReal();
}
/**
* @private
* Convenience function for subclasses that invalidates the
* target's size and displayList so that both layout's <code>measure()</code>
* and <code>updateDisplayList</code> methods get called.
*
* <p>Typically a layout invalidates the target's size and display list so that
* it gets a chance to recalculate the target's default size and also size and
* position the target's elements. For example changing the <code>gap</code>
* property on a <code>VerticalLayout</code> will internally call this method
* to ensure that the elements are re-arranged with the new setting and the
* target's default size is recomputed.</p>
*/
private function invalidateTargetSizeAndDisplayList():void
{
var g:GroupBase = target;
if (!g)
return;
g.invalidateSize();
g.invalidateDisplayList();
}
//--------------------------------------------------------------------------
//
// Drop methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
override protected function calculateDropIndex(x:Number, y:Number):int
{
// Iterate over the visible elements
var layoutTarget:GroupBase = target;
var count:int = layoutTarget.numElements;
// If there are no items, insert at index 0
if (count == 0)
return 0;
// Go through the visible elements
var minDistance:Number = Number.MAX_VALUE;
var bestIndex:int = -1;
var start:int = this.firstIndexInView;
var end:int = this.lastIndexInView;
for (var i:int = start; i <= end; i++)
{
var elementBounds:Rectangle = this.getElementBounds(i);
if (!elementBounds)
continue;
if (elementBounds.left <= x && x <= elementBounds.right)
{
var centerX:Number = elementBounds.x + elementBounds.width / 2;
return (x < centerX) ? i : i + 1;
}
var curDistance:Number = Math.min(Math.abs(x - elementBounds.left),
Math.abs(x - elementBounds.right));
if (curDistance < minDistance)
{
minDistance = curDistance;
bestIndex = (x < elementBounds.left) ? i : i + 1;
}
}
// If there are no visible elements, either pick to drop at the beginning or at the end
if (bestIndex == -1)
bestIndex = getElementBounds(0).x < x ? count : 0;
return bestIndex;
}
/**
* @private
*/
override protected function calculateDropIndicatorBounds(dropLocation:DropLocation):Rectangle
{
var dropIndex:int = dropLocation.dropIndex;
var count:int = target.numElements;
var gap:Number = this.gap;
// Special case, if we insert at the end, and the gap is negative, consider it to be zero
if (gap < 0 && dropIndex == count)
gap = 0;
var emptySpace:Number = (0 < gap ) ? gap : 0;
var emptySpaceLeft:Number = 0;
if (target.numElements > 0)
{
emptySpaceLeft = (dropIndex < count) ? getElementBounds(dropIndex).left - emptySpace :
getElementBounds(dropIndex - 1).right + gap - emptySpace;
}
// Calculate the size of the bounds, take minium and maximum into account
var width:Number = emptySpace;
var height:Number = Math.max(target.height, target.contentHeight) - paddingTop - paddingBottom;
if (dropIndicator is IVisualElement)
{
var element:IVisualElement = IVisualElement(dropIndicator);
width = Math.max(Math.min(width, element.getMaxBoundsWidth(false)), element.getMinBoundsWidth(false));
}
var x:Number = emptySpaceLeft + Math.round((emptySpace - width)/2);
// Allow 1 pixel overlap with container border
x = Math.max(-Math.ceil(width / 2), Math.min(target.contentWidth - Math.ceil(width/2), x));
var y:Number = paddingTop;
return new Rectangle(x, y, width, height);
}
/**
* @private
*/
override protected function calculateDragScrollDelta(dropLocation:DropLocation,
elapsedTime:Number):Point
{
var delta:Point = super.calculateDragScrollDelta(dropLocation, elapsedTime);
// Don't scroll in the vertical direction
if (delta)
delta.y = 0;
return delta;
}
/**
* @private
* Identifies the element which has its "compare point" located closest
* to the specified position.
*/
override mx_internal function getElementNearestScrollPosition(
position:Point,
elementComparePoint:String = "center"):int
{
if (!useVirtualLayout)
return super.getElementNearestScrollPosition(position, elementComparePoint);
var g:GroupBase = GroupBase(target);
if (!g)
return -1;
// We need a valid LLV for this function
updateLLV(g);
// Find the element which overlaps with the position
var index:int = llv.indexOf(position.x);
// Invalid index indicates that the position is past either end of the
// laid-out elements. In this case, choose either the first or last one.
if (index == -1)
index = position.x < 0 ? 0 : g.numElements - 1;
var bounds:Rectangle = llv.getBounds(index);
var adjacentBounds:Rectangle;
// If we're comparing with a right-edge point, check both the current element and the element
// at index-1 to see which is closest.
if ((elementComparePoint == "topRight" || elementComparePoint == "bottomRight") && index > 0)
{
adjacentBounds = llv.getBounds(index - 1);
if (Point.distance(position, adjacentBounds.bottomRight) < Point.distance(position, bounds.bottomRight))
index--;
}
// If we're comparing with a left-edge point, check both the current element and the element
// at index+1 to see which is closest.
if ((elementComparePoint == "topLeft" || elementComparePoint == "bottomLeft") && index < g.numElements-1)
{
adjacentBounds = llv.getBounds(index + 1);
if (Point.distance(position, adjacentBounds.topLeft) < Point.distance(position, bounds.topLeft))
index++;
}
return index;
}
}
}
import mx.containers.utilityClasses.FlexChildInfo;
import mx.core.ILayoutElement;
class HLayoutElementFlexChildInfo extends FlexChildInfo
{
public var layoutElement:ILayoutElement;
}
class SizesAndLimit
{
public var preferredSize:Number;
public var minSize:Number;
}