scaffold.libs_as.feathers.controls.LayoutGroup.as Maven / Gradle / Ivy
/*
Feathers
Copyright 2012-2015 Bowler Hat LLC. All Rights Reserved.
This program is free software. You can redistribute and/or modify it in
accordance with the terms of the accompanying license agreement.
*/
package feathers.controls
{
import feathers.core.FeathersControl;
import feathers.core.IFeathersControl;
import feathers.core.IValidating;
import feathers.events.FeathersEventType;
import feathers.layout.ILayout;
import feathers.layout.ILayoutDisplayObject;
import feathers.layout.IVirtualLayout;
import feathers.layout.LayoutBoundsResult;
import feathers.layout.ViewPortBounds;
import feathers.skins.IStyleProvider;
import flash.geom.Point;
import flash.geom.Rectangle;
import starling.display.DisplayObject;
import starling.display.Quad;
import starling.events.Event;
import starling.rendering.BatchToken;
import starling.rendering.Painter;
/**
* A generic container that supports layout. For a container that supports
* scrolling and more robust skinning options, see ScrollContainer
.
*
* The following example creates a layout group with a horizontal
* layout and adds two buttons to it:
*
*
* var group:LayoutGroup = new LayoutGroup();
* var layout:HorizontalLayout = new HorizontalLayout();
* layout.gap = 20;
* layout.padding = 20;
* group.layout = layout;
* this.addChild( group );
*
* var yesButton:Button = new Button();
* yesButton.label = "Yes";
* group.addChild( yesButton );
*
* var noButton:Button = new Button();
* noButton.label = "No";
* group.addChild( noButton );
*
* @see ../../../help/layout-group.html How to use the Feathers LayoutGroup component
* @see feathers.controls.ScrollContainer
*/
public class LayoutGroup extends FeathersControl
{
/**
* @private
*/
private static const HELPER_RECTANGLE:Rectangle = new Rectangle();
/**
* Flag to indicate that the clipping has changed.
*/
protected static const INVALIDATION_FLAG_CLIPPING:String = "clipping";
/**
* The layout group will auto size itself to fill the entire stage.
*
* @see #autoSizeMode
*/
public static const AUTO_SIZE_MODE_STAGE:String = "stage";
/**
* The layout group will auto size itself to fit its content.
*
* @see #autoSizeMode
*/
public static const AUTO_SIZE_MODE_CONTENT:String = "content";
/**
* An alternate style name to use with LayoutGroup
to
* allow a theme to give it a toolbar style. If a theme does not provide
* a style for the toolbar container, the theme will automatically fall
* back to using the default scroll container skin.
*
* An alternate style name should always be added to a component's
* styleNameList
before the component is initialized. If
* the style name is added later, it will be ignored.
*
* In the following example, the toolbar style is applied to a layout
* group:
*
*
* var group:LayoutGroup = new LayoutGroup();
* group.styleNameList.add( LayoutGroup.ALTERNATE_STYLE_NAME_TOOLBAR );
* this.addChild( group );
*
* @see feathers.core.FeathersControl#styleNameList
*/
public static const ALTERNATE_STYLE_NAME_TOOLBAR:String = "feathers-toolbar-layout-group";
/**
* The default IStyleProvider
for all LayoutGroup
* components.
*
* @default null
* @see feathers.core.FeathersControl#styleProvider
*/
public static var globalStyleProvider:IStyleProvider;
/**
* Constructor.
*/
public function LayoutGroup()
{
super();
this.addEventListener(Event.ADDED_TO_STAGE, layoutGroup_addedToStageHandler);
this.addEventListener(Event.REMOVED_FROM_STAGE, layoutGroup_removedFromStageHandler);
}
/**
* The items added to the group.
*/
protected var items:Vector. = new [];
/**
* The view port bounds result object passed to the layout. Its values
* should be set in refreshViewPortBounds()
.
*/
protected var viewPortBounds:ViewPortBounds = new ViewPortBounds();
/**
* @private
*/
protected var _layoutResult:LayoutBoundsResult = new LayoutBoundsResult();
/**
* @private
*/
override protected function get defaultStyleProvider():IStyleProvider
{
return LayoutGroup.globalStyleProvider;
}
/**
* @private
*/
protected var _layout:ILayout;
/**
* Controls the way that the group's children are positioned and sized.
*
* The following example tells the group to use a horizontal layout:
*
*
* var layout:HorizontalLayout = new HorizontalLayout();
* layout.gap = 20;
* layout.padding = 20;
* container.layout = layout;
*
* @default null
*/
public function get layout():ILayout
{
return this._layout;
}
/**
* @private
*/
public function set layout(value:ILayout):void
{
if(this._layout == value)
{
return;
}
if(this._layout)
{
this._layout.removeEventListener(Event.CHANGE, layout_changeHandler);
}
this._layout = value;
if(this._layout)
{
if(this._layout is IVirtualLayout)
{
IVirtualLayout(this._layout).useVirtualLayout = false;
}
this._layout.addEventListener(Event.CHANGE, layout_changeHandler);
//if we don't have a layout, nothing will need to be redrawn
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
/**
* @private
*/
protected var _clipContent:Boolean = false;
/**
* If true, the group will be clipped to its bounds. In other words,
* anything appearing beyond the edges of the group will be masked or
* hidden.
*
* Since LayoutGroup
is designed to be a light
* container focused on performance, clipping is disabled by default.
*
* In the following example, clipping is enabled:
*
*
* group.clipContent = true;
*
* @default false
*/
public function get clipContent():Boolean
{
return this._clipContent;
}
/**
* @private
*/
public function set clipContent(value:Boolean):void
{
if(this._clipContent == value)
{
return;
}
this._clipContent = value;
if(!value)
{
this.mask = null;
}
this.invalidate(INVALIDATION_FLAG_CLIPPING);
}
/**
* @private
*/
protected var originalBackgroundWidth:Number = NaN;
/**
* @private
*/
protected var originalBackgroundHeight:Number = NaN;
/**
* @private
*/
protected var _backgroundSkinPushToken:BatchToken = new BatchToken();
/**
* @private
*/
protected var _backgroundSkinPopToken:BatchToken = new BatchToken();
/**
* @private
*/
protected var currentBackgroundSkin:DisplayObject;
/**
* @private
*/
protected var _backgroundSkin:DisplayObject;
/**
* The default background to display behind all content. The background
* skin is resized to fill the full width and height of the layout
* group.
*
* In the following example, the group is given a background skin:
*
*
* group.backgroundSkin = new Image( texture );
*
* @default null
*/
public function get backgroundSkin():DisplayObject
{
return this._backgroundSkin;
}
/**
* @private
*/
public function set backgroundSkin(value:DisplayObject):void
{
if(this._backgroundSkin == value)
{
return;
}
if(value && value.parent)
{
value.removeFromParent();
}
this._backgroundSkin = value;
this.invalidate(INVALIDATION_FLAG_SKIN);
}
/**
* @private
*/
protected var _backgroundDisabledSkin:DisplayObject;
/**
* The background to display behind all content when the layout group is
* disabled. The background skin is resized to fill the full width and
* height of the layout group.
*
* In the following example, the group is given a background skin:
*
*
* group.backgroundDisabledSkin = new Image( texture );
*
* @default null
*/
public function get backgroundDisabledSkin():DisplayObject
{
return this._backgroundDisabledSkin;
}
/**
* @private
*/
public function set backgroundDisabledSkin(value:DisplayObject):void
{
if(this._backgroundDisabledSkin == value)
{
return;
}
if(value && value.parent)
{
value.removeFromParent();
}
this._backgroundDisabledSkin = value;
this.invalidate(INVALIDATION_FLAG_SKIN);
}
/**
* @private
*/
protected var _autoSizeMode:String = AUTO_SIZE_MODE_CONTENT;
[Inspectable(type="String",enumeration="stage,content")]
/**
* Determines how the layout group will set its own size when its
* dimensions (width and height) aren't set explicitly.
*
* In the following example, the layout group will be sized to
* match the stage:
*
*
* group.autoSizeMode = LayoutGroup.AUTO_SIZE_MODE_STAGE;
*
* Usually defaults to LayoutGroup.AUTO_SIZE_MODE_CONTENT
.
* However, if this component is the root of the Starling display list,
* defaults to LayoutGroup.AUTO_SIZE_MODE_STAGE
instead.
*
* @see #AUTO_SIZE_MODE_STAGE
* @see #AUTO_SIZE_MODE_CONTENT
*/
public function get autoSizeMode():String
{
return this._autoSizeMode;
}
/**
* @private
*/
public function set autoSizeMode(value:String):void
{
if(this._autoSizeMode == value)
{
return;
}
this._autoSizeMode = value;
if(this.stage)
{
if(this._autoSizeMode == AUTO_SIZE_MODE_STAGE)
{
this.stage.addEventListener(Event.RESIZE, stage_resizeHandler);
}
else
{
this.stage.removeEventListener(Event.RESIZE, stage_resizeHandler);
}
}
this.invalidate(INVALIDATION_FLAG_SIZE);
}
/**
* @private
*/
protected var _ignoreChildChanges:Boolean = false;
/**
* @private
*/
override public function addChildAt(child:DisplayObject, index:int):DisplayObject
{
if(child is IFeathersControl)
{
child.addEventListener(FeathersEventType.RESIZE, child_resizeHandler);
}
if(child is ILayoutDisplayObject)
{
child.addEventListener(FeathersEventType.LAYOUT_DATA_CHANGE, child_layoutDataChangeHandler);
}
var oldIndex:int = this.items.indexOf(child);
if(oldIndex == index)
{
return child;
}
if(oldIndex >= 0)
{
this.items.removeAt(oldIndex);
}
this.items.insertAt(index, child);
this.invalidate(INVALIDATION_FLAG_LAYOUT);
return super.addChildAt(child, index);
}
/**
* @private
*/
override public function removeChildAt(index:int, dispose:Boolean = false):DisplayObject
{
if(index >= 0 && index < this.items.length)
{
this.items.removeAt(index);
}
var child:DisplayObject = super.removeChildAt(index, dispose);
if(child is IFeathersControl)
{
child.removeEventListener(FeathersEventType.RESIZE, child_resizeHandler);
}
if(child is ILayoutDisplayObject)
{
child.removeEventListener(FeathersEventType.LAYOUT_DATA_CHANGE, child_layoutDataChangeHandler);
}
this.invalidate(INVALIDATION_FLAG_LAYOUT);
return child;
}
/**
* @private
*/
override public function setChildIndex(child:DisplayObject, index:int):void
{
super.setChildIndex(child, index);
var oldIndex:int = this.items.indexOf(child);
if(oldIndex === index)
{
return;
}
//the super function already checks if oldIndex < 0, and throws an
//appropriate error, so no need to do it again!
this.items.removeAt(oldIndex);
this.items.insertAt(index, child);
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
/**
* @private
*/
override public function swapChildrenAt(index1:int, index2:int):void
{
super.swapChildrenAt(index1, index2);
var child1:DisplayObject = this.items[index1];
var child2:DisplayObject = this.items[index2];
this.items[index1] = child2;
this.items[index2] = child1;
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
/**
* @private
*/
override public function sortChildren(compareFunction:Function):void
{
super.sortChildren(compareFunction);
this.items.sort(compareFunction);
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
/**
* @private
*/
override public function hitTest(localPoint:Point):DisplayObject
{
var localX:Number = localPoint.x;
var localY:Number = localPoint.y;
var result:DisplayObject = super.hitTest(localPoint);
if(result)
{
if(!this._isEnabled)
{
return this;
}
return result;
}
if(!this.visible || !this.touchable)
{
return null;
}
if(this.currentBackgroundSkin && this._hitArea.contains(localX, localY))
{
return this;
}
return null;
}
/**
* @private
*/
override public function render(painter:Painter):void
{
if(this.currentBackgroundSkin &&
this.currentBackgroundSkin.visible &&
this.currentBackgroundSkin.alpha > 0)
{
var mask:DisplayObject = this.currentBackgroundSkin.mask;
painter.pushState(this._backgroundSkinPushToken);
painter.setStateTo(this.currentBackgroundSkin.transformationMatrix, this.currentBackgroundSkin.alpha, this.currentBackgroundSkin.blendMode);
if(mask)
{
painter.drawMask(mask);
}
this.currentBackgroundSkin.render(painter);
if(mask)
{
painter.eraseMask(mask);
}
painter.popState(this._backgroundSkinPopToken);
}
super.render(painter);
}
/**
* @private
*/
override public function dispose():void
{
if(this._backgroundSkin && this._backgroundSkin.parent !== this)
{
this._backgroundSkin.dispose();
}
if(this._backgroundDisabledSkin && this._backgroundDisabledSkin.parent !== this)
{
this._backgroundDisabledSkin.dispose();
}
this.layout = null;
super.dispose();
}
/**
* Readjusts the layout of the group according to its current content.
* Call this method when changes to the content cannot be automatically
* detected by the container. For instance, Feathers components dispatch
* FeathersEventType.RESIZE
when their width and height
* values change, but standard Starling display objects like
* Sprite
and Image
do not.
*/
public function readjustLayout():void
{
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
/**
* @private
*/
override protected function initialize():void
{
if(this.stage !== null && this.stage.root === this)
{
this.autoSizeMode = AUTO_SIZE_MODE_STAGE;
}
super.initialize();
}
/**
* @private
*/
override protected function draw():void
{
var layoutInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_LAYOUT);
var sizeInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_SIZE);
var clippingInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_CLIPPING);
//we don't have scrolling, but a subclass might
var scrollInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_SCROLL);
var skinInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_SKIN);
var stateInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_STATE);
//scrolling only affects the layout is requiresLayoutOnScroll is true
if(!layoutInvalid && scrollInvalid && this._layout && this._layout.requiresLayoutOnScroll)
{
layoutInvalid = true;
}
if(skinInvalid || stateInvalid)
{
this.refreshBackgroundSkin();
}
if(sizeInvalid || layoutInvalid || skinInvalid || stateInvalid)
{
this.refreshViewPortBounds();
if(this._layout)
{
var oldIgnoreChildChanges:Boolean = this._ignoreChildChanges;
this._ignoreChildChanges = true;
this._layout.layout(this.items, this.viewPortBounds, this._layoutResult);
this._ignoreChildChanges = oldIgnoreChildChanges;
}
else
{
this.handleManualLayout();
}
this.handleLayoutResult();
this.refreshBackgroundLayout();
//final validation to avoid juggler next frame issues
this.validateChildren();
}
if(sizeInvalid || clippingInvalid)
{
this.refreshClipRect();
}
}
/**
* Choose the appropriate background skin based on the control's current
* state.
*/
protected function refreshBackgroundSkin():void
{
var oldBackgroundSkin:DisplayObject = this.currentBackgroundSkin;
if(!this._isEnabled && this._backgroundDisabledSkin)
{
this.currentBackgroundSkin = this._backgroundDisabledSkin;
}
else
{
this.currentBackgroundSkin = this._backgroundSkin
}
if(this.currentBackgroundSkin)
{
if(this.originalBackgroundWidth !== this.originalBackgroundWidth ||
this.originalBackgroundHeight !== this.originalBackgroundHeight) //isNaN
{
if(this.currentBackgroundSkin is IValidating)
{
IValidating(this.currentBackgroundSkin).validate();
}
this.originalBackgroundWidth = this.currentBackgroundSkin.width;
this.originalBackgroundHeight = this.currentBackgroundSkin.height;
}
}
if(this.currentBackgroundSkin !== oldBackgroundSkin)
{
this.setRequiresRedraw();
}
}
/**
* @private
*/
protected function refreshBackgroundLayout():void
{
if(this.currentBackgroundSkin === null)
{
return;
}
if(this.currentBackgroundSkin.width !== this.actualWidth ||
this.currentBackgroundSkin.height !== this.actualHeight)
{
this.currentBackgroundSkin.width = this.actualWidth;
this.currentBackgroundSkin.height = this.actualHeight;
this.setRequiresRedraw();
}
}
/**
* Refreshes the values in the viewPortBounds
variable that
* is passed to the layout.
*/
protected function refreshViewPortBounds():void
{
this.viewPortBounds.x = 0;
this.viewPortBounds.y = 0;
this.viewPortBounds.scrollX = 0;
this.viewPortBounds.scrollY = 0;
if(this._autoSizeMode === AUTO_SIZE_MODE_STAGE &&
this._explicitWidth !== this._explicitWidth)
{
this.viewPortBounds.explicitWidth = this.stage.stageWidth;
}
else
{
this.viewPortBounds.explicitWidth = this._explicitWidth;
}
if(this._autoSizeMode === AUTO_SIZE_MODE_STAGE &&
this._explicitHeight !== this._explicitHeight)
{
this.viewPortBounds.explicitHeight = this.stage.stageHeight;
}
else
{
this.viewPortBounds.explicitHeight = this._explicitHeight;
}
var minWidth:Number = this._explicitMinWidth;
if(minWidth !== minWidth) //isNaN
{
minWidth = 0;
}
var minHeight:Number = this._explicitMinHeight;
if(minHeight !== minHeight) //isNaN
{
minHeight = 0;
}
if(this.originalBackgroundWidth === this.originalBackgroundWidth && //!isNaN
this.originalBackgroundWidth > minWidth)
{
minWidth = this.originalBackgroundWidth;
}
if(this.originalBackgroundHeight === this.originalBackgroundHeight && //!isNaN
this.originalBackgroundHeight > minHeight)
{
minHeight = this.originalBackgroundHeight;
}
this.viewPortBounds.minWidth = minWidth;
this.viewPortBounds.minHeight = minHeight;
this.viewPortBounds.maxWidth = this._maxWidth;
this.viewPortBounds.maxHeight = this._maxHeight;
}
/**
* @private
*/
protected function handleLayoutResult():void
{
this.setSizeInternal(this._layoutResult.viewPortWidth,
this._layoutResult.viewPortHeight, false);
}
/**
* @private
*/
protected function handleManualLayout():void
{
var maxX:Number = this.viewPortBounds.explicitWidth;
if(maxX !== maxX) //isNaN
{
maxX = 0;
}
var maxY:Number = this.viewPortBounds.explicitHeight;
if(maxY !== maxY) //isNaN
{
maxY = 0;
}
var oldIgnoreChildChanges:Boolean = this._ignoreChildChanges;
this._ignoreChildChanges = true;
var itemCount:int = this.items.length;
for(var i:int = 0; i < itemCount; i++)
{
var item:DisplayObject = this.items[i];
if(item is ILayoutDisplayObject && !ILayoutDisplayObject(item).includeInLayout)
{
continue;
}
if(item is IValidating)
{
IValidating(item).validate();
}
var itemMaxX:Number = item.x + item.width;
var itemMaxY:Number = item.y + item.height;
if(itemMaxX === itemMaxX && //!isNaN
itemMaxX > maxX)
{
maxX = itemMaxX;
}
if(itemMaxY === itemMaxY && //!isNaN
itemMaxY > maxY)
{
maxY = itemMaxY;
}
}
this._ignoreChildChanges = oldIgnoreChildChanges;
this._layoutResult.contentX = 0;
this._layoutResult.contentY = 0;
this._layoutResult.contentWidth = maxX;
this._layoutResult.contentHeight = maxY;
var minWidth:Number = this.viewPortBounds.minWidth;
var minHeight:Number = this.viewPortBounds.minHeight;
if(maxX < minWidth)
{
maxX = minWidth;
}
if(maxY < minHeight)
{
maxY = minHeight;
}
var maxWidth:Number = this.viewPortBounds.maxWidth;
var maxHeight:Number = this.viewPortBounds.maxHeight;
if(maxX > maxWidth)
{
maxX = maxWidth;
}
if(maxY > maxHeight)
{
maxY = maxHeight;
}
this._layoutResult.viewPortWidth = maxX;
this._layoutResult.viewPortHeight = maxY;
}
/**
* @private
*/
protected function validateChildren():void
{
if(this.currentBackgroundSkin is IValidating)
{
IValidating(this.currentBackgroundSkin).validate();
}
var itemCount:int = this.items.length;
for(var i:int = 0; i < itemCount; i++)
{
var item:DisplayObject = this.items[i];
if(item is IValidating)
{
IValidating(item).validate();
}
}
}
/**
* @private
*/
protected function refreshClipRect():void
{
if(!this._clipContent)
{
return;
}
var mask:Quad = this.mask as Quad;
if(mask)
{
mask.x = 0;
mask.y = 0;
mask.width = this.actualWidth;
mask.height = this.actualHeight;
}
else
{
mask = new Quad(1, 1, 0xff00ff);
//the initial dimensions cannot be 0 or there's a runtime error,
//and these values might be 0
mask.width = this.actualWidth;
mask.height = this.actualHeight;
this.mask = mask;
}
}
/**
* @private
*/
protected function layoutGroup_addedToStageHandler(event:Event):void
{
if(this._autoSizeMode == AUTO_SIZE_MODE_STAGE)
{
this.stage.addEventListener(Event.RESIZE, stage_resizeHandler);
}
}
/**
* @private
*/
protected function layoutGroup_removedFromStageHandler(event:Event):void
{
this.stage.removeEventListener(Event.RESIZE, stage_resizeHandler);
}
/**
* @private
*/
protected function layout_changeHandler(event:Event):void
{
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
/**
* @private
*/
protected function child_resizeHandler(event:Event):void
{
if(this._ignoreChildChanges)
{
return;
}
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
/**
* @private
*/
protected function child_layoutDataChangeHandler(event:Event):void
{
if(this._ignoreChildChanges)
{
return;
}
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
/**
* @private
*/
protected function stage_resizeHandler(event:Event):void
{
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
}
}