blob: 1ce4bb6628e8f091784fb26fb680a2266c457e89 [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
{
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;
public class WheelLayout extends LayoutBase
{
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
public function WheelLayout()
{
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;
/**
* @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, 1, 0.1);
/**
* The angle to tilt the axis of the wheel
*/
public function set axisAngle(value:Number):void
{
_axis = new Vector3D(0, Math.cos(Math.PI * value /180), Math.sin(Math.PI * value /180));
var layoutTarget:GroupBase = target;
if (layoutTarget)
{
layoutTarget.invalidateSize();
layoutTarget.invalidateDisplayList();
}
}
/**
* @private
* Given the radius of the sphere, return the radius of the
* projected sphere. Uses the projection matrix of the
* layout target to calculate.
*/
private function projectSphere(radius:Number, radius1:Number):Number
{
var fl:Number = target.transform.perspectiveProjection.focalLength;
var alpha:Number = Math.asin( radius1 / (radius + fl) );
return fl * Math.tan(alpha) * 2;
}
/**
* @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(totalWidth:Number, maxWidth:Number, maxHeight:Number, maxHalfWidthDiagonal:Number):Point
{
// Use the the total width as a circumference of an imaginary circle which we will use to
// align the items in 3D:
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 * maxWidth * maxWidth);
var projectedBoundsW:Number = _axis.z * _axis.z * (maxHalfWidthDiagonal + 2 * radius) +
projectSphere(radius, boundingRadius ) * _axis.y * _axis.y;
var projectedBoundsH:Number = Math.abs(_axis.z) * (maxHalfWidthDiagonal + 2 * radius) +
maxHeight * _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.
*/
private function calculateBounds():Point
{
// Calculate total width:
_totalWidth = 0;
var maxHeight:Number = 0;
var maxWidth:Number = 0;
var maxD:Number = 0;
// Add up all the widths
var iter:LayoutIterator = new LayoutIterator(target);
var el:ILayoutElement;
while (el = iter.nextElement())
{
var preferredWidth:Number = el.getPreferredBoundsWidth(false /*postTransform*/);
var preferredHeight:Number = el.getPreferredBoundsHeight(false /*postTransform*/);
// Add up item width
_totalWidth += preferredWidth;
// Max up item size
maxWidth = Math.max(maxWidth, preferredWidth);
maxHeight = Math.max(maxHeight, preferredHeight);
maxD = Math.max(maxD, Math.sqrt(preferredWidth * preferredWidth / 4 +
preferredHeight * preferredHeight));
}
// Add up the gap
_totalWidth += gap * iter.numVisited;
// Project
return projectBounds(_totalWidth, maxWidth, maxHeight, maxD);
}
//--------------------------------------------------------------------------
//
// 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 = calculateBounds();
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 = calculateBounds();
// 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(navigationUnit:uint):Number
{
var g:GroupBase = target;
if (!g || g.numElements == 0)
return 0;
var value:Number;
switch (navigationUnit)
{
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, verticalScrollPosition, w, h);
}
else
g.scrollRect = null;
}
}
}
import mx.core.ILayoutElement;
import spark.components.supportClasses.GroupBase;
class LayoutIterator
{
private var _curIndex:int;
private var _numVisited:int = 0;
private var totalElements:int;
private var _target:GroupBase;
private var _loopIndex:int = -1;
private var _useVirtual:Boolean;
public function get curIndex():int
{
return _curIndex;
}
public function LayoutIterator(target:GroupBase, index:int=-1):void
{
totalElements = target.numElements;
_target = target;
_curIndex = index;
_useVirtual = _target.layout.useVirtualLayout;
}
public function nextElement():ILayoutElement
{
while (_curIndex < totalElements - 1)
{
var el:ILayoutElement = _useVirtual ? _target.getVirtualElementAt(++_curIndex) :
_target.getElementAt(++_curIndex);
if (el && el.includeInLayout)
{
++_numVisited;
return el;
}
}
return null;
}
public function prevElement():ILayoutElement
{
while (_curIndex > 0)
{
var el:ILayoutElement = _useVirtual ? _target.getVirtualElementAt(--_curIndex) :
_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;
}
}