blob: a633f0edf0e2e18324da7685fa7545aecb54856e [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package spark.layouts
{
import flash.events.Event;
import flash.geom.Point;
import flash.geom.Rectangle;
import mx.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;
}
}