| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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 mx.core.IVisualElement; |
| |
| import spark.components.supportClasses.GroupBase; |
| import spark.core.NavigationUnit; |
| import spark.layouts.supportClasses.LayoutBase; |
| |
| /** |
| * The ViewMenuLayout class defines the layout of the ViewMenu container. |
| * The menu can have multiple rows depending on the number of menu items. |
| * |
| * <p>The <code>requestedMaxColumnCount</code> property |
| * defines the maximum number of menu items in a row. |
| * By default, the property is set to three.</p> |
| * |
| * <p>The ViewMenuLayout class define the layout as follows: </p> |
| * |
| * <ul> |
| * <li>If you define three or fewer menu items, |
| * where the <code>requestedMaxColumnCount</code> property contains |
| * the default value of three, the menu items are displayed in a single row. |
| * Each menu item has the same size. |
| * <p>If you define four or more menu items, meaning more menu items |
| * than specified by the <code>requestedMaxColumnCount</code> property, |
| * the ViewMenu container creates multiple rows.</p></li> |
| * <li>If the number of menu items is evenly divisible by |
| * the <code>requestedMaxColumnCount</code> property, |
| * each row contains the same number of menu items. |
| * Each menu item is the same size. |
| * <p>For example the <code>requestedMaxColumnCount</code> property |
| * is set to the default value of three and you define six menu items. |
| * The menu displays two rows, each containing three menu items. </p></li> |
| * <li>If the number of menu items is not evenly divisible by |
| * the <code>requestedMaxColumnCount</code> property, |
| * rows can contain a different number of menu items. |
| * The size of the menu items depends on the number of menu items |
| * in the row. |
| * <p>For example the <code>requestedMaxColumnCount</code> property |
| * is set to the default value of three and you define eight menu items. |
| * The menu displays three rows. |
| * The first row contains two menu items. |
| * The second and third rows each contains three items. </p></li> |
| * </ul> |
| * |
| * <p>You can create your own custom layout for the menu by creating |
| * your own layout class. |
| * By default, the spark.skins.mobile.ViewMenuSkin class defines |
| * the skin for the ViewMenu container. |
| * To apply a customized ViewMenuLayout class to the ViewMenu container, |
| * define a new skin class for the ViewMenu container. </p> |
| * |
| * <p>The ViewMenuSkin class includes a definition for a Group |
| * container named <code>contentGroup</code>, as shown below:</p> |
| * |
| * <pre> |
| * <s:Group id="contentGroup" left="0" right="0" top="3" bottom="2" |
| * minWidth="0" minHeight="0"> |
| * <s:layout> |
| * <s:ViewMenuLayout horizontalGap="2" verticalGap="2" id="contentGroupLayout" |
| * requestedMaxColumnCount="3" requestedMaxColumnCount.landscapeGroup="6"/> |
| * </s:layout> |
| * </s:Group></pre> |
| * |
| * <p>To apply your customized ViewMenuLayout class, your skin class |
| * should define a container named <code>contentGroup</code>. |
| * That container uses the <code>layout</code> property |
| * to specify your customized layout class. </p> |
| * |
| * @mxml |
| * <p>The <code><s:ViewMenuLayout></code> tag inherits all of the tag |
| * attributes of its superclass and adds the following tag attributes:</p> |
| * |
| * <pre> |
| * <s:ViewMenuLayout |
| * <strong>Properties</strong> |
| * horizontalGap="2" |
| * requestedMaxColumnCount="3" |
| * verticalGap="2" |
| * /> |
| * </pre> |
| * |
| * @see spark.components.ViewMenu |
| * @see spark.components.ViewMenuItem |
| * @see spark.skins.mobile.ViewMenuSkin |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 2.5 |
| * @productversion Flex 4.5 |
| */ |
| public class ViewMenuLayout extends LayoutBase |
| { |
| |
| /** |
| * Constructor. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 2.5 |
| * @productversion Flex 4.5 |
| */ |
| public function ViewMenuLayout() |
| { |
| super(); |
| } |
| |
| private var numColsInRow:Array; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Properties |
| // |
| //-------------------------------------------------------------------------- |
| |
| private var _horizontalGap:Number = 2; |
| |
| [Bindable("propertyChange")] |
| [Inspectable(category="General")] |
| |
| /** |
| * The horizontal space between columns, in pixels. |
| * |
| * @see #verticalGap |
| * @default 2 |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 2.5 |
| * @productversion Flex 4.5 |
| */ |
| public function get horizontalGap():Number |
| { |
| return _horizontalGap; |
| } |
| |
| /** |
| * @private |
| */ |
| public function set horizontalGap(value:Number):void |
| { |
| if (value == _horizontalGap) |
| return; |
| |
| _horizontalGap = value; |
| invalidateTargetSizeAndDisplayList(); |
| } |
| |
| private var _verticalGap:Number = 2; |
| |
| [Bindable("propertyChange")] |
| [Inspectable(category="General")] |
| |
| /** |
| * The vertical space between rows, in pixels. |
| * |
| * @see #horizontalGap |
| * @default 2 |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 2.5 |
| * @productversion Flex 4.5 |
| */ |
| public function get verticalGap():Number |
| { |
| return _verticalGap; |
| } |
| |
| /** |
| * @private |
| */ |
| public function set verticalGap(value:Number):void |
| { |
| if (value == _verticalGap) |
| return; |
| |
| _verticalGap = value; |
| invalidateTargetSizeAndDisplayList(); |
| } |
| |
| private var _requestedMaxColumnCount:int = 3; |
| |
| /** |
| * The maximum number of columns to display in a row. |
| * |
| * @default 3 |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 1.5 |
| * @productversion Flex 4 |
| */ |
| public function get requestedMaxColumnCount():int |
| { |
| return _requestedMaxColumnCount; |
| } |
| |
| /** |
| * @private |
| */ |
| public function set requestedMaxColumnCount(value:int):void |
| { |
| if (_requestedMaxColumnCount == value) |
| return; |
| |
| _requestedMaxColumnCount = value; |
| |
| invalidateTargetSizeAndDisplayList(); |
| } |
| |
| private var rowHeight:Number = 0; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Overridden Methods |
| // |
| //-------------------------------------------------------------------------- |
| |
| // TODO (jszeto) Fix up logic to use brick algorithm. Might not have full number of columns |
| /** |
| * @private |
| */ |
| override public function measure():void |
| { |
| super.measure(); |
| |
| var layoutTarget:GroupBase = target; |
| if (!layoutTarget) |
| return; |
| |
| var numItems:int = layoutTarget.numElements; |
| var numRows:int = Math.ceil(numItems / requestedMaxColumnCount); |
| var numColumns:int = Math.ceil(numItems / numRows); |
| |
| var maxItemWidth:Number = 0; |
| var maxItemHeight:Number = 0; |
| |
| for (var i:int = 0; i < numItems; i++) |
| { |
| var item:IVisualElement = layoutTarget.getElementAt(i); |
| maxItemWidth = Math.max(maxItemWidth, item.getPreferredBoundsWidth()); |
| maxItemHeight = Math.max(maxItemHeight, item.getPreferredBoundsHeight()); |
| } |
| |
| layoutTarget.measuredWidth = Math.ceil(maxItemWidth * numColumns) + (numColumns - 1) * horizontalGap; |
| layoutTarget.measuredHeight = Math.ceil(maxItemHeight * numRows) + (numRows - 1) * verticalGap; |
| |
| // Save the maxItemHeight and use in updateDisplayList as the height for all items |
| rowHeight = maxItemHeight; |
| |
| } |
| |
| |
| /** |
| * @private |
| */ |
| override public function updateDisplayList(width:Number, height:Number):void |
| { |
| super.updateDisplayList(width, height); |
| |
| var layoutTarget:GroupBase = target; |
| if (!layoutTarget) |
| return; |
| |
| numColsInRow = []; |
| |
| var xPos:Number = 0; |
| var yPos:Number = 0; |
| var itemIndex:int = 0; |
| var itemW:int = 0; |
| var extraWidth:int = 0; |
| |
| var numItems:int = layoutTarget.numElements; |
| var numRows:int = Math.ceil(numItems / requestedMaxColumnCount); |
| var numColumns:int = Math.ceil(numItems / numRows); |
| // Calculate the number of empty spots by getting the inverse of the column mod |
| var emptySpots:int = (numItems % numColumns) > 0 ? numColumns - numItems % numColumns : 0; |
| |
| for (var rowIndex:int = 0; rowIndex < numRows; rowIndex++) |
| { |
| var currentRowColumns:int = (emptySpots > 0) ? numColumns - 1 : numColumns; |
| var viewWidth:Number = width - (currentRowColumns - 1) * horizontalGap; |
| var w:Number = itemW = Math.floor(viewWidth / currentRowColumns); |
| |
| numColsInRow.push(currentRowColumns); |
| |
| // Keep track of the extra pixels since we round off the item widths |
| extraWidth = Math.round(viewWidth - w * currentRowColumns); |
| |
| for (var colIndex:int = 0; colIndex < currentRowColumns; itemIndex++, colIndex++) |
| { |
| var item:IVisualElement = layoutTarget.getElementAt(itemIndex); |
| |
| // Add a pixel of extra width to the first item |
| if (extraWidth > 0) |
| { |
| itemW += 1; |
| extraWidth--; |
| } |
| |
| item.setLayoutBoundsPosition(xPos, yPos); |
| item.setLayoutBoundsSize(itemW, rowHeight); |
| |
| xPos += itemW + horizontalGap; |
| itemW = w; |
| } |
| |
| xPos = 0; |
| yPos += rowHeight + verticalGap; |
| |
| numItems -= currentRowColumns; |
| |
| emptySpots = (numItems % numColumns) > 0 ? numColumns - numItems % numColumns : 0; |
| } |
| } |
| |
| /** |
| * @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; |
| var newIndex:int = 0; |
| var numRows:int = Math.ceil(target.numElements / requestedMaxColumnCount); |
| |
| if (currentIndex == -1) |
| { |
| if (navigationUnit == NavigationUnit.RIGHT || navigationUnit == NavigationUnit.DOWN) |
| return 0; |
| else |
| return -1; |
| } |
| |
| var currentRow:int = getRowForIndex(currentIndex); |
| var currentColCount:int; |
| var newColCount:int; |
| |
| if (navigationUnit == NavigationUnit.LEFT || |
| navigationUnit == NavigationUnit.RIGHT) |
| |
| { |
| newIndex = currentIndex + (navigationUnit == NavigationUnit.LEFT ? -1 : 1); |
| |
| // We don't support wrapping, so if the old and new index are |
| // on different rows, then don't change the index. |
| if (getRowForIndex(newIndex) != currentRow) |
| newIndex = currentIndex; |
| } |
| else if (navigationUnit == NavigationUnit.UP) |
| { |
| if (currentRow == 0) |
| return currentIndex; |
| |
| currentColCount = numColsInRow[currentRow]; |
| newColCount = numColsInRow[currentRow - 1]; |
| |
| newIndex = currentIndex - newColCount; |
| |
| // If the newIndex isn't on the previous row, then we need to shift |
| // it back one more spot. This situation only occurs when the |
| // number of columns in the two rows are different |
| if ((getRowForIndex(newIndex) != currentRow - 1) && (currentColCount != newColCount)) |
| { |
| newIndex--; |
| } |
| } |
| else if (navigationUnit == NavigationUnit.DOWN) |
| { |
| if (currentRow == numRows - 1) |
| return currentIndex; |
| |
| // Assumes that the smaller column rows are always above the larger column rows |
| newIndex = currentIndex + numColsInRow[currentRow]; |
| } |
| |
| if (newIndex > maxIndex) |
| newIndex = maxIndex; |
| else if (newIndex < 0) |
| newIndex = 0; |
| |
| return newIndex; |
| } |
| |
| // Helper function that figures out the row of a particular index |
| private function getRowForIndex(index:int):int |
| { |
| var currentRow:int = 0; |
| |
| while (currentRow < numColsInRow.length) |
| { |
| index -= numColsInRow[currentRow]; |
| |
| if (index >= 0) |
| currentRow++; |
| else |
| break; |
| } |
| |
| return currentRow; |
| } |
| |
| // Helper function |
| private function invalidateTargetSizeAndDisplayList():void |
| { |
| var g:GroupBase = target; |
| if (!g) |
| return; |
| |
| g.invalidateSize(); |
| g.invalidateDisplayList(); |
| } |
| } |
| |
| |
| } |