//////////////////////////////////////////////////////////////////////////////// // // 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.geom.Point; import flash.utils.Dictionary; import mx.containers.errors.ConstraintError; import mx.containers.utilityClasses.ConstraintColumn; import mx.containers.utilityClasses.ConstraintRow; import mx.containers.utilityClasses.Flex; import mx.core.ILayoutElement; import mx.core.mx_internal; import mx.resources.ResourceManager; import spark.components.supportClasses.GroupBase; import spark.layouts.supportClasses.LayoutBase; import spark.layouts.supportClasses.LayoutElementHelper; use namespace mx_internal; [ResourceBundle("layout")] /** * The ConstraintLayout class arranges the layout elements based on their individual * settings and a set of constraint regions defined by constraint columns and * constraint rows. Although you can use all of the properties and constraints from * BasicLayout to position and size elements, ConstraintLayout gives you the ability * to create sibling-relative layouts by constraining elements to the specified * columns and rows. * *

Note: The Spark list-based controls (the Spark List control and its subclasses * such as ButtonBar, ComboBox, DropDownList, and TabBar) do not support the ConstraintLayout class. * Do not use ConstraintLayout with the Spark list-based controls.

* *

Per-element supported constraints are left, right, * top, bottom, baseline, * percentWidth, and percentHeight. * Element's minimum and maximum sizes will always be respected.

* *

Columns and rows may have an explicit size or content size (no explicit size). * Explicit size regions will be fixed at their specified size, while content size * regions will stretch to fit only the elements constrained to them. If multiple * content size regions are spanned by an element, the space will be divided * equally among the content size regions.

* *

The measured size of the container is calculated from the elements, their * constraints, their preferred sizes, and the sizes of the rows and columns. * The size of each row and column is just big enough to hold all of the elements * constrained to it at their preferred sizes with constraints satisfied. The measured * size of the container is big enough to hold all of the columns and rows as well as * any other elements left at their preferred sizes with constraints satisfied.

* *

During a call to the updateDisplayList() method, * the element's size is determined according to * the rules in the following order of precedence (the element's minimum and * maximum sizes are always respected):

* * *

The element's position is determined according to the rules in the following * order of precedence:

* * *

The content size of the container is calculated as the maximum of the * coordinates of the bottom-right corner of all the layout elements and * constraint regions.

* * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4.5 */ public class ConstraintLayout extends LayoutBase { //-------------------------------------------------------------------------- // // Class methods // //-------------------------------------------------------------------------- /** * @private * * @return true if the constraints determine the element's width; */ private static function constraintsDetermineWidth(elementInfo:ElementConstraintInfo):Boolean { return !isNaN(elementInfo.left) && !isNaN(elementInfo.right); } /** * @private * * @return true if the constraints determine the element's height; */ private static function constraintsDetermineHeight(elementInfo:ElementConstraintInfo):Boolean { return !isNaN(elementInfo.top) && !isNaN(elementInfo.bottom); } /** * @private * @return Returns the maximum value for an element's dimension so that the component doesn't * spill out of the container size. Calculations are based on the layout rules. * Pass in unscaledWidth, left, right, childX to get a maxWidth value. * Pass in unscaledHeight, top, bottom, childY to get a maxHeight value. */ private static function maxSizeToFitIn(totalSize:Number, lowConstraint:Number, highConstraint:Number, position:Number):Number { if (!isNaN(lowConstraint)) { // childWidth + left <= totalSize return totalSize - lowConstraint; } else if (!isNaN(highConstraint)) { // childWidth + right <= totalSize return totalSize - highConstraint; } else { // childWidth + childX <= totalSize return totalSize - position; } } //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4.5 */ public function ConstraintLayout() { super(); } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * @private * Vectors that keep track of children spanning * content size columns or rows or whether the * elements don't use columns or rows at all. */ private var colSpanElements:Vector. = null; private var rowSpanElements:Vector. = null; private var otherElements:Vector. = null; /** * @private * Vectors to store the baseline property of the rows, and * the maximum ascent of the elements in each row. * * In rowBaselines, the value is stored as * [value, maxAscent] if the baseline is maxAscent:value, * and [value, null] if the baseline is just a value. */ private var rowBaselines:Vector. = null; private var rowMaxAscents:Vector. = null; /** * @private * Hashtable that maps elements to their constraint * information. The mapping has type: * ILayoutElement -> ElementConstraintInfo * * This cache is always discarded after measure() or * updateDisplayList() because the constraints may have changed. */ private var constraintCache:Dictionary = null; //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // constraintColumns //---------------------------------- private var _constraintColumns:Vector. = new Vector.(0, true); // An associative array of column id --> column index private var columnsObject:Object = new Object(); /** * A Vector of ConstraintColumn instances that partition the target container. * The ConstraintColumn instance at index 0 is the left-most column; * indices increase from left to right. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4.5 */ public function get constraintColumns():Vector. { // make defensive copy return _constraintColumns.slice(); } /** * @private */ public function set constraintColumns(value:Vector.):void { // clear constraintColumns if (value == null) { _constraintColumns = new Vector.(0, true); columnsObject = new Object(); return; } var n:int = value.length; var col:ConstraintColumn; var temp:Vector. = value.slice(); var obj:Object = new Object(); for (var i:int = 0; i < n; i++) { col = temp[i]; col.container = this.target; obj[col.id] = i; } _constraintColumns = temp; columnsObject = obj; if (target) { target.invalidateSize(); target.invalidateDisplayList(); } } //---------------------------------- // constraintRows //---------------------------------- private var _constraintRows:Vector. = new Vector.(0, true); // An associative array of row id --> row index private var rowsObject:Object = new Object(); /** * A Vector of ConstraintRow instances that partition the target container. * The ConstraintRow instance at index 0 is the top-most column; * indices increase from top to bottom. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4.5 */ public function get constraintRows():Vector. { return _constraintRows.slice(); } /** * @private */ public function set constraintRows(value:Vector.):void { // clear constraintRows if (value == null) { _constraintRows = new Vector.(0, true); rowsObject = new Object(); return; } var n:int = value.length; var row:ConstraintRow; var temp:Vector. = value.slice(); var obj:Object = new Object(); rowBaselines = new Vector.(); for (var i:int = 0; i < n; i++) { row = temp[i]; row.container = this.target; obj[row.id] = i; rowBaselines[i] = LayoutElementHelper.parseConstraintExp(row.baseline); var maxAscentStr:String = rowBaselines[i][1]; if (maxAscentStr && maxAscentStr != "maxAscent") throw new Error(ResourceManager.getInstance().getString("layout", "invalidBaselineOnRow", [ row.id, row.baseline ])); } _constraintRows = temp; rowsObject = obj; if (target) { target.invalidateSize(); target.invalidateDisplayList(); } } /** * @private * Resets the target on the constraintColumns and constraintRows. */ override public function set target(value:GroupBase):void { super.target = value; // setting a new target means we need to reset the targets of // our columns and rows var i:int; var n:int = _constraintColumns.length; for (i = 0; i < n; i++) { _constraintColumns[i].container = value; } n = _constraintRows.length; for (i = 0; i < n; i++) { _constraintRows[i].container = value; } } //-------------------------------------------------------------------------- // // Overridden Methods: LayoutBase // //-------------------------------------------------------------------------- /** * @private * * 1) Parse each element constraint and populate the constraintCache * 2) Measure the columns and rows based on only the elements that use them * and get the sum of the column widths and row heights. * 3) Measure the size of this container based on elements that don't use * either columns or rows or both. * 4) Take the max of 2 and 3 to find the measuredWidth. */ override public function measure():void { checkUseVirtualLayout(); super.measure(); var layoutTarget:GroupBase = target; if (!layoutTarget) return; var width:Number = 0; var height:Number = 0; var minWidth:Number = 0; var minHeight:Number = 0; parseConstraints(); // Find preferred column widths and row heights. var colWidths:Vector. = measureColumns(); var rowHeights:Vector. = measureRows(); var n:Number; for each (n in colWidths) { width += n; } for each (n in rowHeights) { height += n; } // Find minimum measured width/height by passing in 0 for the constrained size. // This means that percent size regions will be set to their min size. constrainPercentRegionSizes(colWidths, 0, true); for each (n in colWidths) { minWidth += n; } constrainPercentRegionSizes(rowHeights, 0, false); for each (n in rowHeights) { minHeight += n; } if (otherElements) { var vec:Vector. = measureOtherContent(); width = Math.max(width, vec[0]); height = Math.max(height, vec[1]); minWidth = Math.max(minWidth, vec[2]); minHeight = Math.max(minHeight, vec[3]); } layoutTarget.measuredWidth = Math.ceil(width); layoutTarget.measuredHeight = Math.ceil(height); layoutTarget.measuredMinWidth = Math.ceil(minWidth); layoutTarget.measuredMinHeight = Math.ceil(minHeight); // clear out cache clearConstraintCache(); } /** * @private * * 1) Re-parse element constraints because they may have changed. * 2) Resize and reposition the columns and rows based on new constraints. * 3) Size and position the elements in the available space. */ override public function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { checkUseVirtualLayout(); super.updateDisplayList(unscaledWidth, unscaledHeight); var layoutTarget:GroupBase = target; if (!layoutTarget) return; // Need to measure in case of explicit width and height on target. // Also need to reparse constraints in case of something changing. measureAndPositionColumnsAndRows(unscaledWidth, unscaledHeight); layoutContent(unscaledWidth, unscaledHeight); } //-------------------------------------------------------------------------- // // Methods: Used by FormItemLayout // //-------------------------------------------------------------------------- /** * Lays out the elements of the layout target using the current * widths and heights of the columns and rows. Used by FormItemLayout * after setting new column widths to lay elements using those new widths. * * @param unscaledWidth Specifies the width of the component, in pixels, * in the component's coordinates, regardless of the value of the * scaleX 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 * scaleY property of the component. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4.5 */ protected function layoutContent(unscaledWidth:Number, unscaledHeight:Number):void { var layoutTarget:GroupBase = target; if (!layoutTarget) return; var count:int = layoutTarget.numElements; var layoutElement:ILayoutElement; var maxX:Number = 0; var maxY:Number = 0; // update children for (var i:int = 0; i < count; i++) { layoutElement = layoutTarget.getElementAt(i); if (!layoutElement || !layoutElement.includeInLayout) continue; applyConstraintsToElement(unscaledWidth, unscaledHeight, layoutElement); // update content limits maxX = Math.max(maxX, layoutElement.getLayoutBoundsX() + layoutElement.getLayoutBoundsWidth()); maxY = Math.max(maxY, layoutElement.getLayoutBoundsY() + layoutElement.getLayoutBoundsHeight()); } // 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), Math.ceil(maxY)); // clear out cache clearConstraintCache(); } /** * Used by FormItemLayout to measure and set new column widths * and row heights before laying out the elements. * * @param constrainedWidth The total width available for columns to stretch * or shrink their percent width columns. If NaN, percent width columns * are unconstrained and fit to their content. * @param constrainedHeight The total height available for rows to stretch * or shrink their percent height rows. If NaN, percent height rows * are unconstrained and fit to their content. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4.5 */ protected function measureAndPositionColumnsAndRows(constrainedWidth:Number = NaN, constrainedHeight:Number = NaN):void { parseConstraints(); setColumnWidths(measureColumns(constrainedWidth)); setRowHeights(measureRows(constrainedHeight)); } /** * @private * This function is mx_internal so that FormItemLayout can use it * in its updateDisplayList. */ mx_internal function checkUseVirtualLayout():void { if (useVirtualLayout) throw new Error(ResourceManager.getInstance().getString("layout", "constraintLayoutNotVirtualized")); } /** * @private */ override mx_internal function get virtualLayoutSupported():Boolean { return false } /** * @private * Used to set new column widths before laying out the elements. * Used by FormItemLayout to set column widths provided by the * Form. */ mx_internal function setColumnWidths(value:Vector.):void { if (value == null) return; var constraintColumns:Vector. = this.constraintColumns; var numCols:int = constraintColumns.length; var totalWidth:Number = 0; for (var i:int = 0; i < numCols; i++) { constraintColumns[i].setActualWidth(value[i]); constraintColumns[i].x = totalWidth; totalWidth += value[i]; } } /** * @private * Used to set new row heights before laying out the elements. */ mx_internal function setRowHeights(value:Vector.):void { if (value == null) return; var constraintRows:Vector. = this.constraintRows; var numRows:int = constraintRows.length; var totalHeight:Number = 0; for (var i:int = 0; i < numRows; i++) { constraintRows[i].setActualHeight(value[i]); constraintRows[i].y = totalHeight; totalHeight += value[i]; } } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * @private * Sizes and positions the element based on the given size of the container * and the element's constraints. * * 1) Retrieves element constraints from the constraint cache. * 2) Determines the x and y boundaries of each side. * 3) Sizes the element based on constraints and its preferred * size. The precedence for sizing is as follows: percent, * top and bottom constraints, preferred size. * 4) Positions the element based on its constraints. The precedence * for positioning is as follows: baseline, left and top, right and bottom, * x and y. */ private function applyConstraintsToElement(unscaledWidth:Number, unscaledHeight:Number, layoutElement:ILayoutElement):void { var elementInfo:ElementConstraintInfo = constraintCache[layoutElement]; var left:Number = elementInfo.left; var right:Number = elementInfo.right; var top:Number = elementInfo.top; var bottom:Number = elementInfo.bottom; var baseline:Number = elementInfo.baseline; var leftBoundary:String = elementInfo.leftBoundary; var rightBoundary:String = elementInfo.rightBoundary; var topBoundary:String = elementInfo.topBoundary; var bottomBoundary:String = elementInfo.bottomBoundary; var baselineBoundary:String = elementInfo.baselineBoundary; var percentWidth:Number = layoutElement.percentWidth; var percentHeight:Number = layoutElement.percentHeight; var availableWidth:Number; var availableHeight:Number; var elementWidth:Number = NaN; var elementHeight:Number = NaN; var elementMaxWidth:Number = NaN; var elementMaxHeight:Number = NaN; var elementX:Number = 0; var elementY:Number = 0; var leftHolder:Number = 0; var rightHolder:Number = unscaledWidth; var topHolder:Number = 0; var bottomHolder:Number = unscaledHeight; var baselineHolder:Number = 0; var i:Number; var col:ConstraintColumn; var row:ConstraintRow; if (leftBoundary) { col = _constraintColumns[elementInfo.colSpanLeftIndex]; leftHolder = col.x; } if (rightBoundary) { col = _constraintColumns[elementInfo.colSpanRightIndex]; rightHolder = col.x + col.width; } if (topBoundary) { row = _constraintRows[elementInfo.rowSpanTopIndex]; topHolder = row.y; } if (bottomBoundary) { row = _constraintRows[elementInfo.rowSpanBottomIndex]; bottomHolder = row.y + row.height; } if (baselineBoundary) { var baselineIndex:Number = elementInfo.baselineIndex; var rowBaseline:Array = rowBaselines[baselineIndex]; row = _constraintRows[baselineIndex]; // add baseline offset from row. baselineHolder = row.y + Number(rowBaseline[0]); // add maxAscent. maxAscent defaults to 0 if not specified. if (rowMaxAscents) baselineHolder += rowMaxAscents[baselineIndex]; // If bottom doesn't exist, then the bottom should be restricted to the // baseline row. if (isNaN(bottom)) bottomHolder = row.y + row.height; } // available width availableWidth = Math.round(rightHolder - leftHolder); // cases are baseline with top and bottom, // baseline with top, baseline with bottom, no baseline if (!isNaN(baseline) && (isNaN(top) || isNaN(bottom))) availableHeight = Math.round(bottomHolder - baselineHolder); else availableHeight = Math.round(bottomHolder - topHolder); // set width if (!isNaN(percentWidth)) { if (!isNaN(left)) availableWidth -= left; if (!isNaN(right)) availableWidth -= right; elementWidth = Math.round(availableWidth * Math.min(percentWidth * 0.01, 1)); elementMaxWidth = Math.min(layoutElement.getMaxBoundsWidth(), maxSizeToFitIn(unscaledWidth, left, right, layoutElement.getLayoutBoundsX())); } else if (!isNaN(left) && !isNaN(right)) { elementWidth = availableWidth - left - right; } // set height if (!isNaN(percentHeight)) { if (!isNaN(top)) availableHeight -= top; if (!isNaN(bottom)) availableHeight -= bottom; elementHeight = Math.round(availableHeight * Math.min(percentHeight * 0.01, 1)); elementMaxHeight = Math.min(layoutElement.getMaxBoundsHeight(), maxSizeToFitIn(unscaledHeight, top, bottom, layoutElement.getLayoutBoundsY())); } else if (!isNaN(top) && !isNaN(bottom)) { elementHeight = availableHeight - top - bottom; } // Apply min and max constraints, make sure min is applied last. In the cases // where elementWidth and elementHeight are NaN, setLayoutBoundsSize will use preferredSize // which is already constrained between min and max. if (!isNaN(elementWidth)) { if (isNaN(elementMaxWidth)) elementMaxWidth = layoutElement.getMaxBoundsWidth(); elementWidth = Math.max(layoutElement.getMinBoundsWidth(), Math.min(elementMaxWidth, elementWidth)); } if (!isNaN(elementHeight)) { if (isNaN(elementMaxHeight)) elementMaxHeight = layoutElement.getMaxBoundsHeight(); elementHeight = Math.max(layoutElement.getMinBoundsHeight(), Math.min(elementMaxHeight, elementHeight)); } layoutElement.setLayoutBoundsSize(elementWidth, elementHeight); // update temp variables elementWidth = layoutElement.getLayoutBoundsWidth(); elementHeight = layoutElement.getLayoutBoundsHeight(); // Horizontal Position if (!isNaN(left)) elementX = leftHolder + left; else if (!isNaN(right)) elementX = rightHolder - right - elementWidth; else elementX = layoutElement.getLayoutBoundsX(); // Vertical Position if (!isNaN(baseline)) elementY = baselineHolder + baseline - layoutElement.baselinePosition; else if (!isNaN(top)) elementY = topHolder + top; else if (!isNaN(bottom)) elementY = bottomHolder - bottom - elementHeight; else elementY = layoutElement.getLayoutBoundsY(); layoutElement.setLayoutBoundsPosition(elementX, elementY); } /** * @private * Updates the widths of content size and percent size columns that are spanned * by the specified element. This method updates the provided column widths * vector in place. The algorithm is as follows: * * 1) Determine the space needed by the element to satisfy its constraints * and be at its preferred size. * * 2) Calculate the number of columns the element will span. * * 3) If the element causes a column to expand, update the column width to match. * a) For the single spanning case, we only need to check if this element's * required width is larger than the column's current width. * b) For the multiple spanning case, we distribute the remaining width after * subtracting the fixed size columns across the content/percent size columns. * Then, we only update a column's width if the new divided width would cause * the column's width to expand. * * @param elementInfo The constraint information of the element. * @param colWidths The vector of column widths to update. */ private function updateColumnWidthsForElement(colWidths:Vector., elementInfo:ElementConstraintInfo):void { var layoutElement:ILayoutElement = elementInfo.layoutElement; var numCols:int = _constraintColumns.length; var col:ConstraintColumn; var leftIndex:int = -1; var rightIndex:int = -1; var span:int; var extX:Number = 0; var preferredWidth:Number = layoutElement.getPreferredBoundsWidth(); var maxExtent:Number; var remainingWidth:Number; var j:int; var colWidth:Number = 0; // 1) Determine how much space the element needs to satisfy its // constraints and be at its preferred width. if (!isNaN(elementInfo.left)) { extX += elementInfo.left; if (elementInfo.leftBoundary) leftIndex = elementInfo.colSpanLeftIndex; else leftIndex = 0; // constrained to parent } if (!isNaN(elementInfo.right)) { extX += elementInfo.right; if (elementInfo.rightBoundary) rightIndex = elementInfo.colSpanRightIndex; else rightIndex = numCols - 1; // constrained to parent } maxExtent = extX + preferredWidth; remainingWidth = maxExtent; // 2) If either the left or the right constraint doesn't exist, // we must find the span of the element. We do this by // determining the index of the last column that the element // occupies in the unconstrained direction. if (leftIndex < 0 || rightIndex < 0) { var isLeft:Boolean = leftIndex < 0; var startIndex:int = isLeft ? rightIndex : leftIndex; var endIndex:int = isLeft ? -1 : numCols; var increment:int = isLeft ? -1 : 1; if (isLeft) // defaults to 0 leftIndex = 0; else // defaults to numCols - 1 rightIndex = numCols - 1; for (j = startIndex; j != endIndex ; j += increment) { col = _constraintColumns[j]; // subtract fixed columns if (!isNaN(col.explicitWidth)) remainingWidth -= col.explicitWidth; if ((col.contentSize || !isNaN(col.percentWidth)) || remainingWidth < 0) { if (isLeft) leftIndex = j; else rightIndex = j; break; } } } // always 1 or positive. span = rightIndex - leftIndex + 1; // 3) If the element causes a column to expand, update the column width to match. if (span == 1) { // a) For the single spanning case, we only need to check if this element's // required width is larger than the column's current width. col = _constraintColumns[leftIndex]; if (col.contentSize || !isNaN(col.percentWidth)) { colWidth = Math.max(colWidths[leftIndex], extX + preferredWidth); if (constraintsDetermineWidth(elementInfo)) colWidth = Math.max(colWidth, extX + layoutElement.getMinBoundsWidth()); // bound with max width of column if (!isNaN(col.maxWidth)) colWidth = Math.min(colWidth, col.maxWidth); colWidths[leftIndex] = Math.ceil(colWidth); } } else { // b) multiple spanning case when span >= 2. // 1) start from leftIndex and subtract fixed columns. // 2) divide space evenly into content/percent size columns. var contentCols:Vector. = new Vector.(); var contentColsIndices:Vector. = new Vector.(); remainingWidth = maxExtent; for (j = leftIndex; j <= rightIndex; j++) { col = _constraintColumns[j]; if (!isNaN(col.explicitWidth)) { if (remainingWidth < col.width) break; remainingWidth -= col.width; } else if (col.contentSize || !isNaN(col.percentWidth)) { contentCols.push(col); contentColsIndices.push(j); } } var numContentCols:Number = contentCols.length; if (numContentCols > 0) { var splitWidth:Number = remainingWidth / numContentCols; for (j = 0; j < numContentCols; j++) { col = contentCols[j]; colWidth = Math.max(colWidths[contentColsIndices[j]], splitWidth); if (!isNaN(col.maxWidth)) colWidth = Math.min(colWidth, col.maxWidth); colWidths[contentColsIndices[j]] = Math.ceil(colWidth); } } } } /** * Adjusts the sizes of percent size columns or rows to fill the constrainedSize. * This method updates the provided column widths or row heights vector in place. * * The term, "region", refers to a column or row. * The percent size region sizes are first reset to their minimum size. * If the given region sizes from the content and fixed size regions already * fill the available space, then the percent size region sizes stay at their * minimum size. Otherwise, the remaining space is distributed to the percent * size regions based on the ratio of its percent size to the sum of all the * percent sizes. */ private function constrainPercentRegionSizes(sizes:Vector., constrainedSize:Number, isColumns:Boolean):void { var col:ConstraintColumn; var row:ConstraintRow; var numSizes:int = isColumns ? _constraintColumns.length : _constraintRows.length; var childInfoArray:Array /* of ConstraintRegionFlexChildInfo */ = []; var childInfo:ConstraintRegionFlexChildInfo; var remainingSpace:Number = constrainedSize; var percentMinSizes:Number = 0; var totalPercent:Number = 0; // Set percent size regions back to minSize and // find the remaining space. for (var i:int = 0; i < numSizes; i++) { var percentSize:Number; var minSize:Number; var maxSize:Number; if (isColumns) { col = _constraintColumns[i]; percentSize = col.percentWidth; minSize = col.minWidth; maxSize = col.maxWidth; } else { row = _constraintRows[i]; percentSize = row.percentHeight; minSize = row.minHeight; maxSize = row.maxHeight; } if (!isNaN(percentSize)) { sizes[i] = (!isNaN(minSize)) ? Math.ceil(Math.max(minSize, 0)) : 0; percentMinSizes += sizes[i]; totalPercent += percentSize; // Fill childInfoArray for distributing the width. childInfo = new ConstraintRegionFlexChildInfo(); childInfo.index = i; childInfo.percent = percentSize; childInfo.min = minSize; childInfo.max = maxSize; childInfoArray.push(childInfo); } else { remainingSpace -= sizes[i]; } } // If there's space remaining, distribute the width to the percent size // columns based on their ratio of percentWidth to sum of all the percentWidths. if (remainingSpace > percentMinSizes) { Flex.flexChildrenProportionally(constrainedSize, remainingSpace, totalPercent, childInfoArray); var roundOff:Number = 0; for each (childInfo in childInfoArray) { // Make sure the calculated widths are rounded to pixel boundaries var size:Number = Math.round(childInfo.size + roundOff); roundOff += childInfo.size - size; sizes[childInfo.index] = size; // remainingSpace -= size; } // TODO (klin): What do we do if there's remainingSpace after all this? } } /** * @private * This function measures the ConstraintColumns partitioning * the target and returns their new widths. The calculations * are based on the current constraintCache and other derived * data structures. To update the constraintCache, one needs to * call the parseConstraints() method. * * The widths are measured with the following requirements: * 1. Fixed size columns honor their pixel values. * * 2. Content size columns whose children only span that column * assumes the width of the widest child. * * 3. Content size columns whose children span more than one column * assumes the widest width possible when the child's size is divided * among the spanned columns. * a. Each child divides its preferred width among the content size * columns that it spans. A child also always honors fixed size * columns that it spans. * b. The column takes the widest width given by its children. * * 4. Percent size columns measure exactly like content size columns * at first, but after measurement, if availableWidth is provided, * the percent size columns are remeasured to allow the columns to * fit exactly in the remaining width in accordance with their given * percentage. * a. The percentages given are treated as ratios for how the width * should be divided among the percent size columns. * b. If no remaining space is available or the measured size of all * the content and fixed size columns are greater than the * constrainedWidth, percent size columns are set to their minimum. * * 5. Columns always honor their max and min widths. * * 6. If constrainedWidth is not specified, sum the column widths to find * the total measured width of the target. * * @param constrainedWidth The constraining width to be used when measuring * percent size columns. The default is NaN. * * @return A vector of the new column widths. */ mx_internal function measureColumns(constrainedWidth:Number = NaN):Vector. { // TODO (klin): Parameterize this to work for both columns and rows. // This may mean we need to add some mx_internal properties to // the columns for "major size", etc... Question is, what about // 1-D properties like baseline? What parts can we parameterize and // what parts aren't possible. // Parse constraints if it hasn't been done yet, make sure to clear // the cache afterwards. var clearCache:Boolean = false; if (!constraintCache) { parseConstraints(); clearCache = true; } if (_constraintColumns.length <= 0) return new Vector.(); var measuredWidth:Number = 0; var i:Number; var numCols:Number = _constraintColumns.length; var col:ConstraintColumn; var hasContentSize:Boolean = false; var hasPercentSize:Boolean = false; var colWidths:Vector. = new Vector.(numCols); // Start column widths at the minWidth of each column or // its explicit width. for (i = 0; i < numCols; i++) { col = _constraintColumns[i]; if (col.contentSize || !isNaN(col.percentWidth)) { hasContentSize ||= col.contentSize; hasPercentSize ||= !isNaN(col.percentWidth); if (!isNaN(col.minWidth)) colWidths[i] = Math.ceil(Math.max(col.minWidth, 0)); else colWidths[i] = 0; } else if (!isNaN(col.explicitWidth)) { var w:Number = col.explicitWidth; if (!isNaN(col.minWidth)) w = Math.max(w, col.minWidth); if (!isNaN(col.maxWidth)) w = Math.min(w, col.maxWidth); colWidths[i] = Math.ceil(w); } } // Assumption: elements in colSpanElements have one or more constraints touching a column. // This is enforced in parseElementConstraints(). if (colSpanElements && (hasContentSize || hasPercentSize)) { // Measure content/percent size columns. for each (var elementInfo:ElementConstraintInfo in colSpanElements) { updateColumnWidthsForElement(colWidths, elementInfo); } } // Adjust percent size columns to account for constraining width. if (!isNaN(constrainedWidth) && hasPercentSize) { constrainPercentRegionSizes(colWidths, constrainedWidth, true); } // Clear the cache only if we created it just for this method call. if (clearCache) clearConstraintCache(); return colWidths; } /** * @private * Updates the heights of content size and percent size rows that are spanned * by the specified element. This method updates the provided row heights * vector in place. The algorithm is as follows: * * 1) Determine the space needed by the element to satisfy its constraints * and be at its preferred size. * * 2) Calculate the number of rows the element will span. * * 3) If the element causes a row to expand, update the row height to match. * a) For the single spanning case, we only need to check if this element's * required height is larger than the row's current height. * b) For the multiple spanning case, we distribute the remaining height after * subtracting the fixed size rows across the content/percent size rows. * Then, we only update a row's height if the new divided height would cause * the row's height to expand. * * @param elementInfo The constraint information of the element. * @param colWidths The vector of column widths to update. */ private function updateRowHeightsForElement(rowHeights:Vector., elementInfo:ElementConstraintInfo):void { var layoutElement:ILayoutElement = elementInfo.layoutElement; var numRows:int = _constraintRows.length; var row:ConstraintRow; var topIndex:int = -1; var bottomIndex:int = -1; var span:int; var extY:Number = 0; var preferredHeight:Number = layoutElement.getPreferredBoundsHeight(); var maxExtent:Number; var remainingHeight:Number; var j:int; var rowHeight:Number = 0; // 1) Determine how much space the element needs to satisfy its // constraints and be at its preferred height. if (!isNaN(elementInfo.top)) { extY += elementInfo.top; if (elementInfo.topBoundary) topIndex = elementInfo.rowSpanTopIndex; else topIndex = 0; // constrained to parent } if (!isNaN(elementInfo.bottom)) { extY += elementInfo.bottom; if (elementInfo.bottomBoundary) bottomIndex = elementInfo.rowSpanBottomIndex; else bottomIndex = numRows - 1; // constrained to parent } // Only include baseline if at least one of top or bottom don't // exist. if (!isNaN(elementInfo.baseline) && (topIndex < 0 || bottomIndex < 0)) { extY += elementInfo.baseline - layoutElement.baselinePosition; if (!isNaN(elementInfo.top)) extY -= elementInfo.top; if (elementInfo.baselineBoundary) { topIndex = elementInfo.baselineIndex; // add baseline offset. extY += Number(rowBaselines[topIndex][0]); // add maxAscent. maxAscent is 0 if not specified on the row. if (rowMaxAscents) extY += rowMaxAscents[topIndex]; } else { topIndex = 0; } } maxExtent = extY + preferredHeight; remainingHeight = maxExtent; // 2) If either the top or the bottom constraint doesn't exist, // we must find the span of the element. We do this by // determining the index of the last row that the element // occupies in the unconstrained direction. if (topIndex < 0 || bottomIndex < 0) { var isTop:Boolean = topIndex < 0; var startIndex:int = isTop ? bottomIndex : topIndex; var endIndex:int = isTop ? -1 : numRows; var increment:int = isTop ? -1 : 1; if (isTop) // defaults to 0 topIndex = 0; else // defaults to numRows - 1 bottomIndex = numRows - 1; for (j = startIndex; j != endIndex ; j += increment) { row = _constraintRows[j]; // subtract fixed rows if (!isNaN(row.explicitHeight)) remainingHeight -= row.explicitHeight; if ((row.contentSize || !isNaN(row.percentHeight)) || remainingHeight < 0) { if (isTop) topIndex = j; else bottomIndex = j; break; } } } // always 1 or positive. span = bottomIndex - topIndex + 1; // 3) If the element causes a row to expand, update the row height to match. if (span == 1) { // a) For the single spanning case, we only need to check if this element's // required height is larger than the row's current height. row = _constraintRows[topIndex]; if (row.contentSize || !isNaN(row.percentHeight)) { rowHeight = Math.max(rowHeights[topIndex], extY + preferredHeight); if (constraintsDetermineHeight(elementInfo)) rowHeight = Math.max(rowHeight, extY + layoutElement.getMinBoundsHeight()); // bound with max height of row if (!isNaN(row.maxHeight)) rowHeight = Math.min(rowHeight, row.maxHeight); rowHeights[topIndex] = Math.ceil(rowHeight); } } else { // b) multiple spanning case. span >= 2. // 1) start from topIndex and subtract fixed rows // 2) divide space evenly into content/percent size rows. var contentRows:Vector. = new Vector.(); var contentRowsIndices:Vector. = new Vector.(); remainingHeight = maxExtent; for (j = topIndex; j <= bottomIndex; j++) { row = _constraintRows[j]; if (!isNaN(row.explicitHeight)) { if (remainingHeight < row.height) break; remainingHeight -= row.height; } else if (row.contentSize || !isNaN(row.percentHeight)) { contentRows.push(row); contentRowsIndices.push(j); } } var numContentRows:Number = contentRows.length; if (numContentRows > 0) { var splitHeight:Number = remainingHeight / numContentRows; for (j = 0; j < numContentRows; j++) { row = contentRows[j]; rowHeight = Math.max(rowHeights[contentRowsIndices[j]], splitHeight); if (!isNaN(row.maxHeight)) rowHeight = Math.min(rowHeight, row.maxHeight); rowHeights[contentRowsIndices[j]] = Math.ceil(rowHeight); } } } } /** * @private * Synonymous to measureColumns(), but with added baseline constraint. * Baseline is only included in the measurement if at least one of the element's * top or bottom constraint doesn't exist. The calculations are based on the * current constraintCache. To update the constraintCache, one needs to call * the parseConstraints() method. */ private function measureRows(constrainedHeight:Number = NaN):Vector. { if (_constraintRows.length <= 0) return new Vector.(); var measuredHeight:Number = 0; var i:Number; var numRows:Number = _constraintRows.length; var row:ConstraintRow; var hasContentSize:Boolean = false; var hasPercentSize:Boolean = false; var rowHeights:Vector. = new Vector.(numRows); // Start row heights at the minHeight of each row or // its explicit height. for (i = 0; i < numRows; i++) { row = _constraintRows[i]; if (row.contentSize || !isNaN(row.percentHeight)) { hasContentSize ||= row.contentSize; hasPercentSize ||= !isNaN(row.percentHeight); if (!isNaN(row.minHeight)) rowHeights[i] = Math.ceil(Math.max(row.minHeight, 0)); else rowHeights[i] = 0; } else if (!isNaN(row.explicitHeight)) { var h:Number = row.explicitHeight; if (!isNaN(row.minHeight)) h = Math.max(h, row.minHeight); if (!isNaN(row.maxHeight)) h = Math.min(h, row.maxHeight); rowHeights[i] = Math.ceil(h); } } // Assumption: elements in rowSpanElements have one or more constraints touching a row. // This is enforced in parseElementConstraints(). if (rowSpanElements && (hasContentSize || hasPercentSize)) { // Measure content/percent size columns. for each (var elementInfo:ElementConstraintInfo in rowSpanElements) { updateRowHeightsForElement(rowHeights, elementInfo); } } // Adjust percent size rows to account for constraining height. if (!isNaN(constrainedHeight) && hasPercentSize) { constrainPercentRegionSizes(rowHeights, constrainedHeight, false); } return rowHeights; } /** * @private * Measures the size of target based on content not included in the columns and rows. * Basically, applies BasicLayout to other content to determine measured size. * Returns a vector with the measured [width, height, minWidth, minHeight]. */ private function measureOtherContent():Vector. { var width:Number = 0; var height:Number = 0; var minWidth:Number = 0; var minHeight:Number = 0; var count:int = otherElements.length; for (var i:int = 0; i < count; i++) { var elementInfo:ElementConstraintInfo = otherElements[i]; var layoutElement:ILayoutElement = elementInfo.layoutElement; // Only measure width if not constrained to columns. if (!elementInfo.leftBoundary && !elementInfo.rightBoundary) { var left:Number = elementInfo.left; var right:Number = elementInfo.right; var extX:Number; if (!isNaN(left) && !isNaN(right)) { // If both left & right are set, then the extents is always // left + right so that the element is resized to its preferred // size (if it's the one that pushes out the default size of the container). extX = left + right; } else if (!isNaN(left) || !isNaN(right)) { extX = isNaN(left) ? 0 : left; extX += isNaN(right) ? 0 : right; } else { extX = layoutElement.getBoundsXAtSize(NaN, NaN); } var preferredWidth:Number = layoutElement.getPreferredBoundsWidth(); width = Math.max(width, extX + preferredWidth); // Find the minimum default extents, we take the minimum height only // when the element size is determined by the parent size var elementMinWidth:Number = constraintsDetermineWidth(elementInfo) ? layoutElement.getMinBoundsWidth() : preferredWidth; minWidth = Math.max(minWidth, extX + elementMinWidth); } // only measure height if not constrained to rows. var noVerticalBoundaries:Boolean = !elementInfo.topBoundary && !elementInfo.bottomBoundary; var noBaselineBoundary:Boolean = !elementInfo.baselineBoundary; if (noVerticalBoundaries || noBaselineBoundary) { var top:Number; var bottom:Number; var baseline:Number; var extY:Number; if (noVerticalBoundaries) { top = elementInfo.top; bottom = elementInfo.bottom; } if (noBaselineBoundary) baseline = elementInfo.baseline; if (!isNaN(top) && !isNaN(bottom)) { // If both top & bottom are set, then the extents is always // top + bottom so that the element is resized to its preferred // size (if it's the one that pushes out the default size of the container). extY = top + bottom; } else if (!isNaN(baseline)) { extY = Math.round(baseline - layoutElement.baselinePosition); } else if (!isNaN(top) || !isNaN(bottom)) { extY = isNaN(top) ? 0 : top; extY += isNaN(bottom) ? 0 : bottom; } else { extY = layoutElement.getBoundsYAtSize(NaN, NaN); } var preferredHeight:Number = layoutElement.getPreferredBoundsHeight(); height = Math.max(height, extY + preferredHeight); // Find the minimum default extents, we take the minimum height only // when the element size is determined by the parent size var elementMinHeight:Number = constraintsDetermineHeight(elementInfo) ? layoutElement.getMinBoundsHeight() : preferredHeight; minHeight = Math.max(minHeight, extY + elementMinHeight); } } var vec:Vector. = new Vector.(4, true); vec[0] = Math.max(width, minWidth); vec[1] = Math.max(height, minHeight); vec[2] = minWidth; vec[3] = minHeight; return vec; } /** * @private * Iterates over elements and calls parseElementConstraints on each. */ private function parseConstraints():void { var layoutTarget:GroupBase = target; if (!layoutTarget) return; var count:Number = layoutTarget.numElements; var layoutElement:ILayoutElement; var cache:Dictionary = new Dictionary(true); var i:int; // Populate rowBaselines with baseline information from rows. var n:int = _constraintRows.length; var row:ConstraintRow; var obj:Object = new Object(); if (rowBaselines == null) rowBaselines = new Vector.(); else rowBaselines.length = 0; for (i = 0; i < n; i++) { row = _constraintRows[i]; rowBaselines[i] = LayoutElementHelper.parseConstraintExp(row.baseline); var maxAscentStr:String = rowBaselines[i][1]; if (maxAscentStr && maxAscentStr != "maxAscent") throw new Error(ResourceManager.getInstance().getString("layout", "invalidBaselineOnRow", [ row.id, row.baseline ])); } for (i = 0; i < count; i++) { layoutElement = layoutTarget.getElementAt(i); if (!layoutElement || !layoutElement.includeInLayout) continue; parseElementConstraints(layoutElement, cache); } this.constraintCache = cache; } /** * @private * This function parses the constraints of a single element, creates an * ElementConstraintInfo object for the element, and throws errors if the * columns or rows are not found for each constraint. */ private function parseElementConstraints(layoutElement:ILayoutElement, constraintCache:Dictionary):void { // Variables to track the offsets var left:Number; var right:Number; var top:Number; var bottom:Number; var baseline:Number; // Variables to track the boundaries from which // the offsets are calculated from. If null, the // boundary is the parent container edge. var leftBoundary:String; var rightBoundary:String; var topBoundary:String; var bottomBoundary:String; var baselineBoundary:String; var message:String; var temp:Array; temp = LayoutElementHelper.parseConstraintExp(layoutElement.left); left = temp[0]; leftBoundary = temp[1]; temp = LayoutElementHelper.parseConstraintExp(layoutElement.right); right = temp[0]; rightBoundary = temp[1]; temp = LayoutElementHelper.parseConstraintExp(layoutElement.top); top = temp[0]; topBoundary = temp[1]; temp = LayoutElementHelper.parseConstraintExp(layoutElement.bottom); bottom = temp[0]; bottomBoundary = temp[1]; temp = LayoutElementHelper.parseConstraintExp(layoutElement.baseline); baseline = temp[0]; baselineBoundary = temp[1]; // save values into a Dictionary based on element name. var elementInfo:ElementConstraintInfo = new ElementConstraintInfo(layoutElement, left, right, top, bottom, baseline, leftBoundary, rightBoundary, topBoundary, bottomBoundary, baselineBoundary); constraintCache[layoutElement] = elementInfo; // If some pair of boundaries don't exist, we will need to measure // the container size based on the element's other properties like // x, y, width, height. var i:Number; if ((!leftBoundary && !rightBoundary) || (!topBoundary && !bottomBoundary) || !baselineBoundary) { if (!otherElements) otherElements = new Vector.(); otherElements.push(elementInfo); } // match columns if (leftBoundary || rightBoundary) { var numColumns:Number = _constraintColumns.length; var colIndex:Object; if (!colSpanElements) colSpanElements = new Vector.(); colSpanElements.push(elementInfo); if (leftBoundary) { colIndex = columnsObject[leftBoundary]; if (colIndex != null) elementInfo.colSpanLeftIndex = int(colIndex); // throw error if no match. if (elementInfo.colSpanLeftIndex < 0) { message = ResourceManager.getInstance().getString( "layout", "columnNotFound", [ leftBoundary ]); throw new ConstraintError(message); } } // can we assume rightIndex >= leftIndex? if (rightBoundary) { colIndex = columnsObject[rightBoundary]; if (colIndex != null) elementInfo.colSpanRightIndex = int(colIndex); // throw error if no match. if (elementInfo.colSpanRightIndex < 0) { message = ResourceManager.getInstance().getString( "layout", "columnNotFound", [ rightBoundary ]); throw new ConstraintError(message); } } } // match rows. if (topBoundary || bottomBoundary || baselineBoundary) { var rowIndex:Object; if (!rowSpanElements) rowSpanElements = new Vector.(); rowSpanElements.push(elementInfo); if (topBoundary) { rowIndex = rowsObject[topBoundary]; if (rowIndex != null) elementInfo.rowSpanTopIndex = int(rowIndex); // throw error if no match. if (elementInfo.rowSpanTopIndex < 0) { message = ResourceManager.getInstance().getString( "layout", "rowNotFound", [ topBoundary ]); throw new ConstraintError(message); } } if (bottomBoundary) { rowIndex = rowsObject[bottomBoundary]; if (rowIndex != null) elementInfo.rowSpanBottomIndex = int(rowIndex); // throw error if no match. if (elementInfo.rowSpanBottomIndex < 0) { message = ResourceManager.getInstance().getString( "layout", "rowNotFound", [ bottomBoundary ]); throw new ConstraintError(message); } } if (baselineBoundary) { rowIndex = rowsObject[baselineBoundary]; if (rowIndex != null) elementInfo.baselineIndex = int(rowIndex); // throw error if no match. if (elementInfo.baselineIndex < 0) { message = ResourceManager.getInstance().getString( "layout", "rowNotFound", [ baselineBoundary ]); throw new ConstraintError(message); } // when using maxAscent, calculate maximum baselinePosition for this row. var bIndex:int = elementInfo.baselineIndex; var numRows:Number = _constraintRows.length; if (rowBaselines[bIndex][1]) { // maxAscents will all default to 0. if (!rowMaxAscents) rowMaxAscents = new Vector.(numRows, true); rowMaxAscents[bIndex] = Math.max(rowMaxAscents[bIndex], layoutElement.baselinePosition); } } } } /** * @private */ private function clearConstraintCache():void { colSpanElements = null; rowSpanElements = null; otherElements = null; rowBaselines = null; rowMaxAscents = null; constraintCache = null; } } } //////////////////////////////////////////////////////////////////////////////// // // Helper class: ElementConstraintInfo // //////////////////////////////////////////////////////////////////////////////// import mx.core.ILayoutElement; class ElementConstraintInfo { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * @private */ public function ElementConstraintInfo( layoutElement:ILayoutElement, left:Number, right:Number, top:Number, bottom:Number, baseline:Number, leftBoundary:String = null, rightBoundary:String = null, topBoundary:String = null, bottomBoundary:String = null, baselineBoundary:String = null, colSpanLeftIndex:int = -1, colSpanRightIndex:int = -1, rowSpanTopIndex:int = -1, rowSpanBottomIndex:int = -1, baselineIndex:int = -1):void { super(); // pointer to element this.layoutElement = layoutElement; // offsets this.left = left; this.right = right; this.top = top; this.bottom = bottom; this.baseline = baseline; // boundaries (ie: parent, column or row edge) this.leftBoundary = leftBoundary; this.rightBoundary = rightBoundary; this.topBoundary = topBoundary; this.bottomBoundary = bottomBoundary; this.baselineBoundary = baselineBoundary; this.colSpanLeftIndex = colSpanLeftIndex; this.colSpanRightIndex = colSpanRightIndex; this.rowSpanTopIndex = rowSpanTopIndex; this.rowSpanBottomIndex = rowSpanBottomIndex; this.baselineIndex = baselineIndex; } //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- public var layoutElement:ILayoutElement; public var left:Number; public var right:Number; public var top:Number; public var bottom:Number; public var baseline:Number; public var leftBoundary:String; public var rightBoundary:String; public var topBoundary:String; public var bottomBoundary:String; public var baselineBoundary:String; public var colSpanLeftIndex:int; public var colSpanRightIndex:int; public var rowSpanTopIndex:int; public var rowSpanBottomIndex:int; public var baselineIndex:int; } //////////////////////////////////////////////////////////////////////////////// // // Helper class: ConstraintRegionFlexChildInfo // //////////////////////////////////////////////////////////////////////////////// import mx.containers.utilityClasses.FlexChildInfo; class ConstraintRegionFlexChildInfo extends FlexChildInfo { public var index:int }