WindTheBusiness/com/yahoo/astra/layout/LayoutContainer.as
2020-10-20 00:58:15 +02:00

453 lines
12 KiB
ActionScript
Executable File

/*
Copyright (c) 2009 Yahoo! Inc. All rights reserved.
The copyrights embodied in the content of this file are licensed under the BSD (revised) open source license
*/
package com.yahoo.astra.layout
{
import com.yahoo.astra.layout.events.LayoutEvent;
import com.yahoo.astra.layout.modes.BoxLayout;
import com.yahoo.astra.layout.modes.ILayoutMode;
import com.yahoo.astra.utils.NumberUtil;
import flash.display.DisplayObject;
import flash.display.Sprite;
import flash.events.Event;
import flash.geom.Rectangle;
//--------------------------------------
// Events
//--------------------------------------
/**
* Dispatched when this container's layout changes.
*
* @eventType com.yahoo.astra.layout.events.LayoutEvent.LAYOUT_CHANGE
*/
[Event(name="layoutChange", type="com.yahoo.astra.layout.events.LayoutEvent")]
/**
* Children of this display object are subject to being positioned, and
* possibly resized, based on a specified layout algorithm. LayoutContainer
* integrates with LayoutManager to refresh its the layout of its children
* when properties on the container itself change or when one of its
* children dispatches a registered invalidating event. This is the default
* implementation of ILayoutContainer.
*
* @example The following code demonstrates the usage of LayoutContainer:
* <listing version="3.0">
* // create an instance of a layout mode
* var mode:ILayoutMode = new BoxLayout();
* mode.direction = "horizontal";
* mode.horizontalGap = 10;
*
* // one may pass the mode to the constructor or the layoutMode property.
* // note: by default, a LayoutContainer will automatically determine
* // its size based on its content.
* var container:LayoutContainer = new LayoutContainer( mode );
* this.addChild(container);
*
* for( var i:int = 0; i < 5; i++ )
* {
* var square:Shape = new Shape();
* square.graphics.beginFill(0xcccccc);
* square.graphics.drawRect(0, 0, 25, 25);
* square.graphics.endFill();
* container.addChild(square);
* }
* </listing>
*
* <p><strong>Important Note:</strong> LayoutContainer leaves certain
* functionality to the implementor to complete. No scrollbars or other user
* interface controls will appear when the contents are larger than the
* LayoutContainer's dimensions.</p>
*
* <p>This limitation is deliberate and by design. The philosophy behind
* this choice centers on allowing an ActionScript developer to use these
* classes as a basis for implementing layout controls for nearly any user
* interface library available for Flash Player.</p>
*
* <p>For a reference implementation of full-featured UI controls that
* implement masking and scrolling, please take a look at the Layout
* Containers available in the <a href="http://developer.yahoo.com/flash/astra-flash/">Yahoo! Astra Components for Flash CS3</a>.</p>
*
* @see LayoutManager
* @see ILayoutContainer
* @see modes/package-detail.html Available Layout Modes (com.yahoo.astra.layout.modes)
* @author Josh Tynjala
*/
public class LayoutContainer extends Sprite implements ILayoutContainer
{
//--------------------------------------
// Static Properties
//--------------------------------------
/**
* @private
* Flag indicating whether we are in render mode.
*/
public static var isRendering:Boolean = false;
//--------------------------------------
// Constructor
//--------------------------------------
/**
* Constructor.
*
* @param mode The ILayoutMode implementation to use.
*/
public function LayoutContainer(mode:ILayoutMode = null)
{
super();
this.scrollRect = new Rectangle();
this.layoutMode = mode;
this.addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
}
//--------------------------------------
// Properties
//--------------------------------------
/**
* @private
* Storage for the contentWidth property.
*/
protected var _contentWidth:Number = 0;
/**
* @inheritDoc
*/
public function get contentWidth():Number
{
return this._contentWidth;
}
/**
* @private
* Storage for the contentHeight property.
*/
protected var _contentHeight:Number = 0;
/**
* @inheritDoc
*/
public function get contentHeight():Number
{
return this._contentHeight;
}
/**
* @private
* Storage for width values explicitly set by a developer.
*/
protected var explicitWidth:Number = NaN;
/**
* @private
*/
override public function get width():Number
{
if(!isNaN(this.explicitWidth))
{
return this.explicitWidth;
}
return this.contentWidth;
}
/**
* @private
*/
override public function set width(value:Number):void
{
if(this.explicitWidth != value)
{
this.explicitWidth = value;
this.invalidateLayout();
}
}
/**
* @private
* Storage for height values explicitly set by a developer.
*/
protected var explicitHeight:Number = NaN;
/**
* @private
*/
override public function get height():Number
{
if(!isNaN(this.explicitHeight))
{
return this.explicitHeight;
}
return this.contentHeight;
}
/**
* @private
*/
override public function set height(value:Number):void
{
if(this.explicitHeight != value)
{
this.explicitHeight = value;
this.invalidateLayout();
}
}
/**
* @private
* Storage for the layoutMode property.
*/
private var _layoutMode:ILayoutMode = new BoxLayout();
/**
* @inheritDoc
*/
public function get layoutMode():ILayoutMode
{
return this._layoutMode;
}
/**
* @private
*/
public function set layoutMode(value:ILayoutMode):void
{
if(this._layoutMode)
{
this._layoutMode.removeEventListener(LayoutEvent.LAYOUT_CHANGE, layoutModeChangeHandler);
}
this._layoutMode = value;
if(this._layoutMode)
{
this._layoutMode.addEventListener(LayoutEvent.LAYOUT_CHANGE, layoutModeChangeHandler, false, 0, true);
}
this.invalidateLayout();
}
/**
* @private
* Storage for the autoMask property.
*/
private var _autoMask:Boolean = true;
/**
* If true, the conent will automatically update the scrollRect to fit
* the dimensions. Uses explicit dimensions if width or height is set by
* the developer. Otherwise, uses the content dimensions. If false, it
* is up to the implementor to set the mask or scrollRect.
*/
public function get autoMask():Boolean
{
return this._autoMask;
}
/**
* @private
*/
public function set autoMask(value:Boolean):void
{
this._autoMask = value;
if(this._autoMask)
{
this.scrollRect = new Rectangle(0, 0, this.width, this.height);
}
else
{
this.scrollRect = null;
}
}
/**
* @private
* Flag indicating if the layout is invalid.
*/
protected var invalid:Boolean = false;
/**
* @private
* Flag indicating if the LayoutContainer is currently validating.
*/
protected var isValidating:Boolean = false;
//--------------------------------------
// Public Methods
//--------------------------------------
/**
* @private
*/
override public function addChild(child:DisplayObject):DisplayObject
{
child = super.addChild(child);
if(child)
{
LayoutManager.registerContainerChild(child);
this.invalidateLayout();
}
return child;
}
/**
* @private
*/
override public function addChildAt(child:DisplayObject, index:int):DisplayObject
{
child = super.addChildAt(child, index);
if(child)
{
LayoutManager.registerContainerChild(child);
this.invalidateLayout();
}
return child;
}
/**
* @private
*/
override public function removeChild(child:DisplayObject):DisplayObject
{
child = super.removeChild(child);
if(child)
{
LayoutManager.unregisterContainerChild(child);
this.invalidateLayout();
}
return child;
}
/**
* @private
*/
override public function removeChildAt(index:int):DisplayObject
{
var child:DisplayObject = super.removeChildAt(index);
if(child)
{
LayoutManager.unregisterContainerChild(child);
this.invalidateLayout();
}
return child;
}
/**
* @inheritDoc
*/
public function invalidateLayout():void
{
//if we're validating, then this change is caused
//by an invalidating event from a child, and we can safely ignore it
if(this.isValidating)
{
return;
}
if(isRendering)
{
//force validation during the render phase. performance hit should be minimal.
this.validateLayout();
}
if(!this.invalid && this.stage)
{
this.invalid = true;
this.stage.addEventListener(Event.ENTER_FRAME, renderHandler);
}
}
/**
* @inheritDoc
*/
public function validateLayout():void
{
this.isValidating = true;
this.layout();
this.isValidating = false;
this.invalid = false;
}
//--------------------------------------
// Protected Methods
//--------------------------------------
/**
* @private
* Updates the layout algorithm and recalculates the content dimensions.
*/
protected function layout():void
{
var oldWidth:Number = this.contentWidth;
var oldHeight:Number = this.contentHeight;
this._contentWidth = Number.POSITIVE_INFINITY;
this._contentHeight = Number.POSITIVE_INFINITY;
//let the layout mode do all the work (strategy pattern)
var bounds:Rectangle = new Rectangle();
if(this.layoutMode)
{
var children:Array = [];
var childCount:int = this.numChildren;
for(var i:int = 0; i < childCount; i++)
{
children.push(this.getChildAt(i));
}
//width and height return the explicit values if available
//otherwise they return the content width and height values
bounds = this.layoutMode.layoutObjects(children, new Rectangle(0, 0, this.width, this.height));
}
this._contentWidth = bounds.x + bounds.width;
this._contentHeight = bounds.y + bounds.height;
if(this.autoMask)
{
var scrollRect:Rectangle = this.scrollRect;
scrollRect.width = this.width;
scrollRect.height = this.height;
this.scrollRect = scrollRect;
}
if(!NumberUtil.fuzzyEquals(this.contentWidth, oldWidth) || !NumberUtil.fuzzyEquals(this.contentHeight, oldHeight))
{
this.dispatchEvent(new LayoutEvent(LayoutEvent.LAYOUT_CHANGE));
}
}
//--------------------------------------
// Private Event Handlers
//--------------------------------------
/**
* @private
* Invalidates when the container is added to the stage.
*/
private function addedToStageHandler(event:Event):void
{
this.invalidateLayout();
}
/**
* @private
* Invalidates the layout if the layoutMode says that it is invalid.
*/
private function layoutModeChangeHandler(event:LayoutEvent):void
{
this.invalidateLayout();
}
/**
* @private
* Validates the layout on the next frame after invalidation.
*/
private function renderHandler(event:Event):void
{
isRendering = true;
event.target.removeEventListener(Event.ENTER_FRAME, renderHandler);
this.validateLayout();
isRendering = false;
}
}
}