blob: 20f84efc3c804c52857311bde374847327c4bf43 [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 VerticalLayout class arranges the layout elements in a vertical sequence,
* top to bottom, with optional gaps between the elements and optional padding
* around the sequence of elements.
*
* <p>The vertical position of the elements is determined by arranging them
* in a vertical sequence, top to bottom, taking into account the padding
* before the first element and the gaps between the elements.</p>
*
* <p>The horizontal position of the elements is determined by the layout's
* <code>horizontalAlign</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 <code>requestedRowCount</code> is set, 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 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 <code>variableRowHeight</code> is <code>false</code>,
* then set the element's height to the
* value of the <code>rowHeight</code> property.</li>
*
* <li>If the element's <code>percentHeight</code> is set, then calculate the element's
* height by distributing the available container height between all
* elements with a <code>percentHeight</code> setting.
* The available container height
* is equal to the container height minus the gaps, the padding and the
* space occupied by the rest of the elements. The element's <code>precentHeight</code>
* property is ignored when the layout is virtualized.</li>
*
* <li>Set the element's height to its preferred height.</li>
* </ul>
*
* <p>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>horizontalAlign</code> is <code>"justify"</code>,
* then set the element's width to the container width.</li>
*
* <li>If <code>horizontalAlign</code> is <code>"contentJustify"</code>,
* then set the element's width to the maximum between the container's width
* and all elements' preferred width.</li>
*
* <li>If the element's <code>percentWidth</code> is set, then calculate the element's
* width as a percentage of the container's width.</li>
*
* <li>Set the element's width to its preferred width.</li>
* </ul>
*
* @mxml
* <p>The <code>&lt;s:VerticalLayout&gt;</code> tag inherits all of the tag
* attributes of its superclass and adds the following tag attributes:</p>
*
* <pre>
* &lt;s:VerticalLayout
* <strong>Properties</strong>
* gap="6"
* horizontalAlign="left"
* paddingBottom="0"
* paddingLeft="0"
* paddingRight="0"
* paddingTop="0"
* requestedMaxRowCount="-1"
* requestedMinRowCount="-1"
* requestedRowCount="-1"
* rowHeight="<i>calculated</i>"
* variableRowHeight="true"
* verticalAlign="top"
* /&gt;
* </pre>
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class VerticalLayout extends LayoutBase
{
include "../core/Version.as";
/**
* @private
* Cached row heights, max column width for virtual layout. Not used unless
* useVirtualLayout=true. See updateLLV(), resetCachedVirtualLayoutState(),
* etc.
*/
private var llv:LinearLayoutVector;
//--------------------------------------------------------------------------
//
// Class methods
//
//--------------------------------------------------------------------------
private static function calculatePercentWidth(layoutElement:ILayoutElement, width:Number):Number
{
var percentWidth:Number;
if (FlexVersion.compatibilityVersion < FlexVersion.VERSION_4_6)
{
percentWidth = LayoutElementHelper.pinBetween(Math.round(layoutElement.percentWidth * 0.01 * width),
layoutElement.getMinBoundsWidth(),
layoutElement.getMaxBoundsWidth() );
return percentWidth < width ? percentWidth : width;
}
else
{
percentWidth = LayoutElementHelper.pinBetween(Math.min(Math.round(layoutElement.percentWidth * 0.01 * width), width),
layoutElement.getMinBoundsWidth(),
layoutElement.getMaxBoundsWidth() );
return percentWidth;
}
}
private static function sizeLayoutElement(layoutElement:ILayoutElement,
width:Number,
horizontalAlign:String,
restrictedWidth:Number,
height:Number,
variableRowHeight:Boolean,
rowHeight:Number):void
{
var newWidth:Number = NaN;
// if horizontalAlign is "justify" or "contentJustify",
// restrict the width to restrictedWidth. Otherwise,
// size it normally
if (horizontalAlign == HorizontalAlign.JUSTIFY ||
horizontalAlign == HorizontalAlign.CONTENT_JUSTIFY)
{
newWidth = restrictedWidth;
}
else
{
if (!isNaN(layoutElement.percentWidth))
newWidth = calculatePercentWidth(layoutElement, width);
}
if (variableRowHeight)
layoutElement.setLayoutBoundsSize(newWidth, height);
else
layoutElement.setLayoutBoundsSize(newWidth, rowHeight);
}
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function VerticalLayout():void
{
super();
// Don't drag-scroll in the horizontal direction
dragScrollRegionSizeHorizontal = 0;
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// gap
//----------------------------------
private var _gap:int = 6;
[Inspectable(category="General")]
/**
* The vertical 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();
}
//----------------------------------
// rowCount
//----------------------------------
private var _rowCount:int = -1;
[Bindable("propertyChange")]
[Inspectable(category="General")]
/**
* The current number of visible elements.
*
* @default -1
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get rowCount():int
{
return _rowCount;
}
/**
* @private
*
* Sets the <code>rowCount</code> property and dispatches a
* PropertyChangeEvent.
*/
mx_internal function setRowCount(value:int):void
{
if (_rowCount == value)
return;
var oldValue:int = _rowCount;
_rowCount = value;
dispatchEvent(PropertyChangeEvent.createUpdateEvent(this, "rowCount", oldValue, value));
}
//----------------------------------
// horizontalAlign
//----------------------------------
/**
* @private
*/
private var _horizontalAlign:String = HorizontalAlign.LEFT;
[Inspectable(category="General", enumeration="left,right,center,justify,contentJustify", defaultValue="left")]
/**
* The horizontal alignment of layout elements.
* 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>If the value is <code>"contentJustify"</code>, then the layout element's actual
* width is set to the <code>contentWidth</code> of the container.
* The <code>contentWidth</code> of the container is the width of the largest layout element.
* If all layout elements are smaller than the width of the container,
* then set the width of all layout elements to the width of the container.</p>
*
* <p>If the value is <code>"justify"</code> then the layout element's actual width
* is set to 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", defaultValue="top")]
/**
* The vertical alignment of the content relative to the container's height.
*
* <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>This property has no effect when <code>clipAndEnableScrolling</code> is true
* and the <code>contentHeight</code> is greater than the container's height.</p>
*
* <p>This property does not affect the layout's measured size.</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;
_verticalAlign = value;
var layoutTarget:GroupBase = target;
if (layoutTarget)
layoutTarget.invalidateDisplayList();
}
//----------------------------------
// paddingLeft
//----------------------------------
private var _paddingLeft:Number = 0;
[Inspectable(category="General")]
/**
* The minimum number of pixels between the container's left edge and
* the left edge of the 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")]
/**
* The minimum number of pixels between the container's right edge and
* the right edge of the 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")]
/**
* Number of pixels between the container's top edge
* and the top edge of the first layout element.
*
* @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")]
/**
* Number of pixels between the container's bottom edge
* and the bottom edge of the last layout element.
*
* @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();
}
//----------------------------------
// requestedMaxRowCount
//----------------------------------
private var _requestedMaxRowCount:int = -1;
[Inspectable(category="General", minValue="-1")]
/**
* The measured height of this layout is large enough to display
* at most <code>requestedMaxRowCount</code> layout elements.
*
* <p>If <code>requestedRowCount</code> is set, then
* this property has no effect.</p>
*
* <p>If the actual size of the container has been explicitly set,
* then this property has no effect.</p>
*
* @default -1
* @see #requestedRowCount
* @see #requestedMinRowCount
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get requestedMaxRowCount():int
{
return _requestedMaxRowCount;
}
/**
* @private
*/
public function set requestedMaxRowCount(value:int):void
{
if (_requestedMaxRowCount == value)
return;
_requestedMaxRowCount = value;
if (target)
target.invalidateSize();
}
//----------------------------------
// requestedMinRowCount
//----------------------------------
private var _requestedMinRowCount:int = -1;
[Inspectable(category="General", minValue="-1")]
/**
* The measured height of this layout is large enough to display
* at least <code>requestedMinRowCount</code> layout elements.
*
* <p>If <code>requestedRowCount</code> is set, then
* this property has no effect.</p>
*
* <p>If the actual size of the container has been explicitly set,
* then this property has no effect.</p>
*
* @default -1
* @see #requestedRowCount
* @see #requestedMaxRowCount
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get requestedMinRowCount():int
{
return _requestedMinRowCount;
}
/**
* @private
*/
public function set requestedMinRowCount(value:int):void
{
if (_requestedMinRowCount == value)
return;
_requestedMinRowCount = value;
if (target)
target.invalidateSize();
}
//----------------------------------
// requestedRowCount
//----------------------------------
private var _requestedRowCount:int = -1;
[Inspectable(category="General", minValue="-1")]
/**
* The measured size of this layout is tall enough to display
* the first <code>requestedRowCount</code> layout elements.
*
* <p>If <code>requestedRowCount</code> is -1, then the measured
* size will be big enough for all of the layout elements.</p>
*
* <p>If the actual size of the container has been explicitly set,
* then this property has no effect.</p>
*
* @default -1
* @see #requestedMinRowCount
* @see #requestedMaxRowCount
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get requestedRowCount():int
{
return _requestedRowCount;
}
/**
* @private
*/
public function set requestedRowCount(value:int):void
{
if (_requestedRowCount == value)
return;
_requestedRowCount = value;
if (target)
target.invalidateSize();
}
//----------------------------------
// rowHeight
//----------------------------------
private var _rowHeight:Number;
[Inspectable(category="General", minValue="0.0")]
/**
* If <code>variableRowHeight</code> is <code>false</code>, then
* this property specifies the actual height of each child, in pixels.
*
* <p>If <code>variableRowHeight</code> is <code>true</code>,
* the default, then this property has no effect.</p>
*
* <p>The default value of this property is the preferred height
* of the <code>typicalLayoutElement</code>.</p>
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get rowHeight():Number
{
if (!isNaN(_rowHeight))
return _rowHeight;
else
{
var elt:ILayoutElement = typicalLayoutElement
return (elt) ? elt.getPreferredBoundsHeight() : 0;
}
}
/**
* @private
*/
public function set rowHeight(value:Number):void
{
if (_rowHeight == value)
return;
_rowHeight = value;
invalidateTargetSizeAndDisplayList();
}
//----------------------------------
// variableRowHeight
//----------------------------------
/**
* @private
*/
private var _variableRowHeight:Boolean = true;
[Inspectable(category="General", enumeration="true,false")]
/**
* Specifies whether layout elements are allocated their
* preferred height.
* Setting this property to <code>false</code> specifies fixed height rows.
*
* <p>If <code>false</code>, the actual height of each layout element is
* the value of <code>rowHeight</code>.
* Setting this property to <code>false</code> causes the layout to ignore
* the layout elements' <code>percentHeight</code> property.</p>
*
* @default true
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get variableRowHeight():Boolean
{
return _variableRowHeight;
}
/**
* @private
*/
public function set variableRowHeight(value:Boolean):void
{
if (value == _variableRowHeight)
return;
_variableRowHeight = value;
invalidateTargetSizeAndDisplayList();
}
//----------------------------------
// firstIndexInView
//----------------------------------
/**
* @private
*/
private var _firstIndexInView:int = -1;
[Inspectable(category="General")]
[Bindable("indexInViewChanged")]
/**
* The index of the first layout element that is part of the
* layout and within the layout target's scroll rectangle, or -1
* if nothing has been displayed yet.
*
* <p>"Part of the layout" means that the element is non-null
* and that its <code>includeInLayout</code> property is <code>true</code>.</p>
*
* <p>Note that the layout element may only be partially in view.</p>
*
* @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 row that's part of the layout and within
* the container's scroll rectangle, or -1 if nothing has been displayed yet.
*
* <p>"Part of the layout" means that the child is non-null
* and that its <code>includeInLayout</code> property is <code>true</code>.</p>
*
* <p>Note that the row may only be partially in view.</p>
*
* @see firstIndexInView
* @see fractionOfElementInView
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get lastIndexInView():int
{
return _lastIndexInView;
}
/**
* 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
*/
mx_internal 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 vAlign:String = verticalAlign;
if (vAlign == VerticalAlign.MIDDLE || vAlign == VerticalAlign.BOTTOM)
{
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 vertical 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 row.
*
* @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 = 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;
// outside the visible index 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 Y and Height
var eltY:Number;
var eltHeight:Number;
if (useVirtualLayout)
{
if (!llv)
return 0.0;
eltY = llv.start(index);
eltHeight = llv.getMajorSize(index);
}
else
{
var elt:ILayoutElement = g.getElementAt(index);
if (!elt || !elt.includeInLayout)
return 0.0;
eltY = elt.getLayoutBoundsY();
eltHeight = elt.getLayoutBoundsHeight();
}
// So, index is either the first or last row in the scrollRect
// and potentially partially visible.
// y0,y1 - scrollRect top,bottom edges
// iy0, iy1 - layout element top,bottom edges
var y0:Number = g.verticalScrollPosition;
var y1:Number = y0 + g.height;
var iy0:Number = eltY;
var iy1:Number = iy0 + eltHeight;
if (iy0 >= iy1) // element has 0 or negative height
return 1.0;
if ((iy0 >= y0) && (iy1 <= y1))
return 1.0;
return (Math.min(y1, iy1) - Math.max(y0, iy0)) / (iy1 - iy0);
}
/**
* @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 y, or -1.
*/
private static function findIndexAt(y:Number, gap:int, g:GroupBase, i0:int, i1:int):int
{
var index:int = (i0 + i1) / 2;
var element:ILayoutElement = g.getElementAt(index);
var elementY:Number = element.getLayoutBoundsY();
var elementHeight:Number = element.getLayoutBoundsHeight();
// TBD: deal with null element, includeInLayout false.
if ((y >= elementY) && (y < elementY + elementHeight + gap))
return index;
else if (i0 == i1)
return -1;
else if (y < elementY)
return findIndexAt(y, gap, g, i0, Math.max(i0, index-1));
else
return findIndexAt(y, 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 top and bottom edges of the scrollRect.
// Values that are exactly equal to scrollRect.bottom aren't actually
// rendered, since the top,bottom interval is only half open.
// To account for that we back away from the bottom edge by a
// hopefully infinitesimal amount.
var y0:Number = scrollR.top;
var y1:Number = scrollR.bottom - .0001;
if (y1 <= y0)
{
setIndexInView(-1, -1);
return;
}
if (useVirtualLayout && !llv)
{
setIndexInView(-1, -1);
return;
}
var i0:int;
var i1:int;
if (useVirtualLayout)
{
i0 = llv.indexOf(y0);
i1 = llv.indexOf(y1);
}
else
{
i0 = findIndexAt(y0 + gap, gap, g, 0, n);
i1 = findIndexAt(y1, gap, g, 0, n);
}
// Special case: no element overlaps y0, is index 0 visible?
if (i0 == -1)
{
var index0:int = findLayoutElementIndex(g, 0, +1);
if (index0 != -1)
{
var element0:ILayoutElement = g.getElementAt(index0);
var element0Y:Number = element0.getLayoutBoundsY();
var elementHeight:Number = element0.getLayoutBoundsHeight();
if ((element0Y < y1) && ((element0Y + elementHeight) > y0))
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 element1Y:Number = element1.getLayoutBoundsY();
var element1Height:Number = element1.getLayoutBoundsHeight();
if ((element1Y < y1) && ((element1Y + element1Height) > y0))
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.top < firstElement.getLayoutBoundsY() ||
scrollRect.bottom >= (lastElement.getLayoutBoundsY() + lastElement.getLayoutBoundsHeight()))
{
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, 0, paddingTop);
if (i >= n)
return new Rectangle(0, getElementBounds(n-1).bottom, 0, paddingBottom);
}
while((i >= 0) && (i < n))
{
var elementR:Rectangle = getElementBounds(i);
// Special case: if the scrollRect r _only_ contains
// elementR, then if we're searching up (dir == -1),
// and elementR's top edge is visible, then try again
// with i-1. Likewise for dir == +1.
if (elementR)
{
var overlapsTop:Boolean = (dir == -1) && (elementR.top == r.top) && (elementR.bottom >= r.bottom);
var overlapsBottom:Boolean = (dir == +1) && (elementR.bottom == r.bottom) && (elementR.top <= r.top);
if (!(overlapsTop || overlapsBottom))
return elementR;
}
i += dir;
}
return null;
}
/**
* @private
*/
override protected function getElementBoundsAboveScrollRect(scrollRect:Rectangle):Rectangle
{
return findLayoutElementBounds(target, firstIndexInView, -1, scrollRect);
}
/**
* @private
*/
override protected function getElementBoundsBelowScrollRect(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, justify:Boolean, result:SizesAndLimit):void
{
// Calculate preferred width first, as it's being used to calculate min width
var elementPreferredWidth:Number = Math.ceil(element.getPreferredBoundsWidth());
// Calculate min width
var flexibleWidth:Boolean = !isNaN(element.percentWidth) || justify;
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, fixedRowHeight:Number, result:SizesAndLimit):void
{
// Calculate preferred height first, as it's being used to calculate min height below
var elementPreferredHeight:Number = isNaN(fixedRowHeight) ? Math.ceil(element.getPreferredBoundsHeight()) :
fixedRowHeight;
// Calculate min height
var flexibleHeight:Boolean = !isNaN(element.percentHeight);
var elementMinHeight:Number = flexibleHeight ? Math.ceil(element.getMinBoundsHeight()) :
elementPreferredHeight;
result.preferredSize = elementPreferredHeight;
result.minSize = elementMinHeight;
}
/**
* @private
* @return rows to measure based on elements in layout and any requested/min/max rowCount settings.
*/
mx_internal function getRowsToMeasure(numElementsInLayout:int):int
{
var rowsToMeasure:int;
if (requestedRowCount != -1)
rowsToMeasure = requestedRowCount;
else
{
rowsToMeasure = numElementsInLayout;
if (requestedMaxRowCount != -1)
rowsToMeasure = Math.min(requestedMaxRowCount, rowsToMeasure);
if (requestedMinRowCount != -1)
rowsToMeasure = Math.max(requestedMinRowCount, rowsToMeasure);
}
return rowsToMeasure;
}
/**
* @private
*
* Compute exact values for measuredWidth,Height and measuredMinWidth,Height.
*
* Measure each of the layout elements. If requestedRowCount >= 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 width of the elements remaining.
*
* If requestedRowCount is -1, we measure all of the layout elements.
*/
private function measureReal(layoutTarget:GroupBase):void
{
var size:SizesAndLimit = new SizesAndLimit();
var justify:Boolean = horizontalAlign == HorizontalAlign.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 requestedRowCount:int = this.requestedRowCount;
var rowsMeasured:int = 0; // How many rows 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 fixedRowHeight:Number = NaN;
if (!variableRowHeight)
fixedRowHeight = rowHeight; // may query typicalLayoutElement, elt at index=0
// Get the numElementsInLayout clamped to requested min/max
var rowsToMeasure:int = getRowsToMeasure(numElementsInLayout);
var element:ILayoutElement;
for (var i:int = 0; i < numElements; i++)
{
element = layoutTarget.getElementAt(i);
if (!element || !element.includeInLayout)
{
numElementsInLayout--;
continue;
}
// Can we measure this row height?
if (rowsMeasured < rowsToMeasure)
{
getElementHeight(element, fixedRowHeight, size);
preferredHeight += size.preferredSize;
minHeight += size.minSize;
rowsMeasured++;
}
// Consider the width of each element, inclusive of those outside
// the requestedRowCount range.
getElementWidth(element, justify, size);
preferredWidth = Math.max(preferredWidth, size.preferredSize);
minWidth = Math.max(minWidth, size.minSize);
}
// Calculate the total number of rows to measure again, since numElementsInLayout may have changed
rowsToMeasure = getRowsToMeasure(numElementsInLayout);
// Do we need to measure more rows?
if (rowsMeasured < rowsToMeasure)
{
// Use the typical element
element = typicalLayoutElement;
if (element)
{
// Height
getElementHeight(element, fixedRowHeight, size);
preferredHeight += size.preferredSize * (rowsToMeasure - rowsMeasured);
minHeight += size.minSize * (rowsToMeasure - rowsMeasured);
// Width
getElementWidth(element, justify, size);
preferredWidth = Math.max(preferredWidth, size.preferredSize);
minWidth = Math.max(minWidth, size.minSize);
rowsMeasured = rowsToMeasure;
}
}
// Add gaps
if (rowsMeasured > 1)
{
var vgap:Number = gap * (rowsMeasured - 1);
preferredHeight += vgap;
minHeight += vgap;
}
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();
// 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 = 71;
llv.defaultMajorSize = 22;
}
var typicalElt:ILayoutElement = typicalLayoutElement;
if (typicalElt)
{
var typicalWidth:Number = typicalElt.getPreferredBoundsWidth();
var typicalHeight:Number = typicalElt.getPreferredBoundsHeight();
llv.defaultMinorSize = typicalWidth;
llv.defaultMajorSize = typicalHeight;
}
if (!isNaN(_rowHeight))
llv.defaultMajorSize = _rowHeight;
if (layoutTarget)
llv.length = layoutTarget.numElements;
llv.gap = gap;
llv.majorAxisOffset = paddingTop;
}
/**
* @private
*/
override public function elementAdded(index:int):void
{
if (llv && (index >= 0) && useVirtualLayout)
llv.insert(index); // insert index parameter is uint
}
/**
* @private
*/
override public function elementRemoved(index:int):void
{
if (llv && (index >= 0) && useVirtualLayout)
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 variableRowHeight="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 = getRowsToMeasure(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 (variableRowHeight)
{
// Special case: fewer elements than requestedRowCount, so temporarily
// make llv.length == requestedRowCount.
var oldLength:int = -1;
if (measuredEltCount > llv.length)
{
oldLength = llv.length;
llv.length = measuredEltCount;
}
// paddingTop is already taken into account as the majorAxisOffset of the llv
// Measured size according to the cached actual size:
var measuredHeight:Number = llv.end(measuredEltCount - 1) + paddingBottom;
// 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)
{
measuredHeight -= llv.getMajorSize(i);
measuredHeight += element.getPreferredBoundsHeight();
}
}
}
layoutTarget.measuredHeight = measuredHeight;
if (oldLength != -1)
llv.length = oldLength;
}
else
{
var vgap:Number = (measuredEltCount > 1) ? (measuredEltCount - 1) * gap : 0;
layoutTarget.measuredHeight = (measuredEltCount * rowHeight) + vgap + vPadding;
}
layoutTarget.measuredWidth = llv.minorSize + hPadding;
layoutTarget.measuredMinWidth = (horizontalAlign == HorizontalAlign.JUSTIFY) ?
llv.minMinorSize + hPadding : layoutTarget.measuredWidth;
layoutTarget.measuredMinHeight = layoutTarget.measuredHeight;
}
/**
* @private
*
* If requestedRowCount is specified then as many layout elements
* or "rows" are measured, starting with element 0, otherwise all of the
* layout elements are measured.
*
* If requestedRowCount is specified and is greater than the
* number of layout elements, then the typicalLayoutElement is used
* in place of the missing layout elements.
*
* If variableRowHeight="true", then the layoutTarget's measuredHeight
* is the sum of preferred heights of the layout elements, plus the sum of the
* gaps between elements, and its measuredWidth is the max of the elements'
* preferred widths.
*
* If variableRowHeight="false", then the layoutTarget's measuredHeight
* is rowHeight multiplied by the number or layout elements, plus the
* sum of the gaps between elements.
*
* The layoutTarget's measuredMinHeight is the sum of the minHeights of
* layout elements that have specified a value for the percentHeight
* property, and the preferredHeight of the elements that have not,
* plus the sum of the gaps between elements.
*
* The difference reflects the fact that elements which specify
* percentHeight are considered to be "flexible" and updateDisplayList
* will give flexible components at least their minHeight.
*
* Layout elements that aren't flexible always get their preferred height.
*
* The layoutTarget's measuredMinWidth is the max of the minWidths for
* elements that have specified percentWidth (that are "flexible") and the
* preferredWidth of the elements that have not.
*
* As before the difference is due to the fact that flexible items are only
* guaranteed their minWidth.
*/
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.UP)
return arrowKeysWrapFocus ? maxIndex : -1;
if (navigationUnit == NavigationUnit.DOWN)
return 0;
}
// Make sure currentIndex is within range
currentIndex = Math.max(0, Math.min(maxIndex, currentIndex));
var newIndex:int;
var bounds:Rectangle;
var y:Number;
switch (navigationUnit)
{
case NavigationUnit.UP:
{
if (arrowKeysWrapFocus && currentIndex == 0)
newIndex = maxIndex;
else
newIndex = currentIndex - 1;
break;
}
case NavigationUnit.DOWN:
{
if (arrowKeysWrapFocus && currentIndex == maxIndex)
newIndex = 0;
else
newIndex = currentIndex + 1;
break;
}
case NavigationUnit.PAGE_UP:
{
// 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 up
if (currentIndex == firstFullyVisible || currentIndex == firstVisible)
{
// currentIndex is visible, we can calculate where the scrollRect top
// would end up if we scroll by a page
y = getVerticalScrollPositionDelta(NavigationUnit.PAGE_UP) + getScrollRect().top;
}
else
{
// currentIndex is not visible, just find an element a page up from currentIndex
y = getElementBounds(currentIndex).bottom - getScrollRect().height;
}
// Find the element after the last element that spans above the y position
newIndex = currentIndex - 1;
while (0 <= newIndex)
{
bounds = getElementBounds(newIndex);
if (bounds && bounds.top < y)
{
// 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:
{
// 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 down
if (currentIndex == lastFullyVisible || currentIndex == lastVisible)
{
// currentIndex is visible, we can calculate where the scrollRect bottom
// would end up if we scroll by a page
y = getVerticalScrollPositionDelta(NavigationUnit.PAGE_DOWN) + getScrollRect().bottom;
}
else
{
// currentIndex is not visible, just find an element a page down from currentIndex
y = getElementBounds(currentIndex).top + getScrollRect().height;
}
// Find the element before the first element that spans below the y position
newIndex = currentIndex + 1;
while (newIndex <= maxIndex)
{
bounds = getElementBounds(newIndex);
if (bounds && bounds.bottom > y)
{
// 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 calculateElementWidth(elt:ILayoutElement, targetWidth:Number, containerWidth:Number):Number
{
// If percentWidth is specified then the element's width is the percentage
// of targetWidth clipped to min/maxWidth and to (upper limit) targetWidth.
var percentWidth:Number = elt.percentWidth;
if (!isNaN(percentWidth))
{
var width:Number = percentWidth * 0.01 * targetWidth;
return Math.min(targetWidth, Math.min(elt.getMaxBoundsWidth(), Math.max(elt.getMinBoundsWidth(), width)));
}
switch(horizontalAlign)
{
case HorizontalAlign.JUSTIFY:
return targetWidth;
case HorizontalAlign.CONTENT_JUSTIFY:
return Math.max(elt.getPreferredBoundsWidth(), containerWidth);
}
return NaN; // not constrained
}
/**
* @private
*
* Used only for virtual layout.
*/
private function calculateElementX(elt:ILayoutElement, eltWidth:Number, containerWidth:Number):Number
{
switch(horizontalAlign)
{
case HorizontalAlign.CENTER:
return Math.round((containerWidth - eltWidth) * 0.5);
case HorizontalAlign.RIGHT:
return containerWidth - eltWidth;
}
return 0; // HorizontalAlign.LEFT
}
/**
* @private
*
* Update the layout of the virtualized elements that overlap
* the scrollRect's vertical extent.
*
* The height of each layout element will be its preferred height, and its
* y will be the bottom of the previous item, plus the gap.
*
* No support for percentHeight, includeInLayout=false, or null layoutElements,
*
* The width of each layout element will be set to its preferred width, unless
* one of the following is true:
*
* - If percentWidth is specified for this element, then its width will be the
* specified percentage of the target's actual (unscaled) width, clipped
* the layout element's minimum and maximum width.
*
* - If horizontalAlign is "justify", then the element's width will
* be set to the target's actual (unscaled) width.
*
* - If horizontalAlign is "contentJustify", then the element's width
* will be set to the larger of the target's width and its content width.
*
* The X coordinate of each layout element will be set to 0 unless one of the
* following is true:
*
* - If horizontalAlign is "center" then x is set so that the element's preferred
* width is centered within the larger of the contentWidth, target width:
* x = (Math.max(contentWidth, target.width) - layoutElementWidth) * 0.5
*
* - If horizontalAlign is "right" the x is set so that the element's right
* edge is aligned with the the right edge of the content:
* x = (Math.max(contentWidth, target.width) - layoutElementWidth)
*
* Implementation note: unless horizontalAlign is either "justify" or
* "left", the layout elements' x or width depends on the contentWidth.
* The contentWidth 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 contentWidth
* for the initial layout and then, if it has changed, we loop through
* the layout items again and fix up the x/width values.
*/
private function updateDisplayListVirtual():void
{
var layoutTarget:GroupBase = target;
var eltCount:int = layoutTarget.numElements;
var targetWidth:Number = Math.max(0, layoutTarget.width - paddingLeft - paddingRight);
var minVisibleY:Number = layoutTarget.verticalScrollPosition;
var maxVisibleY:Number = minVisibleY + layoutTarget.height;
var contentHeight:Number;
var paddedContentHeight: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 minVisibleY - SDK-22497.
var startIndex:int = llv.indexOf(Math.max(0, minVisibleY + gap));
if (startIndex == -1)
{
// No items are visible. Just set the content size.
contentHeight = llv.end(llv.length - 1) - paddingTop;
paddedContentHeight = Math.ceil(contentHeight + paddingTop + paddingBottom);
layoutTarget.setContentSize(layoutTarget.contentWidth, paddedContentHeight);
return;
}
var fixedRowHeight:Number = NaN;
if (!variableRowHeight)
fixedRowHeight = rowHeight; // may query typicalLayoutElement, elt at index=0
var justifyWidths:Boolean = horizontalAlign == HorizontalAlign.JUSTIFY;
var eltWidth:Number = (justifyWidths) ? targetWidth : NaN;
var eltHeight:Number = NaN;
var contentWidth:Number = (justifyWidths) ? Math.max(llv.minMinorSize, targetWidth) : llv.minorSize;
var containerWidth:Number = Math.max(contentWidth, targetWidth);
var y:Number = llv.start(startIndex);
var index:int = startIndex;
var x0:Number = paddingLeft;
// First pass: compute element x,y,width,height based on
// current contentWidth; cache computed widths/heights in llv.
for (; (y < maxVisibleY) && (index < eltCount); index++)
{
var elt:ILayoutElement = layoutTarget.getVirtualElementAt(index, eltWidth, eltHeight);
var w:Number = calculateElementWidth(elt, targetWidth, containerWidth); // can be NaN
var h:Number = fixedRowHeight; // NaN for variable height rows
elt.setLayoutBoundsSize(w, h);
w = elt.getLayoutBoundsWidth();
h = elt.getLayoutBoundsHeight();
var x:Number = x0 + calculateElementX(elt, w, containerWidth);
elt.setLayoutBoundsPosition(x, y);
llv.cacheDimensions(index, elt);
y += h + gap;
}
var endIndex:int = index - 1;
// Second pass: if neccessary, fix up x and width values based
// on the updated contentWidth
if (!justifyWidths && (llv.minorSize != contentWidth))
{
contentWidth = llv.minorSize;
containerWidth = Math.max(contentWidth, targetWidth);
if (horizontalAlign != HorizontalAlign.LEFT)
{
for (index = startIndex; index <= endIndex; index++)
{
elt = layoutTarget.getElementAt(index);
w = calculateElementWidth(elt, targetWidth, containerWidth); // can be NaN
elt.setLayoutBoundsSize(w, elt.getLayoutBoundsHeight());
w = elt.getLayoutBoundsWidth();
x = x0 + calculateElementX(elt, w, containerWidth);
elt.setLayoutBoundsPosition(x, elt.getLayoutBoundsY());
}
}
}
// Third pass: if neccessary, fix up y based on updated contentHeight
contentHeight = llv.end(llv.length - 1) - paddingTop;
var targetHeight:Number = Math.max(0, layoutTarget.height - paddingTop - paddingBottom);
if (contentHeight < targetHeight)
{
var excessHeight:Number = targetHeight - contentHeight;
var dy:Number = 0;
var vAlign:String = verticalAlign;
if (vAlign == VerticalAlign.MIDDLE)
{
dy = Math.round(excessHeight / 2);
}
else if (vAlign == VerticalAlign.BOTTOM)
{
dy = excessHeight;
}
if (dy > 0)
{
for (index = startIndex; index <= endIndex; index++)
{
elt = layoutTarget.getElementAt(index);
elt.setLayoutBoundsPosition(elt.getLayoutBoundsX(), dy + elt.getLayoutBoundsY());
}
contentHeight += dy;
}
}
setRowCount(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.
var paddedContentWidth:Number = Math.ceil(contentWidth + paddingLeft + paddingRight);
paddedContentHeight = 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 horizontalAlign is left, we don't need to figure out the contentWidth
// Otherwise the contentWidth is used to position the element and even size
// the element if it's "contentJustify" or "justify".
var containerWidth:Number = targetWidth;
if (horizontalAlign == HorizontalAlign.CONTENT_JUSTIFY ||
(clipAndEnableScrolling && (horizontalAlign == HorizontalAlign.CENTER ||
horizontalAlign == HorizontalAlign.RIGHT)))
{
for (var i:int = 0; i < count; i++)
{
layoutElement = layoutTarget.getElementAt(i);
if (!layoutElement || !layoutElement.includeInLayout)
continue;
var layoutElementWidth:Number;
if (!isNaN(layoutElement.percentWidth))
layoutElementWidth = calculatePercentWidth(layoutElement, targetWidth);
else
layoutElementWidth = layoutElement.getPreferredBoundsWidth();
containerWidth = Math.max(containerWidth, Math.ceil(layoutElementWidth));
}
}
var excessHeight:Number = distributeHeight(targetWidth, targetHeight, containerWidth);
// default to left (0)
var hAlign:Number = 0;
if (horizontalAlign == HorizontalAlign.CENTER)
hAlign = .5;
else if (horizontalAlign == HorizontalAlign.RIGHT)
hAlign = 1;
// As the layoutElements are positioned, we'll count how many rows
// fall within the layoutTarget's scrollRect
var visibleRows:uint = 0;
var minVisibleY:Number = layoutTarget.verticalScrollPosition;
var maxVisibleY:Number = minVisibleY + targetHeight;
// Finally, position the layoutElements and find the first/last
// visible indices, the content size, and the number of
// visible elements.
var x0:Number = paddingLeft;
var y:Number = paddingTop;
var maxX:Number = paddingLeft;
var maxY:Number = paddingTop;
var firstRowInView:int = -1;
var lastRowInView:int = -1;
// Take verticalAlign into account
if (excessHeight > 0 || !clipAndEnableScrolling)
{
var vAlign:String = verticalAlign;
if (vAlign == VerticalAlign.MIDDLE)
{
y = paddingTop + Math.round(excessHeight / 2);
}
else if (vAlign == VerticalAlign.BOTTOM)
{
y = paddingTop + excessHeight;
}
}
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 x:Number = x0 + (containerWidth - dx) * hAlign;
// In case we have HorizontalAlign.CENTER we have to round
if (hAlign == 0.5)
x = Math.round(x);
layoutElement.setLayoutBoundsPosition(x, y);
// Update maxX,Y, first,lastVisibleIndex, and y
maxX = Math.max(maxX, x + dx);
maxY = Math.max(maxY, y + dy);
if (!clipAndEnableScrolling ||
((y < maxVisibleY) && ((y + dy) > minVisibleY)) ||
((dy <= 0) && ((y == maxVisibleY) || (y == minVisibleY))))
{
visibleRows += 1;
if (firstRowInView == -1)
firstRowInView = lastRowInView = index;
else
lastRowInView = index;
}
y += dy + gap;
}
setRowCount(visibleRows);
setIndexInView(firstRowInView, lastRowInView);
// 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 height of each child
* so that the heights add up to <code>height</code>.
* Each child is set to its preferred height
* if its percentHeight is zero.
* If its percentHeight 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 maxHeight.
*/
private function distributeHeight(width:Number,
height:Number,
restrictedWidth:Number):Number
{
var spaceToDistribute:Number = height;
var totalPercentHeight:Number = 0;
var childInfoArray:Array = [];
var childInfo:LayoutElementFlexChildInfo;
var layoutElement:ILayoutElement;
// rowHeight can be expensive to compute
var rh:Number = (variableRowHeight) ? 0 : Math.ceil(rowHeight);
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.percentHeight) && variableRowHeight)
{
totalPercentHeight += layoutElement.percentHeight;
childInfo = new LayoutElementFlexChildInfo();
childInfo.layoutElement = layoutElement;
childInfo.percent = layoutElement.percentHeight;
childInfo.min = layoutElement.getMinBoundsHeight();
childInfo.max = layoutElement.getMaxBoundsHeight();
childInfoArray.push(childInfo);
}
else
{
sizeLayoutElement(layoutElement, width, horizontalAlign,
restrictedWidth, NaN, variableRowHeight, rh);
spaceToDistribute -= Math.ceil(layoutElement.getLayoutBoundsHeight());
}
}
if (totalCount > 1)
spaceToDistribute -= (totalCount-1) * gap;
// Distribute the extra space among the flexible children
if (totalPercentHeight)
{
Flex.flexChildrenProportionally(height,
spaceToDistribute,
totalPercentHeight,
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, width, horizontalAlign,
restrictedWidth, childSize,
variableRowHeight, rh);
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))
{
setRowCount(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.top <= y && y <= elementBounds.bottom)
{
var centerY:Number = elementBounds.y + elementBounds.height / 2;
return (y < centerY) ? i : i + 1;
}
var curDistance:Number = Math.min(Math.abs(y - elementBounds.top),
Math.abs(y - elementBounds.bottom));
if (curDistance < minDistance)
{
minDistance = curDistance;
bestIndex = (y < elementBounds.top) ? 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).y < y ? 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 emptySpaceTop:Number = 0;
if (target.numElements > 0)
{
emptySpaceTop = (dropIndex < count) ? getElementBounds(dropIndex).top - emptySpace :
getElementBounds(dropIndex - 1).bottom + gap - emptySpace;
}
// Calculate the size of the bounds, take minium and maximum into account
var width:Number = Math.max(target.width, target.contentWidth) - paddingLeft - paddingRight;
var height:Number = emptySpace;
if (dropIndicator is IVisualElement)
{
var element:IVisualElement = IVisualElement(dropIndicator);
height = Math.max(Math.min(height, element.getMaxBoundsHeight(false)), element.getMinBoundsHeight(false));
}
var x:Number = paddingLeft;
var y:Number = emptySpaceTop + Math.round((emptySpace - height)/2);
// Allow 1 pixel overlap with container border
y = Math.max(-1, Math.min(target.contentHeight - height + 1, y));
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 horizontal direction
if (delta)
delta.x = 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.y);
// 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.y < 0 ? 0 : g.numElements - 1;
var bounds:Rectangle = llv.getBounds(index);
var adjacentBounds:Rectangle;
// If we're comparing with a bottom-edge point, check both the current element and the element
// at index-1 to see which is closest.
if ((elementComparePoint == "bottomLeft" || 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 top-edge point, check both the current element and the element
// at index+1 to see which is closest.
if ((elementComparePoint == "topLeft" || elementComparePoint == "topRight") && 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 LayoutElementFlexChildInfo extends FlexChildInfo
{
public var layoutElement:ILayoutElement;
}
class SizesAndLimit
{
public var preferredSize:Number;
public var minSize:Number;
}