Spark Vertical & Horizontal Layout - Functional and Design Specification


Summary and Background


In the Spark architecture, the containers and the layouts are separate objects. With Spark, we are providing generic containers, custom layouts as well as a few stock layouts with functionality that is parallel to the most-commonly used Halo layout containers.

This spec describes the HorizontalLayout and VerticalLayout, which are the Spark counterparts of the Box layout.

Goals:

  • Provide layout behavior on par with Halo Box container.
  • Takes advantage of the ILayoutElement interface to support 2D xforms on the elements.
  • Scrolling support (step up / left / right / down, page up / left / right / down, home / end).
  • Layout calculations place elements on pixel boundaries provided that the input is in integer numbers.

Non-goals:

  • Preserve the absolutely exact behavior of the Halo HBox/VBox container.

Covered separately:

  • virtualization - HorizontalLayout, VerticalLayout virtualization support is covered in a separate spec and is tracked by a separate feature Spark Virtualization .

Usage Scenarios


  • Arrange children horizontally / vertically left-to-right / top-to-bottom.
  • Distribute width / height of container between the children.
  • Create a List with horizontal layout (the default is vertical)

Detailed Description


Both HorizontalLayout and VerticalLayout extend LayoutBase. HorizontalLayout is the default layout for HGroup, and VerticalLayout is the default layout for VGroup and List. The HorizontalLayout arranges the container's elements in left-to-right order. The VerticalLayout arranges the container's elements in top-to-bottom order. Both layouts respect the value of the element's includeInLayout property. Both layouts support gaps between the elements and padding area between the container borders and the elements.

HorizontalLayout properties

  • gap - read / write, default is 6, non-bindable - the vertical gap between the elements.
  • paddingLeft, paddingRight, paddingTop, paddingBottom - read/write, default is 0, non-bindable - the padding between the container border and the elements. The padding is simply white space that is part of the content area and as such gets clipped and scrolled (scrolling the top element out of view will scroll the paddingTop space out of view as well).
  • columnCount - read only, bindable - the number of visible elements (each element occupies a single column).
  • verticalAlign - read / write, default is "top", non-bindable - specifies the way the elements are aligned in the horizontal direction. Options are:
    • top / bottom / middle - positions the element to the top, to the bottom, or in the middle relative to the container's contentHeight
    • contentJustify - all elements are stretched to the maximum of the content height and the container height.
    • justify - all elements are stretched to the container height.
  • requestedColumnCount - read / write, non-bindable, default is -1 - specifies the number of columns that the layout should reserve space for when calculating the container default size during measure(). If there are not enough elements in the container, the space for the excess number of columns is calculated based on the typical element.
  • variableColumnWidth - read / write, non-bindable, default is true - specifies whether all elements (columns) should have the same width.
  • columnWidth - read / write, non-bindable, default is the preferred width of the current typical element (see typical element in the glossary section at beginning of the spec). Setting to a number overrides the default, setting to NaN restores the default. columnWidth has effect only when variableColumnWidth is set to false.
  • firstIndexInView - read only, bindable - returns the index of the first element within the scrollrect of the container.
  • lastIndexInView - read only, bindable - returns the index of the last element within scrollrect of the container.

VerticalLayout properties

  • gap - read / write, default is 6, non-bindable - the vertical gap between the elements.
  • paddingLeft, paddingRight, paddingTop, paddingBottom - read/write, default is 0, non-bindable - the padding between the container border and the elements. The padding is simply white space that is part of the content area and as such gets clipped and scrolled (scrolling the top element out of view will scroll the paddingTop space out of view as well).
  • rowCount - read only, bindable - the number of visible elements (each element occupies a single row).
  • horizontalAlign - read/write, default is "left", non-bindable - specifies the way the elements are aligned in the horizontal direction. Options are:
    • left / right / center - positions the element to the left, to the right, or in the center relative to the container's contentWidth
    • contentJustify - all elements are stretched to the maximum of the content width and the container width.
    • justify - all elements are stretched to the container width.
  • requestedRowCount - read / write, non-bindable, default is -1 - specifies the number of rows that the layout should reserve space for when calculating the container default size during measure(). If there are not enough elements in the container, the space for the excess number of rows is calculated based on the typical element.
  • variableRowHeight - read / write, non-bindable, default is true - specifies whether all elements (rows) should have the same height.
  • rowHeight - read / write, non-bindable, default is the preferred height of the current typical element (see typical element in the glossary section at beginning of the spec). Setting to a number overrides the default, setting to NaN restores the default. rowHeight has effect only when variableRowHeight is set to false.
  • firstIndexInView - read only, bindable - returns the index of the first element within the scrollrect of the container.
  • lastIndexInView - read only, bindable - returns the index of the last element within scrollrect of the container.

HorizontalLayout/VerticalLayout public methods

  • fractionOfElementInView(index:int):Number - returns what's the fraction (from 0 to 1) of the specified element that's within the scrollRect. This method is useful as the layouts perform the calculations very efficiently and is very useful especially for virtualization scenarios where the element (the item renderer) is not always readily available.

Calculating the Default Size - measure()

The HorizontalLayout's major direction/axis is horizontal. The VerticalLayout's major direction / axis is vertical.
To calculate the measured size, both layouts sum up the element's preferred sizes along the major direction and also find the maximum of the element's preferred sizes along the minor direction. Properties like requestedRowCount, requestedColumnCount, varialbleColumnWidth, variableRowHeight, columnWidth, rowHeight, gap and padding are taken into account:

  • Only the first requestedRowCount/requestedColumnCount number of elements are visited
  • If variableColumnWidth / variableRowHeight is set to false, then instead of using the element's preferred size, the layouts use the value of the columnWidth / rowHeight property as defined above.
  • gap is inserted between every two consecutive elements
  • padding is added on all sides of the area containing the elements.

To calculate the measured minimum size, both layouts follow the same steps as for the measured size, with the only difference that if the element's size is constrained to the container size, then the layouts take into account the element's minimum size instead of the element's preferred size. For example:

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
                xmlns:s="library://ns.adobe.com/flex/spark" 
                xmlns:mx="library://ns.adobe.com/flex/halo"
                width="500" height="500">
    <s:Group id="g">
        <s:layout>
            <s:VerticalLayout/>
        </s:layout>
        <s:Button width="50" minWidth="25"/>
        <s:Button width="100%" minWidth="30"/>
    </s:Group>
</s:Application>

when the Group "g" calculates the measured width, it will be the maximum of the first button's preferred width (50) and second button's preferred width (70, which is the default button size) yielding the total measured with as 70.
When the Group calculates the measured minimum width, it will be the maximum of the first buttons's preferred width (50) and the second button's minimum width (30), yielding the total minimum measured width as 50. This is so because the first button is not constrained by the parent size and it will always be sized to 50 and therefore its minimum size is irrelevant to the layout.

Size and Arrange Elements - updateDisplayList(width, height)

Both VerticalLayout, HorizontalLayout size and position the elements very similarly.

Some calculations are based on the content width / height of the container (along the minor axis). In these cases the layouts calculate in advance the content width / height as the maximum of element's size, where element's size is:

  • Percent of container width / height (minus the padding), if element's percentWidth / percentHeight is set, respecting element's min / max width / height.
  • Element's preferred size.

The size of the elements along the major axis is calculated according to the following order of precedence:

  • If variableRowHeight / variableColumnWidth is false, then set the height / width to the value of the rowHeight / columnWidth property
  • If the element's percentHeight / percentWidth is set, then calculate the element's height / width based on the major axis percent distribution algorithm outlined below.
  • Set the height / width to the element's preferred size.

The size of the elements along the minor axis is calculate according to the following order of precedence:

  • If horizontalAlign / verticalAlign is "justify" then set the element's width / height to the container width/height
  • If horizontalAlign / verticalAlign is "contentJustify" then set the element's width / height to the maximum of the container's width / height and container's content width and content height.
  • If the element's percentWidth / percentHeight is set, then calculate the element's width / height as a percentage of the container's width, but respecting the element's min and max sizes.

Distribution of percent size along the major axis - when there are several elements with percent size along the major axis - for example three elements with percentHeight in a VerticalLayout - then the layout calculates the actual sizes as normalized percentages of the available space according to the following steps:

  • Available space is the container major axis size (height for VerticalLayout) less the gaps between the elemtns, and less the size of the elements that don't have percent size along the major axis.
  • Percentage is normalized - each element's percentage is calculated by its percentHeight / percentWidth setting divided by the total sum of precentHeight / precentWidth for all elements.
  • Each element is assigned the height / width that is percentage of the available space. If the element's minimum / maximum limit is reached, then the excess space is re-distributed between the rest of the percentage sized elements (this part of the layouts is code shared between the Halo box layout and the HorizontalLayout / VerticalLayout).

Positioning the elements - the layouts determine the major axis position (y coordinate for the VerticalLayout) by arranging the elements top-to-bottom (VerticalLayout) or left-to-right (HorizontalLayout), taking into account the gap setting as well as the size of the elements as determined by the steps outlined above.

The position along the minor axis (x coordinate for the VerticalLayout) is determined according to the alignment property (horizontalAlign for the VerticalLayout):

  • If the alignment is justify or contentJustify then position as if the align is "left"
  • If left / right / center, then the element is positioned such that its left edge / right edge / middle is aligned with the left edge / right edge / middle of the content area and then padding is added.

Finally, the content size is defined as:

  • container.contentWidth = Max(childX + childWidth) + padding
  • container.contentHeight = Max(childY + childHeight) + padding

Scrolling support

VerticalLayout / HorizontalLayout inherit the scrolling along the minor axis from LayoutBase. The scrolling along the major axis work on per-element basis regarding the scrollRect:

  • ScrollBar steps try to snap in view the first/last element that is currently not fully in view. For example in a VerticalLayout ScrollUnit.UP will try to align the scrollRect's top edge to the top edge of the first partially visible element (or if there's none, then the first element before the first fully visible element).
  • ScrollBar pages try to snap in view the last/first element that is not fully in view. For example in a VerticalLayout ScrollUnit.PAGE_UP will try to align the scrollRect's bottom edge to the bottom edge of the first partially visible element (of if there's none, then the first element before the first fully visible element).
  • When there's positive padding, scrolling before the first element or after the last element causes the layout to scroll into the whitespace defined by the padding. The padding before / after the first / last element is treated as a single virtual element in regards to scrolling.

In all cases while scrolling, the scrollRect is moved no more than its width/height. For example if there's a very large item that is way taller than the scrollRect and its bottom edge is visible, it will take multiple ScrollUnit.PAGE_UP scrolls to reach the top edge of the element (in a VerticalLayout).

Refer to the LayoutBase spec for details on the scrolling APIs.

Navigation

VerticalLayout / HorizontalLayout implement the LayoutBase method getDestinationIndex().
This method returns the index of the destination item, based on the index of the current item and navigation uint. The navigation units supported are home, end, left, right, up, down, page up/down/left/right.

The behavior for VerticalLayout is:

  • left/right - nowhere to go to, return -1
  • home/end - return the index of the first/last item
  • up/down - return the index of the previous/next item
  • page up/down
    • if the index of the current item is in view and is not the first/last fully-visible item in view, then return the first/last fully-visible item in view (respectively for page up/down).
    • else, return the index of the item that is one page up/down (to get the destination index, subtract/add to the current index until the distance, including the items, between the current index and the destination index is the viewport height).
  • page left/right - nowhere to go to, return -1

The behavior for the HrozontalLayout is similar, with the exception that page left/right is treated the same as page up/down and left/right and up/down are swapped. Also page left/right/up/down are implemented in terms of viewport width.

Pixel Boundaries and Rounding

The VerticalLayout and HorizontalLayout take care to perform rounding for any computation that may yield fractional numbers, assuming that the inputs are always integer numbers:

  • childWidth from width and percentWidth is rounded
  • childHeight from height and percentHeight is rounded
  • childX from width and horizontalAlign = center is rounded
  • childY from height and verticalAling = middle is rounded
  • distribution of width / height

API Description


package spark.layout
{

public class HorizontalLayout extends LayoutBase
{
    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------
    
    //----------------------------------
    //  gap
    //----------------------------------
    /**
     *  The horizontal space between layout elements.
     * 
     *  Note that the gap is only applied between layout elements, so if there's
     *  just one element, the gap has no effect on the layout.
     * 
     *  @default 6
     */    
    public function get gap():int
    public function set gap(value:int):void
    
    //----------------------------------
    //  columnCount
    //----------------------------------

    [Bindable("propertyChange")]

    /**
     *  Returns the current number of elements in view.
     * 
     *  @default -1
     */
    public function get columnCount():int

    /**
     *  @private
     * 
     *  Sets the <code>columnCount</code> property and dispatches
     *  a PropertyChangeEvent.
     */
    private function setColumnCount(value:int):void
        
    //----------------------------------
    //  paddingLeft
    //----------------------------------

    /**
     *  Number of pixels between the container's left border
     *  and the left edge of the first layout element.
     * 
     *  @default 0
     */
    public function get paddingLeft():Number
    public function set paddingLeft(value:Number):void
    
    //----------------------------------
    //  paddingRight
    //----------------------------------

    /**
     *  Number of pixels between the container's right border
     *  and the right edge of the last layout element.
     * 
     *  @default 0
     */
    public function get paddingRight():Number
    public function set paddingRight(value:Number):void
    
    //----------------------------------
    //  paddingTop
    //----------------------------------

    /**
     *  All layout elements will have at least this much space
     *  above them. 
     * 
     *  @default 0
     */
    public function get paddingTop():Number
    public function set paddingTop(value:Number):void
    
    //----------------------------------
    //  paddingBottom
    //----------------------------------

    /**
     *  All layout elements will have at least this much space
     *  below them. 
     * 
     *  @default 0
     */
    public function get paddingBottom():Number
    public function set paddingBottom(value:Number):void
    
    //----------------------------------
    //  requestedColumnCount
    //----------------------------------

    /**
     *  The measured size of this layout will be big enough to display 
     *  the first <code>requestedColumnCount</code> layout elements. 
     * 
     *  If <code>requestedColumnCount</code> is -1, then the measured
     *  size will be big enough for all of the layout elements.
     * 
     *  This property implies the layout target's <code>measuredWidth</code>.
     * 
     *  If the actual size of the <code>target</code> has been explicitly set,
     *  then this property has no effect.
     * 
     *  @default -1
     */
    public function get requestedColumnCount():int
    public function set requestedColumnCount(value:int):void
    
    //----------------------------------
    //  columnWidth
    //----------------------------------
    
    /**
     *  If variableColumnWidth="false" then 
     *  this property specifies the actual width of each layout element.
     * 
     *  If variableColumnWidth="true" (the default), then this property
     *  has no effect.
     * 
     *  The default value of this property is the preferred width
     *  of the typicalLayoutElement.
     */
    public function get columnWidth():Number
    public function set columnWidth(value:Number):void

    //----------------------------------
    //  variableColumnWidth
    //----------------------------------

    /**
     *  Specifies that layout elements are to be allocated their 
     *  preferred width.
     * 
     *  Setting this property to false specifies fixed width columns.
     * 
     *  If false, the actual width of each layout element will be 
     *  the value value of <code>columnWidth</code>.
     * 
     *  Setting this property to false causes the layout to ignore 
     *  layout elements' percentWidth.
     * 
     *  @default true
     */
    public function get variableColumnWidth():Boolean
    public function set variableColumnWidth(value:Boolean):void
    
    //----------------------------------
    //  firstIndexInView
    //----------------------------------

    [Bindable("indexInViewChanged")]    

    /**
     *  The index of the first column that's part of the layout and within
     *  the layout target's scrollRect, or -1 if nothing has been displayed yet.
     * 
     *  Note that the column may only be partially in view.
     * 
     *  @see lastIndexInView
     *  @see fractionOfElementInView
     */
    public function get firstIndexInView():int
    
    //----------------------------------
    //  lastIndexInView
    //----------------------------------

    [Bindable("indexInViewChanged")]    

    /**
     *  The index of the last column that's part of the layout and within
     *  the layout target's scrollRect, or -1 if nothing has been displayed yet.
     * 
     *  Note that the column may only be partially in view.
     * 
     *  @see firstIndexInView
     *  @see fractionOfElementInView
     */
    public function get lastIndexInView():int
    
    //----------------------------------
    //  verticalAlign
    //----------------------------------

    [Inspectable(category="General", enumeration="top,bottom,middle,
    justify,contentJustify", defaultValue="top")]

    /** 
     *  Vertical alignment of layout elements.
     * 
     *  If the value is one of "bottom", "middle", "top"  then the 
     *  layout element is aligned relative to the target's contentHeight.
     * 
     *  If the value is "contentJustify" then the layout element's actual
     *  height is set to the contentHeight.
     * 
     *  If the value is "justify" then the layout element's actual height
     *  is set to the target's height.
     *
     *  This property does not affect the layout's measured size.
     *  
     *  @default "top"
     */
    public function get verticalAlign():String
    public function set verticalAlign(value:String):void

    /**
     *  An index is "in view" if the corresponding non-null layout element is 
     *  within the horizontal limits of the layout target's scrollRect
     *  and included in the layout.
     *  
     *  Returns 1.0 if the specified index is completely in view, 0.0 if
     *  it's not, and a value in between if the index is partially 
     *  within the view.
     * 
     *  If the specified index is partially within the view, the 
     *  returned value is the percentage of the corresponding
     *  layout element that's visible.
     * 
     *  Returns 0.0 if the specified index is invalid or if it corresponds to
     *  null element, or a ILayoutElement for which includeInLayout is false.
     * 
     *  @return the percentage of the specified element that's in view.
     *  @see firstIndexInView
     *  @see lastIndexInView
     */
    public function fractionOfElementInView(index:int):Number 
}
}

Examples and Usage


Example from the Spark VideoPlayer skin:

<s:SparkSkin xmlns:fx="http://ns.adobe.com/mxml/2009" 
   xmlns:s="library://ns.adobe.com/flex/spark">

    <!-- host component -->
    <fx:Metadata>
        [HostComponent("spark.components.VideoPlayer")]
    </fx:Metadata>
    
    <!-- states -->
    <s:states>
        <s:State name="buffering" />
        <s:State name="connectionError" />
        <s:State name="disabled" />
        <s:State name="disconnected" />
        <s:State name="loading" />
        <s:State name="paused" />
        <s:State name="playing" />
        <s:State name="seeking" />
        <s:State name="stopped" />
    </s:states>
    
    <s:layout> 
        <s:VerticalLayout horizontalAlign="center"/> 
    </s:layout>
    
    <s:VideoElement id="videoElement" />
    
    <s:HSlider id="scrubBar" />
    
    <s:Group>
        <s:layout>
            <s:HorizontalLayout />
        </s:layout>
        
        <s:Button id="stopButton" label="Stop" />
        
        <s:ToggleButton id="playPauseButton" skinClass="spark.skins.default.VideoPlayerPlayPauseButtonSkin" label="Play" />
        
        <s:ToggleButton id="muteButton" 
             skinClass="spark.skins.default.VideoPlayerMuteButtonSkin" 
             label="Mute" />
        
        <s:VSlider id="volumeBar" liveDragging="true" height="21" 
             valueInterval=".1" />
        
        <s:Button id="fullScreenButton" label="Fullscreen" />
    </s:Group>
</s:SparkSkin>

Example from the Spark List skin:

<s:SparkSkin xmlns:fx="http://ns.adobe.com/mxml/2009" 
          xmlns:s="library://ns.adobe.com/flex/spark"
	  minWidth="112" minHeight="112"
	  alpha.disabled="0.5"> 
	
    <fx:Metadata>
    	[HostComponent("spark.components.List")]
    </fx:Metadata> 
    
    ......

    <s:Scroller left="1" top="1" right="1" bottom="1" id="scroller">
	    <s:DataGroup id="dataGroup" 
             itemRenderer="spark.skins.default.DefaultItemRenderer">
	    	<s:layout>
	    	    <s:VerticalLayout gap="0" horizontalAlign="contentJustify" />
	    	</s:layout>
	    </s:DataGroup>
    </s:Scroller>
</s:SparkSkin>

B features

HorizontalLayout

Two new properties:

  • requestedMinColumnCount - read / write, default is 0, non-bindable - specifies the minimum number of columns the the layout should reserve space for when calculating the container default size during measure(). If there are not enough elements in the container, the space for the excess number of columns is calculated based on the typical element. This property has no effect when requestedColumnCount is explicitly specified.
  • requestedMaxColumnCount - read / write, default is int.MAX_VALUE, non-bindable - specifies the maximum number of columns the the layout should reserve space for when calculating the container default size during measure(). If there are not enough elements in the container, the space for the excess number of columns is calculated based on the typical element. This property has no effect when requestedColumnCount is explicitly specified. requestedMaxColumnCount trumps requestedMinColumnCount.

VerticalLayout

Two new properties:

  • requestedMinRowCount - read / write, default is 0, non-bindable - specifies the minimum number of rows the layout should reserve space for when calculating the container default size during measure(). If there are not enough elements in the container, the space for the excess number of columns is calculated based on the typical element. This property has no effect when requestedRowCount is explicitly specified.
  • requestedMaxRowCount - read / write, default is int.MAX_VALUE, non-bindable - specifies the maximum number of rows the layout should reserve space for when calculating the container default size during measure(). If there are not enough elements in the container, the space for the excess number of columns is calculated based on the typical element. This property has no effect when requestedRowCount is explicitly specified. requestedMaxRowCount trumps requestedMinRowCount.

Additional Implementation Details


Prototype Work


Both HorizontalLayout and VerticalLayout exist and are in almost complete state in the trunk.

Compiler Work


No.

Web Tier Compiler Impact


No.

Flex Feature Dependencies


Dependencies on other Flex features.

Backwards Compatibility


No.

Accessibility


Describe any accessibility considerations.

Performance


Globalization


No issues.

Localization


Compiler Features

No command-line params, warnings, errors, etc.

Framework Features

No RTE messages.

No UI text, images, skins, sounds.

Issues and Recommendations


Documentation


Describe any documentation issues or any tips for the doc team.

QA


If there are testing tips for QA, note them here, include a link to the test plan document.


You must be logged in to comment.

Apologies if this is not the correct place.
There is no mention here of the effect that switching from a state that is BasicLayout to a state that is Vertical/Horizontal layout .

It would appear that the x , y properties
are lost meaning that returning to the State that is BasicLayout the 'layout' is lost .

see http://bugs.adobe.com/jira/browse/SDK-21302 fmi.

How do I vertically align a VGroup? How do I horizontally align an HGroup? These layout options used to be available in the halo equivalents: HBox had horizontalAlign, VBox had verticalAlign.

Now VGroup only has horizontalAlign and HGroup only has verticalAlign.

I can write twice as much code and wrap an HGroup with a VGroup - or vice versa - but this seems like such a terrible solution that I hope its not the officially supported solution for all the "improvements" made in for spark.

I am in the same boat as Matthew. An intensive search on the internets does little to answer this question.

Is there currently a standard way of aligning elements vertically and horizontally using only one parent container?

I agree with Matthew. Please bring back the verticalAlign property to VGroup and horizontalAlign property to HGroup.

I can't think of any logical reason to remove this properties in the first place...

I also agree with the posters above. I haven't been using Flex very long and I'm sad to say that I can do a nice layout in Java much faster than I can in Flex 4 (you can take that as an insult if you like). I realize the layout / skinning system in Flex 4 can be very powerful, but I don't want to spend my time writing custom layout code (ever).

I've read articles on improving UI performance and a lot of them say to avoid nesting containers. Now I have to nest a ton of containers just to get some basic layouts set up so things look half decent. What gives?

Posted by Ryan J at Jan 15, 2010 08:11

Actually, major axis alignment (verticalAlign on VGroup/VerticalLayout and horizontalAlign on HGroup/HorizontalLayout) was recently added. See http://bugs.adobe.com/jira/browse/SDK-24416 for details. The spec hasn't been updated yet.