Menu

Spark Layout

SourceForge Editorial Staff

Spark Layout - Functional and Design Specification


Glossary


  • Post-transform bounds - bounds of an object in object's parent coordinate space, i.e. bounts of the transformed object.

  • Pre-transform bounds - bounds of an object in object's own coordinate space, i.e. bounds of the untransformed object (object's dimensions).

  • Flexible size - size of a layout item that depends on parent size (changes whenever parent size changes), i.e. object with width="100%" has flexible width.

Summary and Background


This spec describes an interface that components/objects need to implement in order to be supported by the Gumbo layouts. This is an internal system interface (i.e. doesn't show up in mxml). This interface is purely a contract between the layout and the layout items.

This interface is not intended to be used by app developers/designers to specify user defined layout properties (i.e. properties like width="100%", minWidth="20", etc.). There will be a separate spec describing user defined layout properties for Gumbo.

Requirements for Gumbo layouts addressed by this spec

  • With Gumbo our new components visuals are separated into the skin and they consist of differently typed objects: GraphicElement, IDisplayObject, UIComponent. To arrange those we need layouts that operate on various types of objects.
  • We would like to enhance the layouts to handle objects with complex transform like rotation, 2.5d, etc. and still make it easy to create new layouts and extend existing ones.

Proposal

  • Define an interface ILayoutElement that will abstract the different objects that need to be sized and arranged from the layout. This interface needs to be such that allows for easy layout of objects with complex transform as well as allowing creation of complex layouts that calculate and set transforms.
  • Implement the ILayoutElement interface for the different objects that the new layouts would support (UIComponent, IFlexDisplayObject, GraphicElement)
  • Implement a factory class that would instantiate the ILayoutElement concrete classes based on the passed in objects (needed by the layouts)

Usage Scenario


  • Implement Gumbo layouts. The layout code will call directly methods on the ILayoutElement interface to arrange elements.

    public class VerticalLayout:ILayout
    {
    public function measure():void
    {
    var width:Number = 0;
    var height:Number = 0;

        var count:int = layoutTarget.numLayoutItems;
        for (var i:int = 0; i < count; i++)
        {
            var el:ILayoutElement = layoutTarget.getLayoutElementAt(i);
            if (!el|| !el.includeInLayout)
                continue;
    
            width = Math.max(width, el.getPreferredBoundsWidth());
            height += el.getPreferredBoundsHeight();
        }
    
        layoutTarget.measuredWidth = width;
        layoutTarget.measuredHeight = height;
    }
    

    }

  • Implement Gumbo components/GraphicElements. Whenever implementing objects that need to be arranged by the Gumbo layouts, those objects need to implement the ILayoutElement interface. In most cases the interface will be implemented in the base classes and developers can choose to override only particular ILayoutElement methods.

    public class CustomComponent extends UIComponent implements ILayoutElement
    {
    public function getPreferredBoundsWidth(postTransform:Boolean=true):Number
    {
    if (!parentCoordinates)
    return getExplicitOrMeasuredWidth;
    return TransformUtil.transformBounds(getExplicitOrMeasuredWidth(),
    getExplicitOrMeasuredHeight(),
    transform.matrix).x;
    }
    }

  • Developers can use the ILayoutElement interface to query for size/position as set by the layout system by calling getLayoutPosition and getLayoutSize after the layout pass has completed.

    public function updateCompleteListener(event:Event)
    {
    var el:ILayoutElement = event.target as ILayoutElement;
    if (!el)
    return;

    // Find the layout bounding box:
    var top:Number = el.getLayoutBoundsX();
    var left:Number = el.getLayoutBoundsY();
    var width:Number = el.getLayoutBoundsWidth();
    var height:Number = el.getLayoutBoundsHeight();
    
    ....
    

    }

Detailed Description


ILayoutElement interface

The ILayoutElement interface represents an object that supports being laid out. Any object that needs to be included in the Gumbo layout, needs to implement ILayoutElement interface. The layout element itself as a concept is a simple x, y axis-aligned bounding box in parent coordinate space. Object complex transforms, rotations, 3D, etc. can be abstracted away by this interface and are taken care of in the ILayoutElement implementations for the given object type (i.e. the implementations are responsible for converting between pre-transfrom and post-transform coordinates) The interface should also expose low-level information such as pre-transform bounds, transfomration matrix.

ILayoutElement needs to expose information that describes layout preferences as supported by the layout system:

  • preferred, min, max bounds sizes (both pre-transfrom and post-trasform). The preferred bounds size represents the object's ideal size in the absence of any constraints. This is similar to the Halo getExplicitOrMeasured() methods. Preferred, min and max sizes must not depend on the current layout size that is set by the layout. Otherwise, repeated layout passes could result in layout sizes continuously changing and lead to infinite loop.
  • percent (optional, parent coordinates): width, height - sizes of child's post-transfrom bounds as a percentage of the parent's pre-transform bounds size that is shared between flexible children.
  • constraints (as set in mxml: optional, parent coordinates): left, top, right, bottom, horizontalCenter, verticalCenter, baseline.

ILayoutElement has to expose methods to allow the layout to position the object

  • setLayoutPosition(x:Number, y:Number, postTransform:Boolean):void, specifies the coordinates of the child's pre/post-transform bounds top-left corner, in parent coordinates.

ILayoutElement has to expose methods to allow the layout to size the object:

In Halo the layout always specifies both width & height of the object. However we need to allow for more general cases, where the width & height of the object bounds may be co-dependent. Examples are:

  • text - changing the number of rows generally changes the number of columns for a fixed text content;
  • transformed objects - for example a rectangle with rotation=60 that has its width+=10 will have both its transformed bounds width and height increased by cos(60)10 and sin(60)10 respectively;
  • sphere - changing the width directly changes the height (if we want to preserve the sphere)
  • object with an option to preserve its aspect ratio.
  • etc.

When a layout determines the size of an object, that size may depend on object's parent dimensions and layout calculations (i.e. both left & right are specified for an object in a canvas layout, or we have percent size in a vertical layout). When an object's width/height, as determined by the layout, has such a dependency we say it's "flexible" since it'll change whenever the size of the parent changes.

In Halo we have a couple of cases for object sizing with several sub-cases

  • If the object is not flexible, we set its size to its preferred size (explicit if present, else measured)
  • If the object is flexible then
    • if only the object's width is flexible - we set the percent width and the preferred height with min/max limits applied
    • if only the object's height is flexible - we set the percent height and the preferred width with min/max limits applied
    • if both the object's width and height are flexible - we set both the percent width and percent height with min/max limits applied

What we propose for the Gumbo ILayoutElement interface API covers the same cases, however it allows ILayoutElement implementations to return different sizes in certain cases. This is new behavior which the new Gumbo layouts need to account for.

The ILayoutElement implementation would:

  • If the object is not flexible - set its size to its preferred size.
  • If the object is flexible then
    • if only the width is flexible - set the object's pre-transformed bounds such that the post-transform bounds have the specified width. In the common cases when the width and height are not co-related, the post-transform height is set to the preferred height (same as Flex3). However, if they are co-related, then the height may be affected by the width and the the ILayoutElement implementation may choose to select different post-transform height.

An example illustrating this is a text label that has width=100% and height unspecified.
<panel title="Pannel" height="300">
<label text="This is a long line of text." width="100%">
</label></panel>
The Gumbo layout would calculate the width in pixels and pass it to the ILayoutElement::setLayoutSize, the implementation will calculate the new height based on the specified width and return the new size.

  • If only the height is flexible - treat similarly to the case above.
  • If both the width & height are flexible - the pre-transform bounds will be calculated so that the post-transform bounds match the width & height. If that's not possible (usually when width and height of the object are co-related we get over constraint), the pre-transform bounds are set to the maximum possible value such that the post-transform bounds are within the specified width & height.

As a consequence the layout will need to deal with the fact that setLayoutSize() could return bounds size different from the specified. There are a couple of options for a general strategy to handle this new behavior (details will be in a separate spec):

  • On updateDisplayList if the new bounds are different from the adjusted, the layout may invalidate again and do a second pass through measure using the new bounds. This is very similar to the way text measure works in Halo and we have a working prototype for the vertical layout. In addition, there are cases where the measured width & height of the layout are co-related as well (i.e. the layout has objects with co-related width & height and some are with flexible width and others with flexible height). Because of those cases we limit the measure passes in the prototype to 2 and we respect the height for the vertical layout rather than the width.
  • We can modify the layout manager to pass down available size recursively during the measure (or return up during updateDisplayList) phase so that the layout can actually size the flexible children and report the correct size… no prototypes of this yet…

Additionally, to allow for custom layouts that explicitly calculate transforms and sizes in child coordinates, the ILayoutElement APIs must be able to work in both post and pre-transform coordinates as well as provide ways to set the layout transformation matrix explicitly.

API Description


/**
 *  The ILayoutElement interface is used primarily by the layout classes to query,
 *  size and position the elements of the GroupBase based containers.
 */
public interface ILayoutElement
{
    /**
     * @copy mx.core.IVisualElement#left
     */
    function get left():Object;

    /**
     * @copy mx.core.IVisualElement#right
     */
    function get right():Object;

    /**
     * @copy mx.core.IVisualElement#top
     */
    function get top():Object;

    /**
     * @copy mx.core.IVisualElement#bottom
     */
    function get bottom():Object;

    /**
     * @copy mx.core.IVisualElement#horizontalCenter
     */
    function get horizontalCenter():Object;

    /**
     * @copy mx.core.IVisualElement#verticalCenter
     */
    function get verticalCenter():Object;

    /**
     * @copy mx.core.IVisualElement#baseline
     */
    function get baseline():Object;

    /**
     * @copy mx.core.IVisualElement#percentWidth
     */
    function get percentWidth():Number;

    /**
     * @copy mx.core.IVisualElement#percentHeight
     */
    function get percentHeight():Number;

    /**
     *  Indicates whether the layout should ignore this element or not.
     */     
    function get includeInLayout():Boolean;

    /**
     *  @return Returns the element's preferred width.  Preferred width is
     *  usually based on the default element size and any explicit overrides.
     *  For UIComponent this is the same as getExplicitOrMeasuredWidth().
     * 
     *  @param postTransform When postTransform is true the method returns
     *  the element's bounding box width.  Bounding box is in element's parent
     *  coordinate space and is calculated from  the element's perferred size and
     *  layout transform matrix.
     *
     *  @see #getPreferredBoundsHeight
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function getPreferredBoundsWidth(postTransform:Boolean = true):Number;

    /**
     *  @return Returns the element's preferred height.  Preferred height is
     *  usually based on the default element size and any explicit overrides.
     *  For UIComponent this is the same as getExplicitOrMeasuredHeight().
     *
     *  @param postTransform When postTransform is true the method returns
     *  the element's bounding box height.  Bounding box is in element's parent
     *  coordinate space and is calculated from  the element's perferred size and
     *  layout transform matrix.
     *
     *  @see #getPreferredBoundsWidth
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function getPreferredBoundsHeight(postTransform:Boolean = true):Number;

    /**
     *  Returns the element's minimum width.
     * 
     *  @param postTransform When postTransform is true the method returns
     *  the element's bounding box width. Bounding box is in element's parent
     *  coordinate space and is calculated from the element's minimum size and
     *  layout transform matrix.
     *
     *  @see #getMinBoundsHeight
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function getMinBoundsWidth(postTransform:Boolean = true):Number;

    /**
     *  Returns the element's minimum height.
     * 
     *  @param postTransform When postTransform is true the method returns
     *  the element's bounding box height. Bounding box is in element's parent
     *  coordinate space and is calculated from the element's minimum size and
     *  layout transform matrix.
     *
     *  @see #getMinBoundsWidth
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function getMinBoundsHeight(postTransform:Boolean = true):Number;

    /**
     *  Returns the element's maximum width.
     * 
     *  @param postTransform When postTransform is true the method returns
     *  the element's bounding box width. Bounding box is in element's parent
     *  coordinate space and is calculated from the element's maximum size and
     *  layout transform matrix.
     *
     *  @see #getMaxBoundsHeight
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function getMaxBoundsWidth(postTransform:Boolean = true):Number;

    /**
     *  Returns the element's maximum height.
     * 
     *  @param postTransform When postTransform is true the method returns
     *  the element's bounding box height. Bounding box is in element's parent
     *  coordinate space and is calculated from the element's maximum size and
     *  layout transform matrix.
     *
     *  @see #getMaxBoundsWidth
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function getMaxBoundsHeight(postTransform:Boolean = true):Number;

    /**
     *  Returns the element's layout width. This is the size that the element uses
     *  to draw on screen.
     *
     *  @param postTransform When postTransform is true the method returns
     *  the element's bounding box width. Bounding box is in element's parent
     *  coordinate space and is calculated from the element's layout size and
     *  layout transform matrix.
     *
     *  @see #getLayoutBoundsHeight
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function getLayoutBoundsWidth(postTransform:Boolean = true):Number;

    /**
     *  Returns the element's layout height. This is the size that the element uses
     *  to draw on screen.
     *
     *  @param postTransform When postTransform is true the method returns
     *  the element's bounding box width. Bounding box is in element's parent
     *  coordinate space and is calculated from the element's layout size and
     *  layout transform matrix.
     *
     *  @see #getLayoutBoundsWidth
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function getLayoutBoundsHeight(postTransform:Boolean = true):Number;

    /**
     *  Returns the x coordinate that the element uses to draw on screen.
     *
     *  @param postTransform When postTransform is true the method returns
     *  x coordinate of the element's bounding box top-left corner.
     *  Bounding box is in element's parent coordinate space and is calculated
     *  from the element's layout size, layout position and layout transform matrix.
     * 
     *  @see #getLayoutBoundsY
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function getLayoutBoundsX(postTransform:Boolean = true):Number;

    /**
     *  Returns the y coordinate that the element uses to draw on screen.
     *
     *  @param postTransform When postTransform is true the method returns
     *  y coordinate of the element's bounding box top-left corner.
     *  Bounding box is in element's parent coordinate space and is calculated
     *  from the element's layout size, layout position and layout transform matrix.
     * 
     *  @see #getLayoutBoundsX
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function getLayoutBoundsY(postTransform:Boolean = true):Number;

    /**
     *  Sets the coordinates that the element uses to draw on screen.
     *
     *  @param postTransform When postTransform is true, the element is positioned
     *  in such a way that the top-left corner of its bounding box is (x, y).
     *  Bounding box is in element's parent coordinate space and is calculated
     *  from the element's layout size, layout position and layout transform matrix.
     *
     *  Note that calls to setLayoutBoundsSize can affect the layout position, so 
     *  setLayoutBoundsPosition should be called after setLayoutBoundsSize.
     *
     *  @see #setLayoutBoundsSize
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function setLayoutBoundsPosition(x:Number, y:Number, postTransform:Boolean = true):void;

    /**
     *  Sets the layout size to the specified dimensions.  This is the size that
     *  the element uses to draw on screen.
     *  
     *  If one of the dimensions is left unspecified (NaN), it's size
     *  will be picked such that element can be optimally sized to fit the other
     *  dimension.  This is useful when the caller doesn't want to 
     *  overconstrain the element, for example when the element's width and height
     *  are corelated (text, components with complex transforms, etc.)
     *  If both dimensions are left unspecified, the element will have its layout size
     *  set to its preferred size.
     * 
     *  <code>setLayoutBoundsSize</code> does not clip against minium or maximum sizes.
     *
     *  Note that calls to setLayoutBoundsSize can affect the layout position, so 
     *  setLayoutBoundsSize should be called before setLayoutBoundsPosition.
     *
     *  @param width The target width.
     *
     *  @param height The target height.
     *
     *  @param postTransform When postTransform is true, the specified dimensions
     *  are those of the element's bounding box.
     *  Bounding box is in element's parent coordinate space and is calculated
     *  from the element's layout size, layout position and layout transform matrix.
     * 
     *  @see #setLayoutBoundsPosition
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function setLayoutBoundsSize(width:Number = NaN,
                                 height:Number = NaN,
                                 postTransform:Boolean = true):void;

    /**
     *  Returns the layout transform Matrix for this element.
     *  Don't directly modify the return value but call setLayoutMatrix instead. 
     */
    function getLayoutMatrix():Matrix;

    /**
     *  Sets the transform Matrix that is used to calculate the component's layout
     *  size and position relative to its siblings.
     *
     *  Note that layout Matrix is factored in the getPreferredSize(),
     *  getMinSize(), getMaxSize(), getLayoutSize() when computed in parent coordinates
     *  as well as in getLayoutPosition() in both parent and child coordinates.
     *
     *  <p>The method is typically used by layouts that calculate the transform
     *  matrix explicitly and work with sizes in child coordinates, ingnoring
     *  getLayoutPosition.  Calling this method does not cause a subsequent layout
     *  pass (doesn't invalidate element's parent size or display list).</p>
     * 
     *  @see #setLayoutMatrix3D
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function setLayoutMatrix(m:Matrix):void;

    /**
     *  Returns the layout transform Matrix3D for this element.
     *  Don't directly modify the return value but call setLayoutMatrix instead. 
     */
    function getLayoutMatrix3D():Matrix3D;

    /**
     *  Sets the transform Matrix3D that is used to calculate the component's layout
     *  size and position relative to its siblings.
     *
     *  Note that layout Matrix3D is factored in the getPreferredSize(),
     *  getMinSize(), getMaxSize(), getLayoutSize() when computed in parent coordinates
     *  as well as in getLayoutPosition() in both parent and child coordinates.
     *
     *  <p>The method is typically used by layouts that calculate the transform
     *  matrix explicitly and work with sizes in child coordinates, ingnoring
     *  getLayoutPosition.  Calling this method does not cause a subsequent layout
     *  pass (doesn't invalidate element's parent size or display list).</p>
     * 
     *  @see #setLayoutMatrix3D
     *  @see mx.core.UIComponent#layoutMatrix
     *  @see mx.core.UIComponent#layoutMatrix3D
     */
    function setLayoutMatrix3D(m:Matrix3D):void;
}

B Features


Additional Implementation Details


Prototype Work


  • ILayoutElement interface
  • ILayoutElementUIC (ILayoutElement implemented for UIComponent)
    • works with arbitrary transform matrices except for case when specifying arbitrary sizes (in those cases the prototype supports only rotation)
  • Modified the Gumbo VerticalLayout to work with the ILayoutElement interface.
    • The prototype uses the two-pass measure strategy to solve the co-related post-transform bounds width/height problem for its flexible width items.
  • Modified the Gumbo BasicLayout to work with the ILayoutElement interface.
    • The prototype doesn't have code for two-pass measure strategy.
  • 3D wheel layout

Compiler Work


No compiler changes required by this feature.

Web Tier Compiler Impact


No impact by this feature.

Flex Feature Dependencies


Backwards Compatibility


Syntax changes

No syntax changes for this feature.

Behavior

Since these layouts will operate only within the new Gumbo components, there should be no behavior/performance implications to existing application.

Performance


ILayoutElement as an wrapper object

We've decided not to use ILayoutElement as an wrapper object.

  • For certain objects ILayoutElement interface could be implemented via wrapper helper classes - ILayoutElementUIC works like this. Do we create/destroy those objects every time the layout manager does a measure/update pass, or does the layout keep those around between measure/update passes?
    • We have decided to implement the interface directly by the object. No wrappers will be used.
  • We could provide optimized implementations of ILayoutElement for the most common cases (i.e. no complex transforms, fixed explicit sizes).

What's the impact of using Point in the interface vs. simple types? How does this affect the garbage collector?

We've decided not to use Point base interface, but have pair of methods returning width and height.

  • I took profiles of a scenarios with ~10,000 measure and 16,000 updateDisplayList of a lot of buttons, most of those with non-complex transforms. The profiles didn't show any noticable difference above margin of error for interface using Point and one returning simple types. After timing the same scenario in a tight loop I found that the non-Point returning interface was ~2.5% faster. This explains why I couldn't see that difference with the profiler, since measure and updateDisplayList took 20% of the app time hence the app time delta was 0.5%. Memory usage was the same.
  • We could provide an optional parameter to pass in an already allocated Point as an out parameter to minimize the allocation of temporary objects.

Related

Wiki: Flex 4
Wiki: Gumbo Component Architecture

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.