blob: 374d50d18f534e0584acb5adf9035c342ccb0b0d [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 mx.containers
{
import mx.containers.gridClasses.GridColumnInfo;
import mx.containers.gridClasses.GridRowInfo;
import mx.core.EdgeMetrics;
//--------------------------------------
// Styles
//--------------------------------------
/**
* Number of pixels between children in the horizontal direction.
* The default value is 8.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="horizontalGap", type="Number", format="Length", inherit="no")]
/**
* Number of pixels between children in the vertical direction.
* The default value is 6.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="verticalGap", type="Number", format="Length", inherit="no")]
//--------------------------------------
// Excluded APIs
//--------------------------------------
[Exclude(name="direction", kind="property")]
[Exclude(name="focusIn", kind="event")]
[Exclude(name="focusOut", kind="event")]
[Exclude(name="focusBlendMode", kind="style")]
[Exclude(name="focusSkin", kind="style")]
[Exclude(name="focusThickness", kind="style")]
[Exclude(name="focusInEffect", kind="effect")]
[Exclude(name="focusOutEffect", kind="effect")]
[IconFile("Grid.png")]
/**
* A Grid container lets you arrange children as rows and columns
* of cells, similar to an HTML table.
* The Grid container contains one or more rows, and each row can contain
* one or more cells, or items. You use the following tags to define a Grid control:
*
* <ul>
* <li>The <code>&lt;mx:Grid&gt;</code> tag defines a Grid container.</li>
*
* <li>The <code>&lt;mx:GridRow&gt;</code> tag defines a grid row,
* which has one or more cells. The grid row must be a child of the
* <code>&lt;Grid&gt;</code> tag.</li>
*
* <li>The <code>&lt;mx:GridItem&gt;</code> tag defines a grid cell,
* and must be a child of the <code>&lt;GridRow&gt;</code> tag.
* The <code>&lt;mx:GridItem&gt;</code> tag can contain
* any number of children.</li>
* </ul>
*
* <p>The height of all the cells in a single row is the same,
* but each row can have a different height.
* The width of all cells in a single column is the same,
* but each column can have a different width.
* You can define a different number of cells
* for each row or each column of the Grid container.
* In addition, a cell can span multiple columns
* or multiple rows of the container.</p>
*
* <p>The Grid, GridRow, and GridItem containers have the following default sizing characteristics:</p>
* <table class="innertable">
* <tr>
* <th>Characteristic</th>
* <th>Description</th>
* </tr>
* <tr>
* <td>Grid height</td>
* <td>The sum of the default or explicit heights of all rows plus the gaps between rows.</td>
* </tr>
* <tr>
* <td>Grid width</td>
* <td>The sum of the default or explicit width of all columns plus the gaps between columns.</td>
* </tr>
* <tr>
* <td>Height of each row and each cell</td>
* <td>The default or explicit height of the tallest item in the row. If a GridItem container does not
* have an explicit size, its default height is the default or explicit height of the child in the cell.</td>
* </tr>
* <tr>
* <td>Width of each column and each cell</td>
* <td>The default or explicit width of the widest item in the column. If a GridItem container does not have an explicit
* width, its default width is the default or explicit width of the child in the cell.</td>
* </tr>
* <tr>
* <td>Gap between rows and columns</td>
* <td>Determined by the horizontalGap and verticalGap styles of the Grid class. The default value for both
* gaps is 6 pixels.</td>
* </tr>
* <tr>
* <td>Default padding</td>
* <td>0 pixels for the top, bottom, left, and right values, for all three container classes.</td>
* </tr>
* </table>
*
* @mxml
*
* <p>The <code>&lt;mx:Grid&gt;</code> tag inherits all of the tag
* attributes of its superclass, except the <code>Box.direction</code>
* property, and adds the following tag attributes:</p>
*
* <pre>
* &lt;mx:Grid
* <strong>Styles</strong>
* horizontalGap="8"
* verticalGap="6"
* &gt;
* ...
* <i>These child tags are examples only:</i>
* &lt;mx:GridRow id="row1"&gt;
* &lt;mx:GridItem
* rowSpan="1"
* colSpan="1">
* &lt;mx:Button label="Button 1"/&gt;
* &lt;/mx:GridItem&gt;
* ...
* &lt;/mx:GridRow&gt;
* ...
* &lt;/mx:Grid&gt;
* </pre>
*
* @includeExample examples/GridLayoutExample.mxml
*
* @see mx.containers.GridRow
* @see mx.containers.GridItem
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public class Grid extends Box
{
include "../core/Version.as";
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function Grid()
{
super();
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
* Minimum, maximum, and preferred width of each column.
*/
private var columnWidths:Array /* of GridColumnInfo */;
/**
* @private
* Minimum, maximum, and preferred height of each row.
*/
private var rowHeights:Array /* of GridRowInfo */;
/**
* @private
*/
private var needToRemeasure:Boolean = true;
//--------------------------------------------------------------------------
//
// Overridden methods:UIComponent
//
//--------------------------------------------------------------------------
/**
* @private
*/
override public function invalidateSize():void
{
// When the Grid's size is invalidated, the rowHeights
// and columnWidths arrays need to be recalculated.
// Those arrays are usually recalculated in measure(),
// but set a flag in case measure() isn't called (which
// will happen if the Grid's width and height are explicit).
if (!isNaN(explicitWidth) && !isNaN(explicitHeight))
needToRemeasure = true;
super.invalidateSize();
}
/**
* Calculates the preferred, minimum, and maximum sizes of the Grid.
* <p>You should not call this method directly; it is an advanced
* method for use in subclassing.
* The Flex LayoutManger calls the <code>measure()</code> method
* at the appropriate time.
* At application startup, the Flex LayoutManager attempts
* to measure all components from the children to the parents
* before setting them to their final sizes.</p>
*
* <p>To understand how the Grid container calculates its measurements,
* assume that every GridItem container has its <code>rowSpan</code>
* property and <code>colSpan</code> property set to 1.
* The measured width of the first column of the Grid container
* is equal to the maximum among of the measured widths
* of all GridItem containers in the first column.
* Similarly, the measured width of the second column is
* the maximum of all measured widths among the GridItem containers
* in the second column, and so on.
* The <code>measuredWidth</code> of the entire Grid container
* is the sum of all columns' measured widths, plus the thickness
* of the border, plus the left and right padding, plus room
* for the horizontal gap between adjacent grid cells.</p>
*
* <p>The <code>measuredHeight</code>, <code>minWidth</code>,
* <code>minHeight</code>, <code>maxWidth</code>, and
* <code>maxHeight</code> properties' values are all calculated
* in a similar manner, by adding together the values of the
* GridItem containers' <code>measuredHeight</code> properties,
* <code>minWidth</code> properties, and so on.</p>
*
* <p>If a GridItem container's <code>colSpan</code> property is 3,
* that GridItem container's <code>measuredWidth</code> is divided
* among 3 columns.
* If the <code>measuredWidth</code> is divided equally,
* each of the three columns calculates its measured width
* as if the GridItem container were only in that column
* and the GridItem container's <code>measuredWidth</code>
* were one-third of its actual value.</p>
*
* <p>However, the GridItem container's <code>measuredWidth</code>
* property is not always divided equally among all the columns it spans.
* If some of the columns have a property with a percentage value
* of <code>width</code>, the GridItem container's
* <code>measuredWidth</code> property is divided accordingly,
* attempting to give each column the requested percentage
* of the Grid container.</p>
*
* <p>All of the values described previously are the
* <i>measured</i> widths and heights of Grid.
* The user can override the measured values by explicitly
* supplying a value for the following properties:</p>
*
* <ul>
* <li><code>minHeight</code></li>
* <li><code>minWidth</code></li>
* <li><code>maxHeight</code></li>
* <li><code>maxWidth</code></li>
* <li><code>height</code></li>
* <li><code>width</code></li>
* </ul>
*
* <p>If you override this method, your implementation must call the
* <code>super.measure()</code> method or set the
* <code>measuredHeight</code> and <code>measuredWidth</code> properties.
* You may also optionally set the following properties:</p>
*
* <ul>
* <li><code>measuredMinWidth</code></li>
* <li><code>measuredMinHeight</code></li>
* </ul>
*
* <p>These properties correspond to the layout properties listed previously
* and, therefore, are not documented separately.</p>
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
override protected function measure():void
{
// 1. Determine the number of grid columns,
// taking into account rowSpan and colSpan
var numGridRows:int = 0;
var numGridColumns:int = 0;
var columnOccupiedUntilRow:Array = [];
var gridRow:GridRow;
var gridItem:GridItem;
var i:int;
var j:int;
var colIndex:int;
var rowList:Array = []; // GridRows
for (var index:int = 0; index < numChildren; index++)
{
gridRow = GridRow(getChildAt(index));
if (gridRow.includeInLayout)
{
rowList.push(gridRow);
numGridRows++;
}
}
for (i = 0; i < numGridRows; i++)
{
colIndex = 0;
gridRow = rowList[i];
// Cache the number of children as a property on the gridRow
gridRow.numGridItems = gridRow.numChildren;
// Tell the grid row what row number it is
gridRow.rowIndex = i;
for (j = 0; j < gridRow.numGridItems; j++)
{
// If this column is occupied by a cell in the previous row,
// then push cells in this row to the right.
if (i > 0)
{
var occupied:int = columnOccupiedUntilRow[colIndex];
while (!isNaN(occupied) && occupied >= i)
{
colIndex++;
occupied = columnOccupiedUntilRow[colIndex];
}
}
// Set the position of this GridItem to the next
// available space.
gridItem = GridItem(gridRow.getChildAt(j));
gridItem.colIndex = colIndex;
// If this cell extends to rows beyond this one, remember
// which columns are occupied.
if (gridItem.rowSpan > 1)
{
var lastRowOccupied:int = i + gridItem.rowSpan - 1;
for (var k:int = 0; k < gridItem.colSpan; k++)
{
columnOccupiedUntilRow[colIndex + k] = lastRowOccupied;
}
}
colIndex += gridItem.colSpan;
}
if (colIndex > numGridColumns)
numGridColumns = colIndex;
}
// 2. Create the rowHeights and colWidths arrays.
// Initially set all heights and widths to zero.
rowHeights = new Array(numGridRows);
columnWidths = new Array(numGridColumns);
for (i = 0; i < numGridRows; i++)
{
rowHeights[i] = new GridRowInfo();
}
for (i = 0; i < numGridColumns; i++)
{
columnWidths[i] = new GridColumnInfo();
}
// 3. Visit all the GridItems again.
// Expand each row and each column so it's large enough
// to hold its GridItems.
// We'll deal with rowSpans and colSpans of 1 first,
// then rowSpans and colSpans of 2, and so on.
var BIG_INT:int = int.MAX_VALUE;
var curSpan:int;
var nextSpan:int = 1;
var horizontalGap:Number = getStyle("horizontalGap");
var verticalGap:Number = getStyle("verticalGap");
do
{
curSpan = nextSpan;
nextSpan = BIG_INT;
for (i = 0; i < numGridRows; i++)
{
gridRow = rowList[i];
// Store pointers to the columnWidths and rowHeights arrays
// on each GridRow object.
gridRow.columnWidths = columnWidths;
gridRow.rowHeights = rowHeights;
for (j = 0; j < gridRow.numGridItems; j++)
{
gridItem = GridItem(gridRow.getChildAt(j));
var rowSpan:int = gridItem.rowSpan;
var colSpan:int = gridItem.colSpan;
// During this iteration of the outermost do-while loop,
// we're dealing with rows and columns that have a rowSpan
// or colSpan equal to curSpan. If we encounter a row or
// column with a larger span, remember its span in nextSpan
// for the next iteration through the do-while loop.
if (rowSpan == curSpan)
distributeItemHeight(gridItem, i, verticalGap, rowHeights);
else if (rowSpan > curSpan && rowSpan < nextSpan)
nextSpan = rowSpan;
if (colSpan == curSpan)
{
distributeItemWidth(gridItem, gridItem.colIndex,
horizontalGap, columnWidths);
}
else if (colSpan > curSpan && colSpan < nextSpan)
{
nextSpan = colSpan;
}
}
}
}
while (nextSpan < BIG_INT);
// 4. Reconcile min/preferred/max values, so that min <= pref <= max.
// Also compute sums of all measurements.
var minWidth:Number = 0;
var minHeight:Number = 0;
var preferredWidth:Number = 0;
var preferredHeight:Number = 0;
for (i = 0; i < numGridRows; i++)
{
var rowInfo:GridRowInfo = rowHeights[i];
if (rowInfo.min > rowInfo.preferred)
rowInfo.min = rowInfo.preferred;
if (rowInfo.max < rowInfo.preferred)
rowInfo.max = rowInfo.preferred;
minHeight += rowInfo.min;
preferredHeight += rowInfo.preferred;
}
for (i = 0; i < numGridColumns; i++)
{
var columnInfo:GridColumnInfo = columnWidths[i];
if (columnInfo.min > columnInfo.preferred)
columnInfo.min = columnInfo.preferred;
if (columnInfo.max < columnInfo.preferred)
columnInfo.max = columnInfo.preferred;
minWidth += columnInfo.min;
preferredWidth += columnInfo.preferred;
}
// 5. Add horizontal space for the gaps between the grid cells
// and the margins around them.
// Add space for grid's left and right margins
var vm:EdgeMetrics = viewMetricsAndPadding;
var padding:Number = vm.left + vm.right;
var row:GridRow;
var rowVm:EdgeMetrics;
var maxRowPadding:Number = 0;
// Add space for horizontal gaps between grid items.
if (numGridColumns > 1)
padding += getStyle("horizontalGap") * (numGridColumns - 1);
// Add space for the gridrow's left and right margins.
for (i = 0; i < numGridRows; i++)
{
row = rowList[i];
rowVm = row.viewMetricsAndPadding;
var rowPadding:Number = rowVm.left + rowVm.right;
if (rowPadding > maxRowPadding)
maxRowPadding = rowPadding;
}
padding += maxRowPadding;
minWidth += padding;
preferredWidth += padding;
// 6. Add vertical space for the gaps between grid cells
// and the margins around them.
// Add space for grid's left and right margins.
padding = vm.top + vm.bottom;
// Add space for vertical gaps between grid items.
if (numGridRows > 1)
padding += getStyle("verticalGap") * (numGridRows - 1);
// Add each of the grid rows' margins.
for (i = 0; i < numGridRows; i++)
{
row = rowList[i];
rowVm = row.viewMetricsAndPadding;
padding += rowVm.top + rowVm.bottom;
}
minHeight += padding;
preferredHeight += padding;
// 7. Now that the Grid is finished measuring itself,
// update all the measurements of the child GridRows.
for (i = 0; i < numGridRows; i++)
{
row = rowList[i];
row.updateRowMeasurements();
}
// 8. Up until now, we've calculated all our measurements
// based on the GridItems.
// If someone has explicitly set the width (or percentWidth
// or whatever) of a GridRow, we've been ignoring it.
// Run the standard Box measurement algorithm, which will
// take into account hard-coded values on the GridRow objects,
// and combine the measured values with the ones
// we've calculated based on GridItems.
super.measure();
measuredMinWidth = Math.max(measuredMinWidth, minWidth);
measuredMinHeight = Math.max(measuredMinHeight, minHeight);
measuredWidth = Math.max(measuredWidth, preferredWidth);
measuredHeight = Math.max(measuredHeight, preferredHeight);
needToRemeasure = false;
}
/**
* Sets the size and position of each child of the Grid.
*
* <p>You should not call this method directly; it is an advanced
* method for use in subclassing.
* The Flex LayoutManager calls the <code>updateDisplayList</code>
* method at the appropriate time.
* At application startup, the Flex LayoutManager calls
* the <code>updateDisplayList()</code> method on every component,
* starting with the root and working downward.</p>
*
* <p>The Grid container follows the same layout rules
* as the VBox container.
* The positions and sizes of the GridRow containers
* are calculated the same way that a VBox container
* determines the positions and sizes of its children.
* Similarly, a GridRow container positions its GridItem containers
* using a similar layout algorithm of an HBox container.</p>
*
* <p>The only difference is that the GridRow containers
* all coordinate with one another, so they all choose
* the same positions and sizes for their children
* (so that the columns of the Grid container align). </p>
*
* <p>If you override this method, your implementation should call
* the <code>super.updateDisplayList()</code> method
* and call the <code>move()</code> and the <code>setActualSize()</code>
* methods on each of the children.
* For the purposes of performing layout, you should get the size
* of this container from the <code>unscaledWidth</code>
* and <code>unscaledHeight</code> properties, not the
* <code>width</code> and <code>height</code> properties.
* The <code>width</code> and <code>height</code> properties do not
* take into account the values of the <code>scaleX</code>
* and <code>scaleY</code> properties for this container.</p>
*
* @param unscaledWidth Specifies the width of the component, in pixels,
* in the component's coordinates, regardless of the value of the
* <code>scaleX</code> property of the component.
*
* @param unscaledHeight Specifies the height of the component, in pixels,
* in the component's coordinates, regardless of the value of the
* <code>scaleY</code> property of the component.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
override protected function updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number):void
{
// If the measure function wasn't called (because the widths
// and heights of the Grid are explicitly set), then we need
// to generate the rowHeights and columnWidths arrays now.
if (needToRemeasure)
measure();
// Follow standard VBox rules for laying out the child rows.
super.updateDisplayList(unscaledWidth, unscaledHeight);
var n:int = 0;
var i:int;
var child:GridRow;
var rowList:Array = []; // GridRows
for (var index:int = 0; index < numChildren; index++)
{
child = GridRow(getChildAt(index));
if (child.includeInLayout)
{
rowList.push(child);
n++;
}
}
// Copy the row heights, which were calculated by the VBox layout
// algorithm, into the rowHeights array.
for (i = 0; i < n; i++)
{
child = rowList[i];
rowHeights[i].y = child.y;
rowHeights[i].height = child.height;
}
// Now that all the rows have been layed out, lay out the GridItems
// in those rows, based on the info stored in the colWidths and
// rowHeights arrays.
for (i = 0; i < n; i++)
{
child = rowList[i];
child.doRowLayout(child.width * child.scaleX,
child.height * child.scaleY);
}
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
private function distributeItemHeight(item:GridItem, rowIndex:Number,
verticalGap:Number,
rowHeights:Array):void
{
var maxHeight:Number = item.maxHeight;
var preferredHeightToDistribute:Number =
item.getExplicitOrMeasuredHeight();
var minHeightToDistribute:Number = item.minHeight;
var rowSpan:int = item.rowSpan;
var totalFlex:Number = 0;
var divideEqually:Boolean = false;
var i:int;
var rowInfo:GridRowInfo;
// If the row(s) spanned by this GridItem are already non-empty,
// subtract the existing sizes of those rows from the item's height.
for (i = rowIndex; i < rowIndex + rowSpan; i++)
{
rowInfo = rowHeights[i];
preferredHeightToDistribute -= rowInfo.preferred;
minHeightToDistribute -= rowInfo.min;
totalFlex += rowInfo.flex;
}
// Subtract space for gaps between the rows.
if (rowSpan > 1)
{
var gap:Number = verticalGap * (rowSpan - 1);
preferredHeightToDistribute -= gap;
minHeightToDistribute -= gap;
}
// If none of the rows spanned by this item are resizable,
// then divide space among the rows equally.
if (totalFlex == 0)
{
totalFlex = rowSpan;
divideEqually = true;
}
// If we haven't yet distributed the height of the object,
// divide remaining height among the rows.
// If some rows are resizable and others are not,
// allocate space to the resizable ones.
preferredHeightToDistribute =
preferredHeightToDistribute > 0 ?
Math.ceil(preferredHeightToDistribute / totalFlex) :
0;
minHeightToDistribute =
minHeightToDistribute > 0 ?
Math.ceil(minHeightToDistribute / totalFlex) :
0;
for (i = rowIndex; i < rowIndex + rowSpan; i++)
{
rowInfo = rowHeights[i];
var flex:Number = divideEqually ? 1 : rowInfo.flex;
rowInfo.preferred += preferredHeightToDistribute * flex;
rowInfo.min += minHeightToDistribute * flex;
}
// The GridItem.maxHeight attribute is respected only for rows
// with a rowSpan of 1.
if (rowSpan == 1 && maxHeight < rowInfo.max)
rowInfo.max = maxHeight;
}
/**
* @private
*/
private function distributeItemWidth(item:GridItem, colIndex:int,
horizontalGap:Number, columnWidths:Array):void
{
var maxWidth:Number = item.maxWidth;
var preferredWidthToDistribute:Number =
item.getExplicitOrMeasuredWidth();
var minWidthToDistribute:Number = item.minWidth;
var colSpan:int = item.colSpan;
var percentWidth:Number = item.percentWidth;
var totalFlex:Number = 0;
var divideEqually:Boolean = false;
var i:int;
var columnInfo:GridColumnInfo;
// If the column(s) spanned by this GridItem are already non-empty,
// subtract the existing sizes of those columns from the item's width.
for (i = colIndex; i < colIndex + colSpan; i++)
{
columnInfo = columnWidths[i];
preferredWidthToDistribute -= columnInfo.preferred;
minWidthToDistribute -= columnInfo.min;
totalFlex += columnInfo.flex;
}
// Subtract space for gaps between the columns.
if (colSpan > 1)
{
var gap:Number = horizontalGap * (colSpan - 1);
preferredWidthToDistribute -= gap;
minWidthToDistribute -= gap;
}
// If none of the columns spanned by this item are resizable,
// then divide space among the columns equally.
if (totalFlex == 0)
{
totalFlex = colSpan;
divideEqually = true;
}
// If we haven't yet distributed the width of the object,
// divide remaining width among the columns.
// If some columns are resizable and others are not,
// allocate space to the resizable ones.
preferredWidthToDistribute =
preferredWidthToDistribute > 0 ?
Math.ceil(preferredWidthToDistribute / totalFlex) :
0;
minWidthToDistribute =
minWidthToDistribute > 0 ?
Math.ceil(minWidthToDistribute / totalFlex) :
0;
for (i = colIndex; i < colIndex + colSpan; i++)
{
columnInfo = columnWidths[i];
var flex:Number = divideEqually ? 1 : columnInfo.flex;
columnInfo.preferred += preferredWidthToDistribute * flex;
columnInfo.min += minWidthToDistribute * flex;
if (percentWidth)
{
columnInfo.percent = Math.max(columnInfo.percent,
percentWidth / colSpan);
}
}
// The GridItem.maxWidth attribute is respected only for columns
// with a colSpan of 1.
if (colSpan == 1 && maxWidth < columnInfo.max)
columnInfo.max = maxWidth;
}
}
}