그동안 정신 없이 바쁘다가 이제조금 정신을 차리고 다시 진행하려고 한다.ㅠ.ㅠ
(잠이 부족해ㅠ.ㅠ.)
다시 마음을 잡고....
분석 5에 있던... 유명한 아저씨의 소스를 분석 해보기로 했당... 잘 될랑가 모르겠다...
아래 이 소스...ㅋㅋ
원본 소스 : DisplayShelf.zip
일단 파일 구조는 : MXML Application 파일 한개와 AS 파일 2개로 구성되어 있고 img 디렉토리 밑에 관련이미지가 들어있다
이미지를 잘 보면알겠지만 이미지를 전부 400 * 400 으로 편집해 놓았다. 왜 그랬는지는 화면 보면 뻔히 알수 있는거고
하지만 이미지 파일을 올리다 보면 가로로 긴것도 있을거고 세로로 긴것도 있을것이다. 그러니 파일을 업로드 할때부터 썸네일을 만들던 해서 이미지 보정을 해야될거 같다.. 보여지는것은 멋져보이지만 뒤에서는 열심히 삽질을 하고 있다는..ㅋㅋ
구조 : Final.mxml <- MXML Application file
: DisplayShelf.as <- AS file
: TiltingPane.as <- AS File
: /img <- IMG Directory
이분이 친절하게 주석을 달아주셨다.. 그런데 영어를 거의 못하기 때문에.. 무슨 소리인지 하나도 모르겠당..ㅎㅎ
그래서 내 맘대로 다시 분석 시작..ㅋㅋ 그런데 주석을 참고 하기는 해야지..ㅎㅎ
파일
1. Final.mxml
<?xml version="1.0" encoding="utf-8"?>
<Application xmlns="http://www.adobe.com/2006/mxml" xmlns:local="*"
height="100%" width="100%" layout="absolute">
<!--
Binding 이놈은 컴포너트와 데이터간의 상호 연결을 할수 있다. 그런데 한쪽 방향으로만 적용이 된다는점.. source-> destincation으로만.
source와 destination으로 구성되는데 source에 선언된 변수가 바뀔때 destination의 값도 같이 변경시킨다.
그러나 destincation의 값이 변화되었다고 해서 source의 값이 변경되는것은 아니다.
-->
<Binding source="sel.value" destination="shelf.selectedIndex"/>
<Binding source="shelf.selectedIndex" destination="sel.value"/>
<Binding source="angle.value" destination="shelf.angle"/>
<Binding source="pop.value" destination="shelf.popout"/>
<!--
Data를 이미 Array 구조에다가 넣어 놓았다. 이 배열이 DataProvider와 연결이된다.
추후 실 데이타를 사용할경우 HTTPService와 같은것으로 연결하면 될거 같다.
-->
<Array id="dataSet">
<String>img/photos400/photo01.jpg</String>
<String>img/photos400/photo02.jpg</String>
<String>img/photos400/photo03.jpg</String>
<String>img/photos400/photo04.jpg</String>
<String>img/photos400/photo05.jpg</String>
<String>img/photos400/photo06.jpg</String>
<String>img/photos400/photo07.jpg</String>
<String>img/photos400/photo08.jpg</String>
<String>img/photos400/photo09.jpg</String>
<String>img/photos400/photo10.jpg</String>
<String>img/photos400/photo11.jpg</String>
<String>img/photos400/photo12.jpg</String>
<String>img/photos400/photo13.jpg</String>
<String>img/photos400/photo14.jpg</String>
<String>img/photos400/photo15.jpg</String>
<String>img/photos400/photo16.jpg</String>
<String>img/photos400/photo17.jpg</String>
<String>img/photos400/photo18.jpg</String>
<String>img/photos400/photo19.jpg</String>
</Array>
<local:DisplayShelf id="shelf" horizontalCenter="0" verticalCenter="0"
borderThickness="10" borderColor="#FFFFFF" dataProvider="{dataSet}"
enableHistory="false" width="100%"/>
<!--
슬라이더별 기능
id값별
angle : 메인 이미지 외에 주변에 나타나 있는 이미지들의 각도
sel : 메인에 보여줄 이미지 선택
pop : 메인 이미지 외에 주변에 나타나 있는 이미지들의 크기
-->
<VBox horizontalCenter="0" bottom="10" horizontalAlign="center" verticalAlign="middle">
<HBox>
<Label text="Angle:" />
<!--
HSlider의 간단한 설명
id = 슬라이더의 id값
liveDragging = 슬라이더의 드래그를 할지 말지 설정하는것 true : 드래그 가능
minimum = 슬라이더 컨트롤의 최소치
maximum = 슬라이더 컨트롤의 최대
value = 슬라이더 컨트롤의 기본값
snapInterval = 슬라이더 컨트롤의 증가 값 , .1은 0.1씩 증가
width = 슬라이더의 width값
-->
<HSlider liveDragging="true" id="angle" minimum="5" value="35" maximum="90" snapInterval=".1" width="400" />
</HBox>
<HBox>
<Label text="Selection:" />
<HSlider liveDragging="true" id="sel" minimum="0" value="0" maximum="{shelf.dataProvider.length}" snapInterval="1" width="400" />
</HBox>
<HBox>
<Label text="pop:" />
<HSlider liveDragging="true" id="pop" minimum="0" value=".43" maximum="1" snapInterval=".01" width="400" />
</HBox>
</VBox>
</Application>
2. DisplayShelf.as
package
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.events.TimerEvent;
import flash.filters.DropShadowFilter;
import flash.geom.Matrix;
import flash.ui.Keyboard;
import flash.utils.Dictionary;
import flash.utils.Timer;
import mx.collections.ArrayCollection;
import mx.collections.ICollectionView;
import mx.collections.IList;
import mx.collections.XMLListCollection;
import mx.controls.Image;
import mx.core.ClassFactory;
import mx.core.IDataRenderer;
import mx.core.IFactory;
import mx.core.UIComponent;
import mx.effects.AnimateProperty;
import mx.effects.easing.Quadratic;
import mx.events.CollectionEvent;
import mx.managers.HistoryManager;
import mx.managers.IFocusManagerComponent;
import mx.managers.IHistoryManagerClient;
// defining styles on the DisplayShelf. By defining these styles here in metadata, developers will be allowed
// to specify values for these styles as attributes on the MXML tag. Note that this component doesn't actually
// use these styles...instead, the TiltingTiles it contains use them. But this component assigns _itself_ as the
// stylename for those TiltingTile instances. That makes the tiltingTile inherit all the style values defined on this component.
// Thus by defining the styles on this component, we are automatically passing them through to the contained subcomponent.
// this is a common practice for aggregating subcomponents.
[Style(name="borderThickness", type="Number")]
[Style(name="borderColor", type="Number")]
// defining the change event. This event is dispatched whenever the selectedIndex of this component changes. By declaring it
// here, in metadata, we allow developers to specify a change handler on our MXML tag.
[Event("change")]
// defining the default property. By declaring dataProvider as our defaultProperty, we are allowing developers to specify the value of
// default property as the content of the DisplayShelf tag, without having to explciitly call it out as the value for defaultProperty.
[DefaultProperty("dataProvider")]
/* our custom component. Note a few things:
/ 1. we're extending UIComponent, not Canvas or some other Container. It's a common misconception that if you're going to
/ have children, you must extend Container. Not True. Extend container if you want to do what containers do...namely, aggregate children
/ specified in MXML...if you want easy access to a container's predefined layout algorithm...or if you want scrolling and clipping capabilities
/ out of the box. Otherwise, using UIComponent as your base class is much simpler. All UIComponents can contain children for implementation purposes.
/ 2. We're implementing the IHistoryManagerClient interface. This allows us to save off our state whenever someone tells the history manager to save.
/ we're making this component behave like the navigator classes...optionally, you can have the back button navigate back to previous selections of this component.
/ 3. We're implementing IFocusManager component. We do that to let the Focus Manager know that we want to accept focus and keyboard events. All of the functionality
/ to do this is already supported in UICompoent, our base class...all we need to do is add this 'marker' interface, and override the keyDownHandler method to add our
/ logic to interpret keystrokes.
*/
public class DisplayShelf extends UIComponent implements IHistoryManagerClient, IFocusManagerComponent
{
//---------------------------------------------------------------------------------------
// constants
//---------------------------------------------------------------------------------------
// how far, in pixels, each child will overlap when stacked sideways. This probably should be a percentage of the size of the children...i.e., overlap 1/5th...but
// we're taking a shortcut here by defining it in pixels.
private const kPaneOverlap:Number = 40;
//---------------------------------------------------------------------------------------
// private state
//---------------------------------------------------------------------------------------
// how far our selected item should 'pop' in front of the non-selected items. We'll use this value to compute a scale-down factor for
// the non-selected items.
private var _popout:Number = .43;
// storage for our data provider property. Note that we're requiring all of our dataproviders to implement the IList interface. We could have
// chosen Arrays, but then we wouldn't be able to detect when the developer added or removed items from the list. We also could have chosen
// ICollectionView, but that's a heavier interface that requires us to use cursors...something we don't really need to do. IList provides
// a nice compromise between functionality and simplicity. Note that all of the collection classes...ArrayCollection and XMLListCollection...defined
// by the framework implement the IList interface.
private var _dataProvider:IList;
// a flag to let us know when our children are dirty. We're going to be putting our children creation logic in our commitProperties function. Often
// there's more than one set of update logic that goes into commitProperties, so it's nice to store an extra flag to let you know whether a particular
// bit of updateLogic needs to be run. We'll set this flag when anything changes that requires us to regenerate our children.
private var _itemsDirty:Boolean = true;
// our array of children. These are the TiltingTiles that we'll generate, one for each item in the dataProvider.
private var _children:Array = [];
// the tilt angle for the non-selected children. This can be set by the developer.
private var _angle:Number = 35;
// the current selected index, as set by the developer.
private var _selectedIndex:Number = 0;
// the index (or rather, value, since it can be fractional) of the item at the center of our list as currently displayed. Since we animate from
// selected index to selected index when it changes, our 'current' position is different from the 'selected' position. By keeping track of this
// value, we can make sure that when we draw we're always drawing the 'current' index as it animates towards the selected index.
private var _currentPosition:Number = 0;
// a map that allows us to use an itemRenderer (actually, a tiltingTile) as a key to map back to the index of the it represents. We'll use this when
// the user clicks on one of the tilting tiles to decide what our new selected index is. We _could_ just iterate over our children list to
// find the index on click, but there are lots of use cases where you need to store extra data about an itemRenderer that can't be easily looked up.
// in those cases, Dictionaries are really useful tools. So we'll use one here just as a demonstration.
private var _itemIndexMap:Dictionary;
// a flag to control whether we want to automatically enable history management when the selected index changes. This way the component
// can be used in scenarios where it doesn't represent a 'location' to the user.
private var _enableHistory:Boolean = false;
// these are structures we'll need temporarily when calculating layout. Rather than allocating them again and again on update, we'll just allocate them
// once and hold on to them.
private var lCP:ChildPosition = new ChildPosition();
private var rCP:ChildPosition = new ChildPosition();
// the selected index, clamped to the range defined by the dataProvider. We store this separate from the actual selected index as assigned by the developer.
// we want to calculate it only once and then store it off. But if we stored it back into our selected index property, we'd need to worry about scenarios where
// the selectedIndex gets assigned before the dataprovider does. So we store it in a separate variable, so as not to trample the 'true' selected index.
private var _safeSelectedIndex:Number;
// storage for the item renderer factory, that will generate item renderer interfaces for us as necessary.
private var _itemRenderer:IFactory;
// the effect we'll use to animate from old to new selected index. If the user changes selected index in the middle of an animation, we'll want to cancel
// the old one, so we keep a reference to it.
private var _animation:AnimateProperty;
// whether or not we should automatically select a child when the user clicks on a particular item. It's generally good practice to avoid hard coding UI gestures
// into your component if you can avoid it...if possible, a good component will provide a default UI gesture, a way to disable it, and a programmatic way to
// build an alternate UI gesture. In this case, by default we select an item on click, we allow the developer to turn that off, and we allow the developer to
// set the selectedIndex programmatically so they can select on, say mouse over.
private var _selectOnClick:Boolean = true;
//---------------------------------------------------------------------------------------
// constructor
//---------------------------------------------------------------------------------------
public function DisplayShelf()
{
super();
// define a default empty dataprovider. Rather than deal with this property being null, it's easiest to always
// assume there's something, and substitute empty 'somethings' for null dataproviders.
dataProvider = new ArrayCollection();
// we register with the history manager to let it know that we will want to save state whenever someone tells the history manager to remember
// the current state of the application.
HistoryManager.register(this);
_itemIndexMap = new Dictionary(true);
// set up a default item renderer. We could require the developer to always specify one, but if we've got an 80% use case, it's nice to define
// a default one. Note that this does force the compiler to link in the Image class, even if the developer turns around and redefines the itemRenderer
// property, so there is a potential price to pay in application size. Chances are pretty good the developer is using Image somewhere though.
_itemRenderer = new ClassFactory(Image);
}
//---------------------------------------------------------------------------------------
// public properties
//---------------------------------------------------------------------------------------
/* True if the developer wants us to automatically save changes to the selectedIndex in the history manager or not.
*/
public function set enableHistory(value:Boolean):void
{
_enableHistory = value;
}
public function get enableHistory():Boolean
{
return _enableHistory;
}
/* How far out the selected item should 'pop' from the background items. A value of 0 doesn't pop it out at all, while a value of 1 will receed
* the background items infinitely to the horizon. Basically, the value is inverted and used as a scale factor for the background items.
* pick something appropriate.
* FWIW, now that I look at this, it really should be a style, not a property
*/
[Bindable] public function set popout(value:Number):void
{
_popout = value;
/* Being a good flex component, we don't want to recalculate every time someone changes this value. Instead, we store the change,
* and invalidate so we'll get to redrew the next time the screen is going to be updated.
*/
invalidateDisplayList();
}
public function get popout():Number
{
return _popout;
}
/* the index of the currently selected item in the dataProvider. Note that since this component animates its position, this is not necessarily the
* same as the item we are currently looking at. We might be in the middle of animating towards the selected item.
* note that since we are going to dispatch a well defined, named event when this value changes, we specify that
* event in the binding metadata. That let's flex know that we're going to be reponsible for dispatching the event ourselves.
* Otherwise the binding metadata would result in _another_ event being dispatched, which would be wasteful.
*/
[Bindable("change")]
public function set selectedIndex(value:Number):void
{
// save time and performancing by doing nothing if the selected index is already the new value.
if(_selectedIndex == value)
return;
// store off the new value.
_selectedIndex = value;
// since we are going to use this value to index into the item renderers, we want to make sure we don't use a value outside the range of
// existing renderers. Rather than having to liter our code with those checks all over the place, we'll clamp it to the legal range once now
// and store off the 'safe' value.
_safeSelectedIndex = Math.max(0,Math.min(_selectedIndex,_children.length-1));
// dispatch an event letting listeners know that
dispatchEvent(new Event("change"));
// when the selected index changes, we'll want to kick-start our animation.
startAnimation();
// tell the history manager that something significant to the history has changed.
if(_enableHistory)
HistoryManager.save();
}
public function get selectedIndex():Number
{
return _selectedIndex;
}
/* This property represents the current position in the child items that our component is looking at.
* All of our rendering is done off of this property. By exposing it as a public property, we can animate
* it, which means that even though we're incorporating animation, our rendering will always be in sync
* with the internal state of our application
*/
public function set currentPosition(value:Number):void
{
_currentPosition = value;
invalidateDisplayList();
}
public function get currentPosition():Number
{
return _currentPosition;
}
/* where we're getting our data from. We're going to follow the flex SDK convention of leaving our dataProvider property
* untyped, and automatically wrapping raw Arrays and XMLLists as a convenience. For this component, we're going to require
* that our dataProvider either implement the IList interface, or be something we can convert into an IList implementation.
*/
[Bindable] public function set dataProvider(value:Object):void
{
/* first, if we have a previous dataProvider, we're going to want to remove any event listeners from it
*/
if(_dataProvider != null)
{
_dataProvider.removeEventListener(CollectionEvent.COLLECTION_CHANGE,dataChangeHandler,false);
}
/* Now, as a convenience to the caller, convert our dataProvider into an IList implementation */
if (value is Array)
{
_dataProvider = new ArrayCollection(value as Array);
}
else if (value is IList)
{
_dataProvider = IList(value);
}
else if (value is XMLList)
{
_dataProvider = new XMLListCollection(value as XMLList);
}
/* Add an event listener so we know and can react when our dataProvider changes. Note that the convention in flex is that
* list-like components are only responsible for detecting and reacting to changes in the list itself, _not_ changes
* to the properties of the items themselves. It's the responsibility of the item renderers to do that as necessary.
*
* Also, note that we're using a weak listener here. Since the data provider is being passed in by an external caller,
* we don't know what the lifetime of the dataProvider is w/relation to our lifetime. Since we don't have a constructor,
* we won't ever get a chance to remove our listener. So we use a weak listener to make sure we don't get locked into
* memory by this.
*/
_dataProvider.addEventListener(CollectionEvent.COLLECTION_CHANGE,dataChangeHandler,false,0,true);
/* since we now need to re-allocate our item renderers, we'll set a flag and invalidate our properties. As with layout and size,
* by putting the item renderer generation into commitProperties, we avoid having to run it too often
*/
_itemsDirty = true;
invalidateProperties();
/* Our measured size is dependent on our number and size of items in the dataProvider, so we need to invalidate it here*/
invalidateSize();
}
public function get dataProvider():Object
{
return _dataProvider;
}
/* The UIComponent that we'll use to render our items. Since we need to create multiple of these...one for each item in the
* dataprovider...we need not an itemRenderer, but a _factory_ that can create itemRenderers on demand. That's why we type
* this property as an IFactory. IFactory is a special interface that signals to the compiler that we need an object that implements
* the factory pattern. When the MXML compiler sees a property of type IFactory, it allows the developer specify it's value in one
* of three ways:
* By specifying an object that implements the IFactory interface (that's normal).
* By specifying the name of a class...it automatically wraps the class in an instance of ClassFactory and assigns that to the property.
* By defining a component inline via <mx:Component>...it defines an implicit class, wraps it in a ClassFactory instance, and assigns that.
*/
public function set itemRenderer(value:IFactory):void
{
_itemRenderer = value;
/* store off the value, and set the flag to say that we need to re-generate all of our item renderers*/
_itemsDirty = true;
invalidateProperties();
invalidateSize();
}
public function get itemRenderer():IFactory
{
return _itemRenderer;
}
/* The angle of the background non-selected items*/
public function set angle(value:Number):void
{
_angle = value;
invalidateDisplayList();
}
public function get angle():Number
{
return _angle;
}
/* whether or not we should automatically select a child when the user clicks on a particular item. It's generally good practice to avoid hard coding UI gestures
* into your component if you can avoid it...if possible, a good component will provide a default UI gesture, a way to disable it, and a programmatic way to
* build an alternate UI gesture. In this case, by default we select an item on click, we allow the developer to turn that off, and we allow the developer to
* set the selectedIndex programmatically so they can select on, say mouse over.
*/
public function set selectOnClick(value:Boolean):void
{
_selectOnClick = value;
}
public function get selectOnClick():Boolean
{
return _selectOnClick;
}
//---------------------------------------------------------------------------------------
// property management
//---------------------------------------------------------------------------------------
/* this is the standard function where components put performance intensive computations and side-effects
* from changes to their properties. By calling invalidateProperties(), a component guarantees that this function
* will get called by the layout manager before the next time the screen is going to be updated, before their
* measure() or updateDisplayList() functions are called (if necessary). Note that there is no guarantee about the
* order in which commitProperties is called from component to component (i.e., it's not parent before child or vice versa).
*/
override protected function commitProperties():void
{
/* as components get more and more complicated, this function often grows to do more and more processing.
* as a performance optimization, it's usually good to put guards around different computations to make sure
* you're only re-calculating what you need to on any given pass. In this case, we've defined a flag to let us
* know when something has changed that requires us to regenerate our item renderers.
*
* When a component creates children/sub-components, there's generally two places it should consider doing it.
* For 'static' sub components, that don't come and go as the component is used, it's best to create them in the
* createChildren() function. But for children that are created and destroyed as the component is used,
* it's best to muck with them in the commitProperties() function. Adding children to a component automatically
* invalidates its size and display. Since commit properties runs before measure() and updateDisplayList() runs,
* adding children here won't accidentally trigger _another_ validation pass, which would happen if you tried to create them
* in updateDisplayList().
*/
if(_itemsDirty)
{
_itemsDirty = false;
/* we're going to create an item renderer for each item in the data provider */
/* first, let's clear out our old item renderers. Now this is horribly inefficient...if, say, the
* developer just added a single item to the data provider, there's no reason we need to throw all the
* old ones away. But we're going to do it for simplicity's sake here. In your code, be more efficient ;)
*/
for(i = numChildren-1;i>=0;i--)
{
removeChildAt(numChildren-1);
}
/* clear out our children list and child -> index map, since we just threw away all of our children */
_itemIndexMap = new Dictionary(true);
_children = [];
for(var i:int = 0;i<_dataProvider.length;i++)
{
/* first, create a tilting Tile for the item, since that's going to give us our 3D effect */
var t:TiltingPane = new TiltingPane();
/* put an entry in our dictionary mapping our tilting tile to its index in the dataProvider.
* When the user clicks on one of our tilting tile, we'll use this map to figure out the index
* of the item they just clicked on, and hence what our new selected index should be */
_itemIndexMap[t] = i;
/* add a click event handler to our tiltingPane, so we can automatically update the selected index.
* note that we're again using weak references here for our event listener. In this case, since these are
* entirely self contained objects, we don't actually need to use a weak listener here. But we're a bit lazy,
* and since we know that we're not going to run into any of the pitfalls of weak references, we go ahead
* and use them anyway. Alternatively, we could have been explicit about removing the listener when we
* removed the tilting panes later on.
*/
t.addEventListener(MouseEvent.CLICK,itemClickHandler,false,0,true);
/* set the tiltingTile's styleName to us, the parent componment. This is common practice for styling sub-components
* of a parent component. By doing this, the TiltingTile inherits _all_ of our styles...not just the inheriting ones...
* which allows us to easily facade style values from the children up through us for our component developers to specify in CSS.
*/
t.styleName = this;
/* add the tilting tile to our array of children*/
_children[i] = t;
/* Now it's time to use our itemRenderer factory. We've created a TiltingTile for our item, but our TiltingTile needs to
* know exactly what it is that it's going to be tilting. To do that, we ask our itemRenderer factory to create an instance for us.
*/
var content:UIComponent = UIComponent(_itemRenderer.newInstance());
/* of course, in order to render our data, the itemRenderer instance needs to know what it's going to be rendering.
* In flex, things that render data implement the IDataRenderer interface. Since we can't imagine someone using this component
* in a way that didn't require the individual item renderers to know what data they're rendering, we're going to go ahead
* and assume that our new item renderer instance implements the IDataRenderer interface. So we'll use it to assign the nth item
* out of the dataProvider to our nth item renderer instance.
*/
IDataRenderer(content).data = _dataProvider.getItemAt(i);
/* OK, we've got an item renderer instance that now owns an item from the dataProvider. We'll put that in our tilting tile,
* and add the tilting tile as a child.
*/
t.content = content;
addChildAt(t,0);
}
}
/* since the size of our dataProvider might have just changed, we'll revalidate our selected index to make sure it's
* a valid index into the data.
*/
_safeSelectedIndex = Math.max(0,Math.min(_selectedIndex,_children.length-1));
/* since we've just recalculated our state, chances are pretty good we need to re-render ourselves now.
*/
invalidateDisplayList();
}
//---------------------------------------------------------------------------------------
// measurement
//---------------------------------------------------------------------------------------
/* this is our measurement function. A component's measure() routine is where it should calculate what it's 'natural' size should be...
* i.e., how big it would like to be if the developer doesn't assign it an explicit size. The layout manager calls this function whenever
* it thinks your component needs to remeasure itself. That happens under a number of circumstances. a) some state that your measured size
* uses in calculation changes, so your component explicitly calls invalidateSize() (i.e., see the set angle() function). b) the measured or explicit
* of one of your children changes size...the layout manager assumes that your measured size relies on the size of your children, so it will ask you to
* re-measure. Note that according to the conventions of the SDK, your measured size is generally ignored if you have an explicit size set. That means
* that the layout manager might optimize by not calling your measure() routine if you have an explicit size. So don't do any calculation in here that
* _must_ happen for your component to function properly.
*/
override protected function measure():void
{
var mHeight:Number = 0;
var mWidth:Number = 0;
var t:TiltingPane;
/* Since each child could potentially be at the middle of the component at its natural size with the other children stacked along side it, we need to
* look at each child to calculate our measured size.
* So for each child:
*/
for(var i:int = 0;i<_children.length;i++)
{
t = _children[i];
/* our measured size will just be the largest measured size of all of our children, to make
* sure we can correctly render all of them*/
mHeight = Math.max(t.measuredHeight,mHeight);
/* our measured width, however, is more complicated. For each child, we want to calculate how big we need to be if that child was selected.
* That's the size of the child plus the amount of space we need to stack the other children in the background. Now since each child has a different
* number of children to the left and right, that will be different for each child. But we want the child to stay in the middle, so we need it to be
* symmetrical. So our calculation is: figure out how many children go to the left and right of child N. Take the maximum of that, figure out how
* much space we need to stack those children in the background, and double that, since we want the same amount of space on each side. Now add the measured
* width of child N, since it's going to be at the middle. That's how much space we need for Child N. Now for all children, calculate that, and take the
* largest number we find
*/
mWidth = Math.max(mWidth, t.measuredWidth + Math.max(i,_children.length - i - 1) * kPaneOverlap * 2);
}
/* store off our measured sizes. If we were really being good, we'd probably calculate a minimum size too. But since our component doesn't adjust its layout
* to match its size, we don't really have a minimum size. i.e., if we wanted to we could squeeze the overlapping children in the background together if we
* didn't have enough space. If that were the case, our minimum size would be the same calculation above, but for whatever we consider our 'minimum' overlap
* to be. I'll leave that as an exercise for the reader
*/
measuredHeight = mHeight;
measuredWidth = mWidth;
}
//---------------------------------------------------------------------------------------
// layout
//---------------------------------------------------------------------------------------
/* this is our main function that does all of our layout and rendering. The LayoutManager takes care of making sure this function gets called
* right before the screen is updated if our component needs updating. We tell the layout manager that we need updating by calling
* invalidateDisplayList(). This automatically happens whenever our component changes size.
*/
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
/* often list-like components need to do things like check values of the first child as a starting point for calculations.
* that assumes we have a first child, and can RTE if we're empty. Rather than putting guards all over the place, we'll just
* bail out here if we have no children at all
*/
if(_children.length == 0)
return;
/* ChildPosition is a simple value structure we use to pass around the calculated position and angle of a single child. It's defined at
* the end of this file.
*/
var c:ChildPosition = new ChildPosition();
var t:TiltingPane;
var m:Matrix;
/* for each child to the left of our currently selected child...
*/
for(var i:int = 0;i<=_currentPosition;i++)
{
/* calculate where it should be based on our currently focused position. This function is defined below.
*/
calcPositionForSelection(i,_currentPosition,c);
t = _children[i];
/* and put it there. Note that the first thing we do is set the size of the child to its requested (measured or explicit size).
* in Flex, it's always that parent's responsibility to tell a child what size it should be. If we didn't do this, the child
* would just sit at size 0, even if we or someone else explciitly set the width/height properties.
* In this case, we're not doing any resizing of the children. So we just want them to be their measured size, or their explicit
* size if someone has set an explicit size on them. This is such a common if/or calculation that UIComponents have a convenience function
* defined called 'getExplciitOrMeasuredWidth/Height().'
* Note that we use the setActualSize function. Components sizing their children should _always_ use this function. It serves two purposes:
* first, it differentiates between the _explicit_ size that might be set by the developer, and the 'current actual' size that the parent wants
* the child to be. Since explicit size probably is used in a parent's computed measurement and layout algorithms, we don't want to confuse
* the inputs to those algorithms with the output (the actual size). Second, since explicit size of a child is usually an input to the parent's
* measurement and layout, any time they change the parent needs to be invalidated and re-layout. So if we accidentally set the explicit size here,
* it would trigger another invalidation and layout, and potentially an infinite layout loop. Instead, setting the actual size doesn't invalidate
* the parent (since it assumes child actual size is _not_ an input to the parent's measurement/layout algorithm).
*/
t.setActualSize(t.getExplicitOrMeasuredWidth(),t.getExplicitOrMeasuredHeight());
/* we need to set the angle of the child here. Now TiltingTiles use their angle as an input to their measured size. And as with all UICompoennts,
* their measured size is an input to their parent's measurement and layout algorithms. So if we weren't careful, setting their angle here would
* force another measurement/layout on our component, causing an infinite loop. So as with size, we need to differentiate between the explicit
* angle of the TiltingTile, and the 'acutal' angle as assigned by the parent (us).
*/
t.setActualAngle(c.angle);
/* we want the children on the left to stack up from left to right. Children with higher indexes go above children with lower indexes, so we'll
* set the child index here to get it stacking correctly
*/
setChildIndex(t,i);
/* Lastly, we'll set the scale of the tilting tile. Selected children are displayed at full scale, while background children are scaled down a bit
* to make it look like they're receeding into the distance. Now scale is _also_ an input into the measured size of a child (set its scale to 2X,
* and its measured size doubles) so we have the same problem with scale as we do with sizing and angle above. Flex unfortunately doesn't differentiate
* between explicit and actual scale. But we can work around it by manipulating the child component's scale factors in its matrix, which shortcuts
* the part of the framework that causes the invalidate and potentially the infinite loop. This is a hack, one we hopefully won't need in an upcoming release
* of the SDK.
*/
m = t.transform.matrix;
/* assign the scale */
m.a = m.d = c.scale;
/* and assign the matrix back to the item. Matrices are copy on access...meaning when you ask for the matrix of an object, you get a copy of it.
* So our changes won't affect the object until we assign it back to the child as its transform matrix
*/
t.transform.matrix = m;
/* set its location */
t.move(c.x,c.y);
}
/* this is exactly the same logic as the previous loop, except that we want to stack each child on the right hide side _below_ the previous child.
*/
for(i = Math.floor(_currentPosition)+1; i< _children.length;i++)
{
calcPositionForSelection(i,_currentPosition,c);
t = _children[i];
t.setActualSize(t.getExplicitOrMeasuredWidth(),t.getExplicitOrMeasuredHeight());
t.setActualAngle(c.angle);
t.move(c.x,c.y);
/* each time we move to the next child, we set its index to 0. This bumps all previous children up one level, and puts this child at the bottom,
* ensuring it ends up below the child to its left
*/
setChildIndex(t,0);
m = t.transform.matrix;
m.a = m.d = c.scale;
t.transform.matrix = m;
}
/* lastly, we make sure the currently selected child is on top
*/
setChildIndex(_children[Math.round(_currentPosition)],numChildren-1);
}
/* this function calculates the scale, angle, and position a child should be given a particular
* selected position. Since we animated our 'currentPosition', we need to be able to calculate
* selected position for any real positive number. To do that, we calculate two different positions
* and average them out. If, for example, the current position was 3.7, we'll calculate the values
* for a currentPosition of 3, and a currentPosition of 4, and average .7 of the first and .3 of the second
*/
private function calcPositionForSelection(i:Number,sel:Number,c:ChildPosition):void
{
var delta:Number = sel - Math.floor(sel);
/* if sel is already an integer, we just calculate our position for that integer, and return
*/
if(delta == 0)
{
calcPositionForIndexSelection(i,sel,c);
return;
}
/* otherwise, calculate our position for the previous and next integers
*/
calcPositionForIndexSelection(i,sel-delta,lCP);
calcPositionForIndexSelection(i,sel-delta+1,rCP);
/* and compute a weighted average
*/
c.angle = lCP.angle + delta * (rCP.angle - lCP.angle);
c.scale = lCP.scale + delta * (rCP.scale - lCP.scale);
c.x = lCP.x + delta * (rCP.x - lCP.x);
c.y = lCP.y + delta * (rCP.y - lCP.y);
}
/* this function calculates the position for a given child assuming our currentPosition value is 'sel.'
* unlike the previous funciton, this one assumes that sel is an integer.
*/
private function calcPositionForIndexSelection(i:Number,sel:Number,c:ChildPosition):void
{
var t:TiltingPane = _children[i];
var selected:TiltingPane = _children[sel];
var adjacent:TiltingPane;
var a:Number = _angle;
if(i == sel)
{
/* if the item we're calculating the position for _is_ the selected item,
* then we know exactly where it goes...smack dab in the middle, at full size, full scale,
* with an angle of 0.
*/
c.scale = 1;
c.x = unscaledWidth/2 - t.getExplicitOrMeasuredWidth()/2;
c.y = unscaledHeight/2 - t.getExplicitOrMeasuredHeight()/2;
c.angle = 0;
}
else if (i < sel)
{
/* otherwise, if it's to the left of the selected item,
* we want to scale it down to make it look like it's receding into the background...
*/
c.scale = (1-_popout);
/* tilt it in towards the selected item */
c.angle = _angle;
/* and push it off to the left. To do that, we need to calculate it's position. Most of the children to the left just go a fixed distance
* from the child to its right, and so we don't care about their actual size. But the first child immediately to the left of the selected child
* is mostly visible, so we need to position it so that only a little bit overlaps. Which means we need to know it's size.
* so first, let's calculate the position of that first child to the left.
* that's going to be the left edge of the selected item, minus approximately 8/10th of the widths of the next item over (since we want it to
* overlap by about 2/10ths.)
*/
adjacent= _children[sel-1];
var leftBase:Number = unscaledWidth/2 - selected.widthForAngle(0)/2 - (adjacent.getExplicitOrMeasuredWidth()/2 +adjacent.widthForAngle(a)*2/10) * c.scale;
/* now that we know where that first item to the left sits, we can calculate the position of our child as a simple fixed distance based on how many
* children sit between it and that first item to the left
*/
c.x = leftBase - kPaneOverlap*(sel-1-i),
/* lastly, center it vertically */
c.y = unscaledHeight/2 - t.getExplicitOrMeasuredHeight()* (1-_popout)/2;
}
else
{
/* this is basically the same logic as above, but for children to the right of the selection. It sets it to
* a negative angle, and calculates the position as a distance from the first child to the right of the selection.
*/
c.scale = (1-_popout);
adjacent = _children[sel+1];
var rightBase:Number = unscaledWidth/2 + selected.widthForAngle(0)/2 + (adjacent.widthForAngle(-_angle)*3/10 - adjacent.getExplicitOrMeasuredWidth()/2) * c.scale;
c.angle = -_angle;
c.x = rightBase + kPaneOverlap*(i-(sel+1));
c.y = unscaledHeight/2 - t.getExplicitOrMeasuredHeight() * (1-_popout)/2;
}
}
//---------------------------------------------------------------------------------------
// interaction
//---------------------------------------------------------------------------------------
/* this is our event handler for when a user clicks on an item.
*/
private function itemClickHandler(e:MouseEvent):void
{
/* again, if the developer wants different UI behavior, allow them to disable this */
if(_selectOnClick == false)
return;
/* find out what the index is of the selected item. To do this, we map back from the
* item clicked on to an index in our itemIndexMap. Since we re-order our children
* to get depth and layering correct, we couldn't necessarily just ask for the child index...
* the child index would be different from the item's index in the dataProvider. We could
* iterate over the children array to find the one that was clicked on, but that might have
* bad performance implications. Instead, we use a dictionary to quickly map from a child to
* an index. This is a really useful way to generally store metadata about your items/renderers
* in custom components.
*/
var index:Number = _itemIndexMap[e.currentTarget];
selectedIndex = index;
}
/* this is our event handler for when our dataProvider changes.
* in this case, all we do is set a flag indicating that we want to regenerate our item renderers,
* and invalidate our properties. The change event from the collection typically carries additional
* data...was an item added, removed, or just changed? We could, and really should, optimize how
* we respond to this event based on what really happened...i.e., if an item was added, there's no
* need to regenerate _all_ our item renderers. Exercise for the reader ;)
*/
private function dataChangeHandler(event:CollectionEvent):void
{
_itemsDirty = true;
invalidateProperties();
}
//---------------------------------------------------------------------------------------
// Keyboard Management
//---------------------------------------------------------------------------------------
/* this event handler is where we respond to key presses when we have focus. Note that this event handler
* is already defined by the UIComponent base class...so we didn't have to add it anywhere. Instead, by
* simply implementing the marker interface IFocusManagerComponent, and overriding this method, we get to
* handler key down events.
*/
override protected function keyDownHandler(event:KeyboardEvent):void
{
super.keyDownHandler(event);
switch(event.keyCode)
{
case Keyboard.LEFT:
selectedIndex = Math.max(0,selectedIndex-1);
event.stopPropagation();
break;
case Keyboard.RIGHT:
selectedIndex = Math.min(_dataProvider.length-1,selectedIndex+1);
event.stopPropagation();
break;
}
}
//---------------------------------------------------------------------------------------
// animation
//---------------------------------------------------------------------------------------
/* This is where we do our animation. This function is called whenever the selected index changes.
*/
private function startAnimation():void
{
/* when you add animation to a component, you need to decide what will happen if two animations
* try to fire at once. What happens, in this case, if the user sets the selected index while we're
* still animating towards a previous selected index?
* Our decision here is to finish the previous animation (i.e., jump directly to the end of the animation).
*/
if(_animation != null && _animation.isPlaying)
{
_animation.end();
}
/* our animation is simple. Since our component tracks 'selectedIndex' and 'currentPosition' as separate concepts,
* animating is just a question of tweening the currentPosition variable to the selectedIndex value.
* every time the animation updates currentPosition, our component will invalidate and redraw. Easy as pie.
*/
_animation = new AnimateProperty(this);
_animation.property = "currentPosition";
_animation.toValue = _selectedIndex;
_animation.target = this;
/* if we picked a fixed duration, we'd have to deal with the fact that sometimes we're only moving a single position,
* and sometimes we may be moving a thousand. Either short distances would be way too slow, or long distances would go
* way to fast and look bad. Instead, we'll calculate a duration based on how far we're animating. We also put in a minimum animation
* so short distances don't go too quickly. We probably should also putting a cap so even in large data sets animations don't take too long.
* We could tweak this endlessly.
*/
_animation.duration = Math.max(500,Math.abs(_selectedIndex - _currentPosition) * 200);
_animation.easingFunction = mx.effects.easing.Quadratic.easeOut;
_animation.play();
}
//---------------------------------------------------------------------------------------
// history managmeent
//---------------------------------------------------------------------------------------
/* These are the two methods a component needs to implement in order to save state with the history manager.
* in our constructor, we registered ourselves as a history enabled component with the history manager. Once that
* happens, any time someone tries to save a state in the history, the manager will call this function to let our
* component store off whatever values it needs to capture its current state
*/
public function saveState():Object
{
if(_enableHistory == false)
return {};
/* all we really need to store is our selected index. */
var index:int = _safeSelectedIndex == -1 ? 0 : _safeSelectedIndex;
return { selectedIndex: index };
}
/* this function, in turn, gets called whenever someone tries to navigate (back or forth) to a stored state.
* this funciton gives us a chance to read out our stored state and react accordingly.
*/
public function loadState(state:Object):void
{
if(_enableHistory == false)
return;
var newIndex:int = state ? int(state.selectedIndex) : 0;
if (newIndex == -1)
newIndex = 0;
if (newIndex != _safeSelectedIndex)
{
// When loading a new state, we don't want to
// save our current state in the history stack.
var eh:Boolean = _enableHistory;
_enableHistory = false;
selectedIndex = newIndex;
_enableHistory = eh;
}
}
}
}
import flash.events.EventDispatcher;
/* this little doodad is just a value object we use to store the position information for a single child.
* by defining the class here, we don't clutter up the global namespace. This class is only visible inside this file.
*/
class ChildPosition
{
public var angle:Number;
public var x:Number;
public var y:Number;
public var scale:Number;
}
3.TiltingPane.as
/*
Copyright (c) 2006 Adobe Systems Incorporated
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
package
{
import mx.core.UIComponent;
import flash.geom.Matrix;
import flash.display.Sprite;
import flash.display.Shape;
import flash.display.Graphics;
import flash.events.Event;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.GradientType;
import flash.geom.Rectangle;
import flash.geom.Point;
import flash.display.DisplayObject;
import flash.display.CapsStyle;
import flash.display.LineScaleMode;
import flash.display.JointStyle;
import mx.events.FlexEvent;
/* by defining a default property, we are allowing a developer to use our component and specify the value of this property as the
* 'content' of the TiltingPane tag in their MXML. This is a reasonable thing to do when there is a property that reasonably maps
* to the developer's concept of the 'content' of the component. It wouldn't make sense, for example, to set the angle of the
* component to be the default property, since a developer doesn't think of the tilting pane as 'containing' the angle. But
* naturally, our 'content' property is a good match here. This is a convenient way to make what is sometimes referred to as a
* 'custom container.' to the developer, this component looks and feels like a container, even though it doesn't extend the
* container base class.
*/
[DefaultProperty("content")]
/* by defining our styles here, we allow the developer to specify their values on the tag in MXML. If we didn't declare them here
* the compiler would not recognize these styles as legal attributes in MXML.
*/
[Style(name="borderThickness", type="Number")]
[Style(name="borderColor", type="Number")]
/* As a new custom component that defines basic control behavior, we'll choose to extend UIComponent. Since we want this to be a full fledged
* flex component, our choices are essentially UIComponent, a Container, or some other previously existing component. No existing component
* is close to the behavior we want, so that leaves UIComponent or Container. Container defines all sorts of great functionality...scrolling, clipping,
* etc. But we don't need any of that. The only things that component does define is the ability to have children, and the ability to define those
* children in MXML. But we get children as well when extending UIComponent, and using templating and defaultProperty (see below and above) we can
* let develoeprs specify children in MXML as well. so we'll go with UIComponent.
*/
public class TiltingPane extends UIComponent
{
//---------------------------------------------------------------------------------------
// constructor
//---------------------------------------------------------------------------------------
public function TiltingPane()
{
super();
}
//---------------------------------------------------------------------------------------
// initialization
//---------------------------------------------------------------------------------------
/* createChildren() is the right place to create the sub components that we'll need to implement our TiltingPane. If you need to dynamically
* create children based on the state of the component, you should do that in commitProperties (see DisplayShelf for an example). But for
* sub-components that will be needed for the lifetime of the component, doing it here gets you the best performance at initialization time.
*
* for our component rendering, we're going to be needing a bunch of flash display objects...shapes and bitmaps...to get the effect we're looking for.
* When writing a UIComponent, it's perfectly legal to use raw flash display objects as sub-components to get whatever effect you need.
* a UIComopnent is like your own little sandbox...in here, you can use whatever flash and flex APIs and objects you want.
*/
override protected function createChildren():void
{
// first, create a simple shape object. We'll use this as a mask to give our content a perspective trapezoid.
_mask = new Shape();
// next, create another simple shape obecjt. This one will overlay our content and be used to draw a nice border.
_border = new Shape();
// add these as our children. While the mask doesn't technically get shown on screen, it still needs to go on the display List somehwere.
// flash let's you use any arbitrary displayObject on the displayList as a mask...it doesn't necessarily need to have the same parent as the
// thing it's masking. Flash will just look at the postiion of the two objects on screen, see where they overlap, and clip the content
// accordingly. But it's easiest to figure out how the mask, content, and border will relate to each other if they're in the same coordinate space...
// i.e., if 0,0 means the same thing to all of them...so we'll make all three children. We don't create or attach our content here, since we're going
// to let the user of our compoennt dictate what that is.
addChild(_mask);
addChild(_border);
// if we already have our content object, we tell it to our our mask object as its mask. If not, we'll do it later when we get our content.
if(_content != null)
_content.mask = _mask;
}
//---------------------------------------------------------------------------------------
// constants
//---------------------------------------------------------------------------------------
// some constants that affect how we render our fake 3D and reflection. Good rule of thumb...constants like these are
// usually prime candidates for turning into styles.
private static const kPerspective:Number = .15;
private static const kFalloff:Number = .4;
//---------------------------------------------------------------------------------------
// private state
//---------------------------------------------------------------------------------------
// the subcomponent that we'll be applying our faux 3d effect to. We know very little about this component.
private var _content:UIComponent;
// the shape we'll use to clip off the content into a perspective trapezoid.
private var _mask:Shape;
// the shape we'll draw the border around the content into.
private var _border:Shape;
// the bitmap object we'll copy the content into to create a reflection.
private var _reflectionBitmap:Bitmap;
// the tilt angle requested from the developer. This value is used to calculate measured size.
private var _explicitAngle:Number = 0;
// the actual angle being used to render the component. By default, when the explicit angle is set,
// this is set too. But a parent component compositing the tilting tile can assign an actual angle,
// which will be used to render but not in measurement calculations.
private var _actualAngle:Number = 0;
// the shear factor we assign to our content based on the current actual angle.
private var _verticalShear:Number;
// how far, in pixels, our content is offset as a result of the verticalshear.
private var _verticalShearEffect:Number;
// how much we scale down our content based on the current actual angle.
private var _horizontalScale:Number;
//---------------------------------------------------------------------------------------
// properties
//---------------------------------------------------------------------------------------
/* the actual content we'll be applying our faux 3D effect to. By defining a property of type
* UIComponent, we actually are allowing the developer to specify the content in MXML or actionscript.
* This ability to parameterize the content of a component is often referred to as Templating.
*/
public function set content(value:UIComponent):void
{
// if we had a previous content assigned, we need to clean up from it.
if(_content != null)
{
// remove it from our display tree.
removeChild(_content);
// stop listening for update events.
_content.removeEventListener(FlexEvent.UPDATE_COMPLETE,contentUpdateHandler);
}
_content = value;
if(_content != null)
{
// add the new child. We want the content to be behind the frame, so we add it at
// index 0.
addChildAt(_content,0);
// in order to make sure we can update the reflection whenever our content
// updates, we need to listen for the update complete event, which fires whenever
// updateDisplayList runs on a component. One thing to note is that this event
// doesn't bubble, which means that we won't necessarily know if a sub-component
// updates. That's a limitation you might run into if you use this to
// display more complex content.
_content.addEventListener(FlexEvent.UPDATE_COMPLETE,contentUpdateHandler);
_content.cacheAsBitmap = true;
_content.mask = _mask;
}
// our 'natural' size is based on the size of our content, so when our content changes,
// we need to remeasure.
invalidateSize();
// since we have new content, we'll need to update our reflection to match.
invalidateReflection();
}
public function get content():UIComponent
{
return _content;
}
/* the tilt angle we'll use to display our content at.
*/
public function set angle(value:Number):void
{
// store off the value. Since we track explicit and actual angle separately, the value
// gets stuffed back into both of them.
_explicitAngle = _actualAngle = value;
invalidateSize();
invalidateDisplayList();
}
public function get angle():Number
{
return _actualAngle;
}
/* When components aggregate a TiltingTile, the angle is sometimes both an input to their layout
* computation (as part of this component's measure() calculations) and an output...something
* they explicitly set. As such, we need to differentiate between explicit angle, and parent
* calculated actual angle.
*/
public function setActualAngle(value:Number):void
{
_actualAngle = value;
invalidateDisplayList();
}
//---------------------------------------------------------------------------------------
// measurement
//---------------------------------------------------------------------------------------
/* the measure function is where every component declares what their 'natural' size is...i.e., the most reasonable default size
* given their content and state, assuming the developer hasn't assigned a specific size. In this csae, our 'natural' height is going
* to be just the measured height of our content. That's potentially a little problematic...since we're skewing the content, it will actually be
* a little taller than its measured size. We could account for that in our measured size, but instead I'll just report the same measured size.
* what does that mean? It means that by default, we'll actually stick a little outside of our assigned bounds. Which is a perfectly legal thing to do
* in flex, if you think it's the right thing for your component to do (i.e., there's nothing about flex or flash that _prevents_ your from doing it).
* For the measuredWidth, we'll calculate how wide our content would be at our currently explicitly assigned angle.
*/
override protected function measure():void
{
if(_content != null)
{
measuredHeight = _content.getExplicitOrMeasuredHeight();
measuredWidth = widthForAngle(_explicitAngle);
}
}
/* a utility function that measures how wide we'll be at a given tilt angle. essentially, we use a little faux 3d math to compute a horizontal
* scale factor based on the angle.
*/
public function widthForAngle(angle:Number):Number
{
/* take our value from -90 to 90, and turn it into a value from 0 to 1. */
var p:Number = (Math.abs(angle)/90);
/* now take the square root. When you watch something turn away from you, it doesn't squeeze in your vision linearly. Again, this is
* faux 3d...what we care about is that it 'looks' right, not that it is right. */
p = Math.sqrt(p);
/* invert the result to get a scale factor...i.e., an angle of 0 should
* mean a scale of 1, and an angle of 90 should mean a scale of zero */
var scale:Number = 1 - p;
/* finally, multiply our scale factor by the measured (or explicit) size of the content */
var r:Number = _content.getExplicitOrMeasuredWidth() * scale;
return r;
}
//---------------------------------------------------------------------------------------
// rendering and layout
//---------------------------------------------------------------------------------------
/* updateDisplayList() is where we'll do all of our actual layout and rendering. This funciton is called by the
* layout manager, whenever we (or our base class code) indicate we need to be updated by calling invalidateDisplayList().
*/
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
/* make sure we have content before we bother with any of the tough stuff */
if(_content)
{
var contentWidth:Number = _content.getExplicitOrMeasuredWidth();
var contentHeight:Number= _content.getExplicitOrMeasuredHeight();
var centerX:Number = unscaledWidth/2;
/* first calculate some values based on our current angle, and the size of our content.
* we'll do this once, store it off, and use it in our various layout subroutines. Normally,
* this is the kind of thing we'll do in our commitProperties function. But because this is based
* on actualAngle, which is potentially set by our parents during their layout pass, this is really
* the only time for us to do it. our parent's updateDisplayList function is typically called after our
* commitProperties and mesure function, so the only place to do this that we can guarantee will be called
* after our parent has the chance to set our actualSize is here.
*/
/* take our value from -90 to 90, and turn it into a value from 0 to 1. */
var p:Number = (Math.abs(_actualAngle)/90);
/* now take the square root. When you watch something turn away from you, it doesn't squeeze in your vision linearly. Again, this is
* faux 3d...what we care about is that it 'looks' right, not that it is right. */
p = Math.sqrt(p);
/* invert the result to get a scale factor...i.e., an angle of 0 should
* mean a scale of 1, and an angle of 90 should mean a scale of zero */
_horizontalScale = 1 - p;
// now compute a shear factor based on how far we're turned, and whether we're turned positive or negative.
if(_actualAngle >= 0)
{
_verticalShear = p * -kPerspective;
}
else
{
_verticalShear = p * kPerspective;
}
/* based on the size of our content, figure out how far the right edge of our content will be offset from its untransformed
* location. */
_verticalShearEffect = contentWidth/2 * _verticalShear;
/* first set the actual size of our content. It's the responsibility of each parent to set the actual size of each
* of its UIComponent children during layout. In our case, we're going to set the size of our content to its explicit
* or measured size, which is the conventional way to size children in flex. */
_content.setActualSize(contentWidth,contentHeight);
/* now that our child is sized to its default, we're going to manipulate its transform matrix to get the scaling
* and shearing that will give us half of our 3D effect.
*/
perspectiveDistort(centerX,0,contentWidth,contentHeight);
/* now draw the border. if no border color or thickness is specified, we can skip this step. */
var borderColor:Number = getStyle("borderColor");
var borderThickness:Number = getStyle("borderThickness");
/* all drawing in flex/flash is done into a graphics object. FLash is 'retained' mode, meaning all the drawing
* that happens in a graphics object stays there until you explicitly clear it. So the first thing we need to do
* is call clear(). It's a common mistake to forget to do this, and end up drawing over and over again on top
* of the previous drawing. Even if you're drawing on top, it doesn't 'remove' the old graphics. Eventually,
* you'll notice a slowdown in the application as a result of all the duplicate drawing.
* Similarly, even if we don't have a border color or thickness, we still need to clear out the border shape graphics,
* to make sure a border isn't lying around from a previous update.
*/
var g:Graphics = _border.graphics;
g.clear();
if(!isNaN(borderColor) && !isNaN(borderThickness))
{
/* set the linestyle in the border shape graphics object, and draw the border trapezoid. */
g.lineStyle(borderThickness,borderColor,1,false,LineScaleMode.NORMAL,CapsStyle.NONE,JointStyle.MITER);
drawPerspectiveFrame(g,centerX,0, contentWidth, contentHeight);
}
/* if we don't have a reflection bitmap, we need to first generate a new one from our content. Whenever our content
* changes we throw away our bitmap and redraw it. We could be a little more intelligent here...reuse the bitmap if the
* content size isn't changing, reuse some of the pieces used in the rendering step....but that's an exercise for the reader
*/
if(_reflectionBitmap == null)
{
createReflectionBitmap(_content);
}
/* lastly, put a matrix transform on the bitmap to get it inverted, in place, and sheared correctly. */
positionReflectionBitmap(centerX,0,contentWidth,contentHeight);
}
}
/* this internal layout utility assigns a perspective distortion and mask to its target, based on our previously computed values from the angle.
*/
private function perspectiveDistort(centerX:Number, yPosition:Number, frameWidth:Number, frameHeight:Number ):void
{
/* grab the transformation matrix from our content. Remember that the transformation is copy on read...meaning that when
* you ask for the matrix, you're getting a copy. Which means any changes we make will only have an effect if we assign
* the matrix back to the transform when we're done.
*/
var m:Matrix = _content.transform.matrix;
/* set the shear and horizontal scale to get the basic 3D effect
*/
m.b = _verticalShear;
m.a = _horizontalScale;
/* position the content. we want it centered horizontally. vertically, we want it to look as though it's at yPosition, but
* with 3D perspective. So we need to offset by the effect of the shearing.
*/
m.tx = centerX - frameWidth/2 * _horizontalScale;
m.ty = yPosition - _verticalShearEffect;
/* make sure our changes actually affect it! */
_content.transform.matrix = m;
/* shearing is only half the faux 3d effect. We also need to turn our content into a trapezoid, to make it look like it's
* receeding into the distance. To do that, we'll draw a trapezoid into our mask shape, which will clip the content.
*/
// first clear out any previous graphics.
_mask.graphics.clear();
// now begin a fill. It actually doesn't matter what kind of fill we use here, since masks are not visible on screen. But
// we do need _some_ fill.
_mask.graphics.beginFill(0,0);
// and draw our trapezoid
drawPerspectiveFrame(_mask.graphics,centerX,yPosition,frameWidth, frameHeight);
_mask.graphics.endFill();
}
/* this utility function draws a trapezoid based on our currently computed actual angle. Since we'll use this to
* draw both our mask and border, we pass in the graphics object we'll draw into as a parameter. We also assume that
* the caller has already set up the fill and linestyle they want, just as with the built in drawRect, etc. functions.
*
* Nothing interesting going on in this function, just some math. If I could draw a diagram in comments, I'd show
* the basis for the math, but it's not too complicated (in fact, I arrived at this via trial and error ;) Another
* exercise for the reader ;)
*/
private function drawPerspectiveFrame( g:Graphics, centerX:Number, yPosition:Number, frameWidth:Number, frameHeight:Number ):void
{
var frameLeft:Number= centerX - frameWidth/2 * _horizontalScale;
if(_actualAngle >= 0)
{
g.moveTo(frameLeft,-_verticalShearEffect);
g.lineTo(frameLeft,frameHeight - _verticalShearEffect);
g.lineTo(frameLeft + frameWidth*_horizontalScale,frameHeight + _verticalShearEffect);
g.lineTo(frameLeft + frameWidth*_horizontalScale,3*-_verticalShearEffect);
g.lineTo(frameLeft,-_verticalShearEffect);
}
else
{
g.moveTo(frameLeft,3*_verticalShearEffect);
g.lineTo(frameLeft, frameHeight - _verticalShearEffect);
g.lineTo(frameLeft+frameWidth*_horizontalScale, frameHeight + _verticalShearEffect);
g.lineTo(frameLeft+frameWidth*_horizontalScale, _verticalShearEffect);
g.lineTo(frameLeft,+3*_verticalShearEffect);
}
}
/* this event handler gets called whenever our content updates its layout and rendering. We'll want to update our reflection bitmap
* to match, we we just invalidate our reflection
*/
private function contentUpdateHandler(event:Event):void
{
invalidateReflection();
}
/* this function clears out any cached data we have about our reflection, and invalidates our display list.
* as with all of our other code, we don't just rebuild our reflection whenever anything changes...we set a flag
* (in this case, just the fact that our reflection bitmap is null will be enough of a flag) and do the heavy lifting
* when our updateDisplayList() function is called
*/
private function invalidateReflection():void
{
// throw out any previously existing reflection bitmap. It's worth pointing out that this is a little bit of overkill..
// there's some data that we could likely reuse when the content updates...i.e., if the content doesn't change size,
// we can use the same bits, but just redraw into them. Exercise for the reader ;)
if(_reflectionBitmap != null)
removeChild(_reflectionBitmap);
_reflectionBitmap = null;
// request an update.
invalidateDisplayList();
}
/* this utility function creates our reflection bitmap from our content. It gets called whenever the
* content changes. I should point out that this code was culled from the reflection example created
* by the great Narcisso Jaramillo.
*/
private function createReflectionBitmap(target:UIComponent):void
{
// first, figure out how big our bitmap needs to be. Flash bitmap APIs don't like
// 0x0 bitmaps, so we'll constrain it to make sure we at least create a 1x1 bitmap.
var tw:Number = Math.max(1,target.width);
var th:Number = Math.max(1,target.height);
var rect: Rectangle = new Rectangle(0, 0, target.width, target.height);
// Create a temporary alpha gradient bitmap. When we draw our content into our
// reflection bitmap, we'll combine it with this to get our fadeout effect.
// note that in the code below, we create a shape, draw into it, then blit it into
// our bitmap, and throw the sprite away, all without ever actually adding the shape
// to the display list. DisplayObjects can be useful even if they never end up on screen.
var alphaGradientBitmap:BitmapData = new BitmapData(tw, th, true, 0x00000000);
var gradientMatrix: Matrix = new Matrix();
var gradientShape: Shape = new Shape();
gradientMatrix.createGradientBox(tw, th * kFalloff, Math.PI/2,
0, th * (1.0 - kFalloff));
gradientShape.graphics.beginGradientFill(GradientType.LINEAR, [0xFFFFFF, 0xFFFFFF],
[0, 1], [0, 255], gradientMatrix);
gradientShape.graphics.drawRect(0, th * (1.0 - kFalloff),
tw, th * kFalloff);
gradientShape.graphics.endFill();
alphaGradientBitmap.draw(gradientShape, new Matrix());
// create a temporary bitmap to hold the image of our content.
var targetBitmap:BitmapData = new BitmapData(tw, th, true, 0x00000000);
// initialize it to empty. Note that's not an RGB value, but an ARGB value.
// the bitmap API adds alpha values to typical RGB hex values.
targetBitmap.fillRect(rect, 0x00000000);
// we need to temporariliy remove the mask from the target component before
// we can grab its bits. Otherwise we'd get the clipped version.
var mm:DisplayObject = target.mask;
target.mask = null;
// capture the bits.
targetBitmap.draw(target, new Matrix());
// restore the mask.
target.mask = mm;
// now create the final bitmap for our reflection.
var reflectionData:BitmapData = new BitmapData(tw, th, true, 0x00000000);
// initialize it to empty. Again, we're using RGBA values
reflectionData.fillRect(rect, 0x00000000);
// copy in the bits from our content, and merge it with the gradient bitmap as an alpha channel.
reflectionData.copyPixels(targetBitmap, rect, new Point(), alphaGradientBitmap);
// alright, now we've got our reflection bitmap data. To actually put it on the display list, we need to
// wrap it up in a Bitmap object, which is a DisplayObject.
_reflectionBitmap = new Bitmap(reflectionData);
// give it that nice faint transparent look by setting its alpha down.
_reflectionBitmap.alpha = .3;
// and add it to our display list.
addChildAt(_reflectionBitmap,0);
}
/* this utility function takes our reflection bitmap and sets up its matrix transform to invert it, shear it, and place it below
* our content.
*/
private function positionReflectionBitmap(centerX:Number, yPosition:Number, frameWidth:Number, frameHeight:Number ):void
{
// grab the matrix transform
var m:Matrix = _reflectionBitmap.transform.matrix;
// assign the shear and horizontal scale.
m.b = _verticalShear;
m.a = _horizontalScale;
// we need our reflection to be upsidown. So set its vertical scale to -1.
m.d = -1;
// center it horizontally.
m.tx = centerX - frameWidth/2 * _horizontalScale;
// and position it _below_ our content. if our content is normally at yPosition, the bottom of our sheared content is at yPosition plus the
// pixel distance of the shear effect, plus the height of the content. However, since our reflection has a vertical scale of -1, it will stick
// _up_ from wherever we place it. So we need to add the size of the reflection bitmap to our position, to guarantee that the _bottom_ of the bitmap,
// which is extending upwards, ends up at the bottom of the content.
m.ty = yPosition - _verticalShearEffect + 2*frameHeight;
// reassign the matrix.
_reflectionBitmap.transform.matrix = m;
}
}
}