Return to Snippet

Revision: 50430
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