//////////////////////////////////////////////////////////////////////////////// // // 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.core.ILayoutElement; import mx.core.IVisualElement; import mx.core.mx_internal; import spark.components.DataGroup; import spark.components.SpinnerList; import spark.components.supportClasses.GroupBase; import spark.core.NavigationUnit; use namespace mx_internal; [ExcludeClass] /** * Custom wrapping layout for the SpinnerList */ public class VerticalSpinnerLayout extends VerticalLayout { public function VerticalSpinnerLayout() { super(); } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- private function get totalHeight():Number { return Math.ceil(target.numElements * rowHeight); } // If true, then when the layout encounters a disabled element, it will scroll past it // in ascending index order. If false, then it will scroll in the descending index order mx_internal var autoScrollAscending:Boolean = false; mx_internal static const FORCE_NO_WRAP_ELEMENTS_CHANGE:String = "forceNoWrapElementsChange"; // If the scrollPosition is too small or large, we need to shift the y positions // of the item renderers or else we hit a player limitation. private var yOffset:Number = 0; // The max value for x/y seems to be 2^30 / 10. int.MAX_VALUE = 2^31; private static const MAX_Y_VALUE:Number = int.MAX_VALUE / 20; private static const MIN_Y_VALUE:Number = int.MIN_VALUE / 20; //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // requestedWrapElements //---------------------------------- private var _requestedWrapElements:Boolean = true; /** * This is the suggested value for wrapElements. However, the layout might not honor this value * if there are too few elements to display in the viewable area. * * @default true */ public function get requestedWrapElements():Boolean { return _requestedWrapElements; } public function set requestedWrapElements(value:Boolean):void { if (value == _requestedWrapElements) return; _requestedWrapElements = value; target.invalidateSize(); target.invalidateDisplayList(); } /** * If true, the layout has forced wrapElements to be false */ public var forceNoWrapElements:Boolean = false; //---------------------------------- // wrapElements //---------------------------------- /** * When true, scrolling past the last element will scroll to the first element. * * @default true */ public function get wrapElements():Boolean { if (forceNoWrapElements) return false; else return requestedWrapElements; } //-------------------------------------------------------------------------- // // Overridden methods // //-------------------------------------------------------------------------- override public function measure():void { var preferredWidth:Number = 0; // max of the elt preferred widths var element:ILayoutElement; var startIndex:int = 0; var iter:LayoutIterator = new LayoutIterator(target, startIndex); if (useVirtualLayout) { if (typicalLayoutElement) preferredWidth = typicalLayoutElement.getPreferredBoundsWidth(); } else { do { element = iter.getCurrentElement(); if (element && element.includeInLayout) preferredWidth = Math.max(Math.ceil(element.getPreferredBoundsWidth()), preferredWidth); iter.next(); } while (startIndex != iter.currentIndex); // Loop until we are back at the start } var rowsToMeasure:int = getRowsToMeasure(target.numElements); // Calculate the height by multiplying the number of elements time the row height target.measuredHeight = Math.ceil(rowsToMeasure * Math.max(5, rowHeight)); target.measuredWidth = preferredWidth; } override public function updateDisplayList(width:Number, height:Number):void { var element:ILayoutElement; var numElements:int = target.numElements; var oldForceNoWrapElements:Boolean = forceNoWrapElements; // If there are fewer elements than will fit, we need to set wrapElements = false if (requestedWrapElements && (height > numElements * rowHeight)) forceNoWrapElements = true; else forceNoWrapElements = false; if (forceNoWrapElements != oldForceNoWrapElements) dispatchEvent(new Event(FORCE_NO_WRAP_ELEMENTS_CHANGE)); var scrollPosition:Number = verticalScrollPosition; var itemIndex:int = Math.floor(scrollPosition / rowHeight); var yPos:Number; var yPosMax:Number; var foundLastVisibleElement:Boolean = false; var numVisibleElements:int = 0; var numVisitedElements:int = 0; // Translate the vsp to the item index if (!wrapElements) { if (!useVirtualLayout) itemIndex = 0; else itemIndex = Math.max(Math.min(itemIndex, numElements - 1), 0); } yPos = itemIndex * rowHeight + yOffset; // Calculate the y position of the bottom of the viewable area yPosMax = yPos + rowHeight + height; // Normalize the itemIndex if (wrapElements) itemIndex = normalizeItemIndex(itemIndex); // Start at the top index var iter:LayoutIterator = new LayoutIterator(target, itemIndex); if (numElements > 0) { do { element = iter.getCurrentElement(); if (element && element.includeInLayout) { numVisitedElements++; element.setLayoutBoundsSize(width, rowHeight); element.setLayoutBoundsPosition(0, yPos); yPos += rowHeight; // If we are using virtual layout, only size and position // the visible elements if (yPos > yPosMax && !foundLastVisibleElement) { foundLastVisibleElement = true; // Keep track of the number of elements visible in the viewing area numVisibleElements = numVisitedElements; if (useVirtualLayout) break; } } // Make sure to not wrap if wrapElements = false if (!wrapElements && iter.currentIndex == numElements - 1) break; iter.next(); } while (itemIndex != iter.currentIndex) } setRowCount(numVisibleElements); // Set the contentWidth and contentHeight target.setContentSize(target.width, Math.ceil(numElements * rowHeight)); } override public function updateScrollRect(w:Number, h:Number):void { var g:GroupBase = target; if (!g) return; if (clipAndEnableScrolling) { var hsp:Number = horizontalScrollPosition; var vsp:Number = verticalScrollPosition; // If the verticalScrollPosition exceeds the max/min y value, then the // renderers will not be properly positioned. In which case, // we offset the y position of the renderers by the verticalScrollPosition if (((vsp + yOffset + g.getPreferredBoundsHeight()) > MAX_Y_VALUE) || ((vsp + yOffset) < MIN_Y_VALUE)) yOffset = -vsp; g.scrollRect = new Rectangle(hsp, vsp + yOffset, w, h); } else g.scrollRect = null; } override public function getElementBounds(index:int):Rectangle { return new Rectangle(0, index * rowHeight, target.measuredWidth, rowHeight); } //-------------------------------------------------------------------------- // // Overridden scroll methods // //-------------------------------------------------------------------------- override protected function scrollPositionChanged():void { var g:GroupBase = target; if (!g) return; updateScrollRect(g.width, g.height); var n:int = g.numElements - 1; if (n < 0) { setIndexInView(-1, -1); return; } var scrollR:Rectangle = getScrollRect(); if (!scrollR) { setIndexInView(0, n); return; } // Apply the offset var y0:Number = scrollR.top + yOffset; var y1:Number = scrollR.bottom + yOffset - .0001; if (y1 <= y0) { setIndexInView(-1, -1); return; } var i0:int; var i1:int; if (wrapElements) { i0 = normalizeItemIndex(Math.floor(y0 / rowHeight)); i1 = normalizeItemIndex(Math.floor(y1 / rowHeight)); } else { i0 = Math.min(Math.max(Math.floor(y0 / rowHeight), 0),n); i1 = Math.min(Math.max(Math.floor(y1 / rowHeight), 0),n); } setIndexInView(i0, i1); var firstElement:ILayoutElement = g.getElementAt(firstIndexInView); var lastElement:ILayoutElement = g.getElementAt(lastIndexInView); if (wrapElements) { if (!firstElement || !lastElement || y0 < firstElement.getLayoutBoundsY() || y1 >= (lastElement.getLayoutBoundsY() + lastElement.getLayoutBoundsHeight())) { g.invalidateDisplayList(); } } else { if (!firstElement || !lastElement || (y0 < firstElement.getLayoutBoundsY() && firstIndexInView != 0) || (y1 >= (lastElement.getLayoutBoundsY() + lastElement.getLayoutBoundsHeight()) && lastIndexInView != n)) { g.invalidateDisplayList(); } } } override public function getHorizontalScrollPositionDelta(navigationUnit:uint):Number { return 0; } override public function getVerticalScrollPositionDelta(navigationUnit:uint):Number { return 0; } override mx_internal function getElementNearestScrollPosition( position:Point,elementComparePoint:String = "center"):int { var index:int = Math.floor(position.y / rowHeight); // may be larger than numElements to indicate wrapping var item:Object; var startIndex:int = index % target.numElements; var distance:int = 0; var direction:int = autoScrollAscending ? 1 : -1; var dataGroup:DataGroup = target as DataGroup; if (startIndex < 0) startIndex += target.numElements; // If the element at index % numElements) is not selectable, find the nearest one that is var iter:LayoutIterator = new LayoutIterator(target, startIndex); if (dataGroup && dataGroup.dataProvider && dataGroup.dataProvider.length > 0) { while (Math.abs(distance) <= (target.numElements / 2) + 1) { // Try searching in one direction iter.currentIndex = startIndex + distance * direction; item = dataGroup.dataProvider.getItemAt(normalizeItemIndex(iter.currentIndex)); if (isElementEnabled(item)) break; if (distance != 0) { // Flip the direction direction *= -1; // Try searching in the other direction iter.currentIndex = startIndex + distance * direction; item = dataGroup.dataProvider.getItemAt(normalizeItemIndex(iter.currentIndex)); if (isElementEnabled(item)) break; // Flip the direction back direction *= -1; } distance++; } } // If we don't allow wrapping, then cap the max index if(!wrapElements) index = Math.max(0, Math.min(index, target.numElements - 1)); return index + distance * direction; } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * Returns the index of the element intersected by the vertical center of the viewable area */ public function getIndexAtVerticalCenter():int { var midY:Number = target.getLayoutBoundsHeight() / 2; var vsp:Number = wrapElements ? normalizeScrollPosition(verticalScrollPosition + midY) : verticalScrollPosition + midY; return getElementNearestScrollPosition(new Point(0, vsp), "center"); } /** * Takes an index between 0 and numElements and returns the closest index * to the current position, taking wrapping into account */ public function getClosestUnwrappedElementIndex(index:int):int { if (wrapElements) { // Figure out the wrapCount of the center index var midVSP:Number = target.getLayoutBoundsHeight() / 2 + verticalScrollPosition; var wrapCount:int = Math.floor(midVSP / totalHeight); // Get unwrapped middle index var centerIndex:int = getElementNearestScrollPosition(new Point(0, midVSP), "center"); // Get the unwrapped indicies near the center index var prevIndex:int = index + (wrapCount - 1) * target.numElements; var midIndex:int = prevIndex + target.numElements; var nextIndex:int = midIndex + target.numElements; var prevDistance:int = Math.abs(centerIndex - prevIndex); var midDistance:int = Math.abs(midIndex - centerIndex); var nextDistance:int = Math.abs(nextIndex - centerIndex) // Figure out which index is closer to the centerIndex and return that value if (prevDistance < midDistance) index = prevIndex; else if (midDistance < nextDistance) index = midIndex; else index = nextIndex; } return index; } // Helper function to calculate the non-wrapped, non-negative scroll position private function normalizeScrollPosition(vsp:int):int { // Normalize the scrollPosition if (!isNaN(totalHeight)) { vsp %= totalHeight; if (vsp < 0) vsp += totalHeight; } return vsp; } // Helper function to normalize the item index private function normalizeItemIndex(index:int):int { if (target) { index %= target.numElements; if (index < 0) index += target.numElements; } return index; } // Helper function to return whether an element is enabled or not private function isElementEnabled(element:Object):Boolean { var result:Boolean = true; // If data is a String or other primitive, this call will fail try { result = element["_enabled_"] == undefined || element["_enabled_"]; } catch (e:Error) { } return result; } } } //////////////////////////////////////////////////////////////////////////////// // // Helper class: LayoutIterator // //////////////////////////////////////////////////////////////////////////////// import mx.core.ILayoutElement; import spark.components.supportClasses.GroupBase; /** * Layout helper class. Iterates over a set of items. The iterator can optionally wrap around the * end of the set back to the beginning of the set. */ class LayoutIterator { /** * Constructor. Takes a layout target and the starting index for the iterator * * @param target The GroupBase target that contains the elements * @param index The starting index for the iterator */ public function LayoutIterator(target:GroupBase, index:int = 0):void { totalElements = target.numElements; _target = target; _curIndex = index; _useVirtual = _target.layout.useVirtualLayout; } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- private var _curIndex:int; private var _target:GroupBase; private var _useVirtual:Boolean; private var totalElements:int; //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- /** * Returns the index that the iterator is currently pointing */ public function get currentIndex():int { return _curIndex; } public function set currentIndex(value:int):void { _curIndex = value; } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * Get the element at the currentIndex */ public function getCurrentElement():ILayoutElement { return _useVirtual ? _target.getVirtualElementAt(_curIndex) : _target.getElementAt(_curIndex); } /** * Move the currentIndex to the next index. If the currentIndex is at * the last index, then it is set to the first index. */ public function next():int { if (_curIndex == totalElements - 1) _curIndex = 0; else _curIndex++; return _curIndex; } /** * Move the currentIndex to the previous index. If the currentIndex is at * the fist index, then it is set to the last index. */ public function prev():int { if (_curIndex == 0) _curIndex = totalElements - 1; else _curIndex--; return _curIndex; } }