Revision: 50430
Initial Code
Initial URL
Initial Description
Initial Title
Initial Tags
Initial Language
at August 21, 2011 06:27 by ryanstewart
Initial Code
package layouts
{
import flash.geom.Matrix;
import flash.geom.Matrix3D;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.geom.Vector3D;
import mx.core.ILayoutElement;
import mx.core.IVisualElement;
import spark.components.supportClasses.GroupBase;
import spark.core.NavigationUnit;
import spark.layouts.supportClasses.LayoutBase;
/***
* Evtim is the man. This is all his - http://evtimmy.com/ - modified by someone who only
* half knows what he's doing. Sorry Evtim if I butchered the code.
**/
public class MonthWheelLayout extends LayoutBase
{
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
public function MonthWheelLayout()
{
super();
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// gap
//----------------------------------
private var _gap:Number = 0;
/**
* The gap between the items
*/
public function get gap():Number
{
return _gap;
}
public function set gap(value:Number):void
{
_gap = value;
var layoutTarget:GroupBase = target;
if (layoutTarget)
{
layoutTarget.invalidateSize();
layoutTarget.invalidateDisplayList();
}
}
//----------------------------------
// axisAngle
//----------------------------------
/**
* @private
* The total width of all items, including gap space.
*/
private var _totalWidth:Number = 4440;
private var _itemWidth:Number = 370;
private var _itemHeight:Number = 144;
private var _halfWidthDiagonal:Number = Math.sqrt(_itemWidth * _itemWidth / 4 + _itemHeight * _itemHeight);
/**
* @private
* Cache which item is currently in view, to facilitate scrollposition delta calculations
*/
private var _centeredItemIndex:int = 0;
private var _centeredItemCircumferenceBegin:Number = 0;
private var _centeredItemCircumferenceEnd:Number = 0;
private var _centeredItemDegrees:Number = 0;
/**
* The axis to tilt the 3D wheel
*/
private var _axis:Vector3D = new Vector3D(0, Math.cos(Math.PI * -90 /180), Math.sin(Math.PI * -90 /180));
/**
* @private
* Given the totalWidth, maxHeight and maxHalfWidthDiagonal, calculate the bounds of the items
* on screen. Uses the projection matrix of the layout target to calculate.
*/
private function projectBounds():Point
{
var radius:Number = _totalWidth * 0.5 / Math.PI;
// Now since we are going to arrange all the items along circle, middle of the item being the tangent point,
// we need to calculate the minimum bounding circle. It is easily calculated from the maximum width item:
var boundingRadius:Number = Math.sqrt(radius * radius + 0.25 * _itemWidth * _itemWidth);
var projectedBoundsW:Number = _axis.z * _axis.z * (_halfWidthDiagonal + 2 * radius);
var projectedBoundsH:Number = Math.abs(_axis.z) * (_halfWidthDiagonal + 2 * radius) +
_itemHeight * _axis.y * _axis.y;
return new Point(projectedBoundsW + 10, projectedBoundsH + 10);
}
/**
* @private
* Iterates through all the items, calculates the projected bounds on screen, updates _totalWidth member variable.
*/
//--------------------------------------------------------------------------
//
// Overridden methods: LayoutBase
//
//--------------------------------------------------------------------------
/**
* @private
*/
override public function set target(value:GroupBase):void
{
// Make sure that if layout is swapped out, we clean up
if (!value && target)
{
target.maintainProjectionCenter = false;
var iter:LayoutIterator = new LayoutIterator(target);
var el:ILayoutElement;
while (el = iter.nextElement())
{
el.setLayoutMatrix(new Matrix(), false /*triggerLayout*/);
}
}
super.target = value;
// Make sure we turn on projection the first time the layout
// gets assigned to the group
if (target)
target.maintainProjectionCenter = true;
}
override public function measure():void
{
var bounds:Point = projectBounds();
target.measuredWidth = bounds.x;
target.measuredHeight = bounds.y;
}
override public function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
// Get the bounds, this will also update _totalWidth
var bounds:Point = projectBounds();
// Update the content size
target.setContentSize(_totalWidth + unscaledWidth, bounds.y);
var radius:Number = _totalWidth * 0.5 / Math.PI;
var gap:Number = this.gap;
_centeredItemDegrees = Number.MAX_VALUE;
var scrollPosition:Number = target.horizontalScrollPosition;
var totalWidthSoFar:Number = 0;
// Subtract the half width of the first element from totalWidthSoFar:
var iter:LayoutIterator = new LayoutIterator(target);
var el:ILayoutElement = iter.nextElement();
if (!el)
return;
totalWidthSoFar -= el.getPreferredBoundsWidth(false /*postTransform*/) / 2;
// Set the 3D Matrix for all the elements:
iter.reset();
while (el = iter.nextElement())
{
// Size the item, no need to position it, since we'd set the computed matrix
// which defines the position.
el.setLayoutBoundsSize(NaN, NaN, false /*postTransform*/);
var elementWidth:Number = el.getLayoutBoundsWidth(false /*postTransform*/);
var elementHeight:Number = el.getLayoutBoundsHeight(false /*postTransform*/);
var degrees:Number = 360 * (totalWidthSoFar + elementWidth/2 - scrollPosition) / _totalWidth;
// Remember which item is centered, this is used during scrolling
var curDegrees:Number = degrees % 360;
if (Math.abs(curDegrees) < Math.abs(_centeredItemDegrees))
{
_centeredItemDegrees = curDegrees;
_centeredItemIndex = iter.curIndex;
_centeredItemCircumferenceBegin = totalWidthSoFar - gap;
_centeredItemCircumferenceEnd = totalWidthSoFar + elementWidth + gap;
}
// Calculate and set the 3D Matrix
var m:Matrix3D = new Matrix3D();
m.appendTranslation(-elementWidth/2, -elementHeight/2 + radius * _axis.z, -radius * _axis.y );
m.appendRotation(-degrees, _axis);
m.appendTranslation(unscaledWidth/2, unscaledHeight/2, radius * _axis.y);
el.setLayoutMatrix3D(m, false /*triggerLayout*/);
// Update the layer for a correct z-order
if (el is IVisualElement)
IVisualElement(el).depth = Math.abs( Math.floor(180 - Math.abs(degrees % 360)) );
// Move on to next item
totalWidthSoFar += elementWidth + gap;
}
}
private function scrollPositionFromCenterToNext(next:Boolean):Number
{
var iter:LayoutIterator = new LayoutIterator(target, _centeredItemIndex);
var el:ILayoutElement = next ? iter.nextElementWrapped() : iter.prevElementWrapped();
if (!el)
return 0;
var elementWidth:Number = el.getLayoutBoundsWidth(false /*postTransform*/);
var value:Number;
if (next)
{
if (_centeredItemDegrees > 0.1)
return (_centeredItemCircumferenceEnd + _centeredItemCircumferenceBegin) / 2;
value = _centeredItemCircumferenceEnd + elementWidth/2;
if (value > _totalWidth)
value -= _totalWidth;
}
else
{
if (_centeredItemDegrees < -0.1)
return (_centeredItemCircumferenceEnd + _centeredItemCircumferenceBegin) / 2;
value = _centeredItemCircumferenceBegin - elementWidth/2;
if (value < 0)
value += _totalWidth;
}
return value;
}
override protected function scrollPositionChanged():void
{
if (target)
target.invalidateDisplayList();
}
override public function getHorizontalScrollPositionDelta(scrollUnit:uint):Number
{
var g:GroupBase = target;
if (!g || g.numElements == 0)
return 0;
var value:Number;
switch (scrollUnit)
{
case NavigationUnit.LEFT:
{
value = target.horizontalScrollPosition - 30;
if (value < 0)
value += _totalWidth;
return value - target.horizontalScrollPosition;
}
case NavigationUnit.RIGHT:
{
value = target.horizontalScrollPosition + 30;
if (value > _totalWidth)
value -= _totalWidth;
return value - target.horizontalScrollPosition;
}
case NavigationUnit.PAGE_LEFT:
return scrollPositionFromCenterToNext(false) - target.horizontalScrollPosition;
case NavigationUnit.PAGE_RIGHT:
return scrollPositionFromCenterToNext(true) - target.horizontalScrollPosition;
case NavigationUnit.HOME:
return 0;
case NavigationUnit.END:
return _totalWidth;
default:
return 0;
}
}
/**
* @private
*/
override public function getScrollPositionDeltaToElement(index:int):Point
{
var layoutTarget:GroupBase = target;
if (!layoutTarget)
return null;
var gap:Number = this.gap;
var totalWidthSoFar:Number = 0;
var iter:LayoutIterator = new LayoutIterator(layoutTarget);
var el:ILayoutElement = iter.nextElement();
if (!el)
return null;
totalWidthSoFar -= el.getLayoutBoundsWidth(false /*postTransform*/) / 2;
iter.reset();
while (null != (el = iter.nextElement()) && iter.curIndex <= index)
{
var elementWidth:Number = el.getLayoutBoundsWidth(false /*postTransform*/);
totalWidthSoFar += gap + elementWidth;
}
return new Point(totalWidthSoFar - elementWidth / 2 -gap - layoutTarget.horizontalScrollPosition, 0);
}
/**
* @private
*/
override public function updateScrollRect(w:Number, h:Number):void
{
var g:GroupBase = target;
if (!g)
return;
if (clipAndEnableScrolling)
{
// Since scroll position is reflected in our 3D calculations,
// always set the top-left of the srcollRect to (0,0).
g.scrollRect = new Rectangle(0, -700, w, h);
}
else
g.scrollRect = null;
}
}
}
import spark.components.supportClasses.GroupBase;
import mx.core.ILayoutElement;
class LayoutIterator
{
private var _curIndex:int;
private var _numVisited:int = 0;
private var totalElements:int;
private var _target:GroupBase;
private var _loopIndex:int = -1;
public function get curIndex():int
{
return _curIndex;
}
public function LayoutIterator(target:GroupBase, index:int=-1):void
{
totalElements = target.numElements;
_target = target;
_curIndex = index;
}
public function nextElement():ILayoutElement
{
while (_curIndex < totalElements - 1)
{
var el:ILayoutElement = _target.getElementAt(++_curIndex);
if (el && el.includeInLayout)
{
++_numVisited;
return el;
}
}
return null;
}
public function prevElement():ILayoutElement
{
while (_curIndex > 0)
{
var el:ILayoutElement = _target.getElementAt(--_curIndex);
if (el && el.includeInLayout)
{
++_numVisited;
return el;
}
}
return null;
}
public function nextElementWrapped():ILayoutElement
{
if (_loopIndex == -1)
_loopIndex = _curIndex;
else if (_loopIndex == _curIndex)
return null;
var el:ILayoutElement = nextElement();
if (el)
return el;
else if (_curIndex == totalElements - 1)
_curIndex = -1;
return nextElement();
}
public function prevElementWrapped():ILayoutElement
{
if (_loopIndex == -1)
_loopIndex = _curIndex;
else if (_loopIndex == _curIndex)
return null;
var el:ILayoutElement = prevElement();
if (el)
return el;
else if (_curIndex == 0)
_curIndex = totalElements;
return prevElement();
}
public function reset():void
{
_curIndex = -1;
_numVisited = 0;
_loopIndex = -1;
}
public function get numVisited():int
{
return _numVisited;
}
}
Initial URL
Initial Description
Taken from Evtim's source - http://evtimmy.com
Initial Title
Custom Wheel Layout
Initial Tags
Initial Language
ActionScript