Menu

Spark TileLayout

SourceForge Editorial Staff

Summary and Background


The TileLayout is intended to allow developers to get the layout functionality of the Halo Tile container as a Gumbo assignable layout class. That is to layout elements in equally-sized cells in rows and columns.
TileLayout can be used with any Gumbo GroupBase derived container.

Usage Scenarios


Jane is using the new Gumbo classes and she's looking for a Halo-like Tile container but with the functionality of List. She looks through the possible Gumbo layouts and tries to set the List layout as TileLayout. It works!

John is creating a custom skin and he wants to arrange elements in NxN grid equally sized cells. He adds a Group to contain the elements, then sets the layout to be TileLayout. He adjusts the gaps, tweaks the cell sizes. It works.

Sizing Settings and Scenarios

Here's a high-level list of options that determine the number or columns, number of rows, column width, row height and horizontal gap and vertical gap:

  1. Set explicit number of columns and/or rows.
  2. Set explicit column width and/or row height.
  3. Set explicit gap size
  4. Justify columns and/or rows
    1. Don't justify
    2. Gap grows so that all the fully visible elements add up exactly to the viewport width/height. In case there's a single fully visible element and a second partially visible element, the gap will grow to push the partially visible element out of view.
    3. Column width / row height grows so that all the fully visible columns/rows add up exactly to the viewport width/height.

Using one or two options from the above list address a number of scenarios. There are, however, a few scenarios which may require a greater number of settings combinations used:

  • Have the tile layout always slice a rectangular area in N rows and/or M columns with runtime-determined container width/height
    • Set number of columns/rows to N/M.
    • Set justify to columnWidth/rowHeight - this way column width / row height will grow to occupy the container width/height.
    • Note that justify only grows the columnWidth/rowHeight, so to handle cases where the size of the columns/rows has to shrink, the default column width/row height should set explicitly to zero.

Detailed Description


TileLayout arranges element on screen in rows and columns of equally-sized cells.
The TileLayout performs two major functions - determine measured size of the layout target (container) and layout target's elements in rows and columns of equally-sized cells.

Interactive Embedded Prototype

Note the I've updated the prototype to use List and therefore setting only one of explicit width/explicit height will not affect the measured height/width. This is documented in the issues section.

Non-bindable properties:

  • orientation
    • "rows" - default, elements are arranged row by row, left to right, top to bottom.
    • "column" - elements are arranged in column by column, top to bottom, left to right.
  • elementHorizontalAlign - the way elements are aligned within the cells in the horizontal direction.
    • "justify" - make elements width same as the cell width.
    • "left" - align elements with the left edge of the cell.
    • "center" - place elements in the center of the cell, horizontally.
    • "right" - align elements with the right edge of the cell.
  • elementVerticalAlign - the way elements are aligned within the cells in the vertical direction.
    • "justify" - make elements height same as the cell height.
    • "top" - align elements with the top edge of the cell.
    • "middle" - place elements in the middle of the cell, vertically.
    • "bottom" - align elements with the bottom edge of the cell.
  • justifyColumns
    • "none" - default, no justification.
    • "gapSize" - gap between columns grows (from the default setting) so that the last fully-visible column right edge aligns with the viewport right edge. In case of a single fully-visible column and a partially visible column, the gap will grow enough to push the partially visible column just beyond the viewport right edge.
    • "columnSize" - the column width grows so that the last fully-visible column right edge aligns with the viewport right edge.
  • justifyRows
    • "none" - default, no justification.
    • "gapSize" - gap between rows grows (from the default setting) so that the last fully-visible row bottom edge aligns with the viewport bottom edge. In case of a single fully-visible row and a partially visible row, the gap will grow enough to push the partially visible row just beyond the viewport bottom edge.
    • "rowSize" - the row height grows so that the last fully-visible row bottom edge aligns with the viewport bottom edge.

Bindable properties:

TileLayout calculates a number of properties in updateDisplayList - the number of rows and columns, the size of a single cell, etc. These properties depend on one another and for each the user can specify an explicit override. At the end of updateDisplayList, the TileLayout dispatches events (only if there are any listeners).

  • columnWidth - height of a single column
    • set - sets explicit override for the column width
    • get - returns the current columnWidth
  • rowHeight - height of a single row
    • set - sets explicit override for the row height
    • get - returns the current rowHeight
  • horizontalGap - number of pixels between columns of cells.
    • set - sets explicit override for the gap between the columns
    • get - returns the current gap between the columns (useful when jistifyColumns is true)
  • verticalGap - number of pixels between rows of cells.
    • set - sets explicit override for the gap between the rows
    • get - returns the current gap between the rows (useful when jistifyRows is true)
  • columnCount - number of columns of cells
    • set - sets explicit override for the number of columns
    • get - If no explicit override, returns the number of columns in the TileLayout that is enough to fit all cells (depends on the number of rows). In case both rowCount and columnCount are set, the getter for the property in the minor direction will return a calculated value based on the number of elements.
  • rowCount - number of rows of cells
    • set - sets explicit override for the number of rows
    • get - if no explicit override, returns the number of rows in the TileLayout that is enough to fit all cells (depends on the number of columns). In case both rowCount and columnCount are set, the getter for the property in the minor direction will return a calculated value based on the number of elements.

Calculating bindable properties values

Parameters are totalWidth, totalHeight which could be any number or NaN. During updateDisplayList, totalWidth and totalHeight are the unscaledWidth, unscaledHeight parameters, during measure totalWidth and totalHeight are the explicitWidth and explicitHeight overrides of the container.

  • Determine cell size
    • If there is columnWidth/rowHeight explicit override
    • use it
    • Else
    • use the the maximum of the preferred width and/or the maximum of the preferred height of the elements.
  • Determine columnCount, rowCount
    • If there are any explicit overrides
    • use them to set columnCount and/or rowCount
    • Else If totalWidth/totalHeight is known along the main direction
    • use it with the columnWidth/columnHeight to determine one of columnCount/rowCount.
    • If only one of columnCount or rowCount is known
    • calculate the other based on the number of elements.
    • Else
    • calculate columnCount and rowCount based on cellWidth, rowHeight, horizontalGap, verticalGap so that pixel area of TileLayout is as square as possible.

Measure

  • Calculate bindable properties based on explicitWidth, explicitHeight for the target container.
  • Measured width/height is calculated from the bindable properties columnWidth, rowHeight, columnCount, rowCount, horizontalGap, verticalGap. justifyRows and justifyColumns are ignored.
  • Minimum measured width/height is enough to fit all elements, including any whitespace the the user has requested via the columnCount and rowCount properties. In most cases measured minimum will be the same as the measured size, however in cases where the user specifies columnCount and rowCount values that are not sufficient to represent all the elements, measured minimum will be greater than the measured size - this is not an issue, since if the layout target has clipping turned on, it will override the measured minimum sizes.

UpdateDisplayList

  • Calculate bindable properties based on parameters unscaledWidth, unscaledHeight
  • If necessary adjust gap/columnWidth/rowHeight based on justify settings.
  • For each element
    • If it has percent size, set it to percent size of the cell size, maximum percent is 100.
    • Else set it to its preferred size.
    • Try to limit maximum element size to cell size, but respect element min and max size settings. This is needed for example in cases where explicit columnWidth and/or rowHeight are specified or we have a virtualized TileLayout (not subject to this spec).
    • Position element within the cell based on its size and the layout's alignment settings.

Pixel boundaries - rounding:

To allow for crisp graphics and avoid pixel hinting issues the layout needs to take extra care when positioning and sizing elements. In the same time we don't want to add any excessive rounding.

TileLayout will not directly round:

  • User inputs.
  • Preferred sizes of elements.
  • Getters for the bindable properties.

Places where TileLayout will round:

  • For each cell, we calculate the cell top-left corner and bottom-right coordinates before rounding
  • Use Math.round() to obtain integer numbers for the cell coordinates and use those to size/position the element.
  • If element width/height depends on the cell size, make sure width/height is rounded with Math.round()
  • If the element position depends on its size and/or the cell size, make sure it's rounded with Math.round()
  • When calculating contentWidth, contentHeight, measuredWidth, measuredHeight, measuredMinWidth, measuredMinHeight, those may need to be rounded.

Since elements are positioned and sized at integer coordinates, there are cases where gaps or columns/rows will differ by a pixel. In most cases this is acceptable.

As an illustration here are two extreme cases:

justify column size.

justify horizontal gap.

Scrolling

In order to support pixel scrolling, TileLayout will calculate contentWidth and contentHeight of the container.

  • on updateDisplayList, after the bindableProperties are calculated
  • contentWidth = columnCount * (columnWidth + horizontalGap) - horizontalGap;
  • contentHeight = rowCount * (rowHeight + verticalGap) - verticalGap;

Besides that, there are the following LayoutBase scrolling-related APIs. Those get used for keyboard navigation in containers as well as keeping selection (selected item) visible:

    public function getHorizontalScrollPositionDelta(unit:ScrollUnit):Number;
    public function getVerticalScrollPositionDelta(unit:ScrollUnit):Number;
    public function getScrollPositionDelta(index:int):Point;

TileLayout will override getHorizontalScrollPositionDelta and getVerticalScrollPositionDelta

  • Calculating scroll position delta
    • For left/right/top/bottom - scroll by column/row, snap partially visible rows/columns
    • For paging - calculate scroll delta so that the last invisible/partially-visible column/row becomes the first fully visible one.

TileLayout will not override getScrollPositionDelta

  • Calculating how much to scroll to get a specified element in view - TileLayout will rely on the LayoutBase default implementation of this API, which tries to minimize the scrolling needed to bring the element fully in view.

Differences from the layout of the Halo Tile container:

  • New properties justifyRows, justifyColumns
  • New properties columnCount, rowCount (for Tile, they existed for TileList)
  • When there are no explicit overrides TileLayout tries to measure as square an area as possible, while Tile measures number of columns as close to number of rows as possible.
  • Don't support styles - horizontalAlign, verticalAlign, padding, horizontalGap, verticalGap
  • TileLayout will limit the element's size the same way regardless whether size is calculated by percentage. Halo layout doesn't limit the element size when element size is percentage of cell size.
  • Scrolling logic.

API Description


public class TileLayout extends LayoutBase
{
    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  horizontalGap
    //----------------------------------
    /**
     *  Horizontal space between columns.
     * 
     *  @see #verticalGap
     *  @see #justifyColumns
     *  @default 6
     */
    public function get horizontalGap():Number
    public function set horizontalGap(value:Number):void

    //----------------------------------
    //  verticalGap
    //----------------------------------
    /**
     *  Vertical space between rows.
     * 
     *  @see #horizontalGap
     *  @see #justifyRows
     *  @default 6
     */
    public function get verticalGap():Number
    public function set verticalGap(value:Number):void

    //----------------------------------
    //  columnCount
    //----------------------------------
    /**
     *  Number of columns to be displayed.
     *  This property will contain the actual column count after 
     *  <code>updateDisplayList()</code>.
     *  Set to -1 to remove explicit override and allow the TileLayout to determine
     *  the column count automatically.
     * 
     *  Setting this property won't have any effect, if <code>orientation</code> is
     *  set to "rows", <code>rowCount</code> is explicitly set, and the
     *  explicit container width is also specified.
     * 
     *  @see #rowCount
     *  @see #justifyColumns
     *  @default -1
     */
    public function get columnCount():int
    public function set columnCount(value:int):void

    //----------------------------------
    //  rowCount
    //----------------------------------
    /**
     *  Number of rows to be displayed.
     *  This property will contain the actual rows count after 
     *  <code>updateDisplayList()</code>.
     *  Set to -1 to remove explicit override and allow the TileLayout to determine
     *  the row count automatically.
     * 
     *  Setting this property won't have any effect, if <code>orientation</code> is
     *  set to "columns", <code>columnCount</code> is explicitly set, and the
     *  explicit container height is also specified.
     * 
     *  @see #columnCount
     *  @see #justifyRows
     *  @default -1
     */
    public function get rowCount():int
    public function set rowCount(value:int):void

    //----------------------------------
    //  columnWidth
    //----------------------------------
    /**
     *  Explicit override for the column width.
     *  This property will contain the actual column width after
     *  <code>updateDisplayList()</code>.
     *  
     *  <p>If not explicitly set, the column width will be
     *  determined from the maximum of elements' width.
     *  Set to NaN to remove explicit override.</p>
     *  
     *  If <code>justifyColumns</code> is set to "columnSize", the actual column width
     *  will grow to justify the fully-visible columns to the container width.
     * 
     *  @see #rowHeight
     *  @see #justifyColumns
     *  @default NaN
     */
    public function get columnWidth():Number
    public function set columnWidth(value:Number):void

    //----------------------------------
    //  rowHeight
    //----------------------------------
    /**
     *  Explicit override for the row height.
     *  This property will contain the actual row height after
     *  <code>updateDisplayList()</code>.
     *  
     *  <p>If not explicitly set, the row height will be
     *  determined from the maximum of elements' height.
     *  Set to NaN to remove explicit override.</p>
     *  
     *  If <code>justifyRows</code> is set to "rowSize", the actual row height
     *  will grow to justify the fully-visible rows to the container height.
     * 
     *  @see #columnWidth
     *  @see #justifyRows
     *  @default NaN
     */
    public function get rowHeight():Number
    public function set rowHeight(value:Number):void

    //----------------------------------
    //  horizontalAlign
    //----------------------------------
    /**
     *  Specifies how to align the elements within the cells in the horizontl direction.
     *  Supported values are 
     *  <code>HorizontalAlign.LEFT</code>, 
     *  <code>HorizontalAlign.CENTER</code>,
     *  <code>HorizontalAlign.RIGHT</code>,
     *  <code>HorizontalAlign.JUSTIFY</code>.
     * 
     *  @default <code>HorizontalAlign.JUSTIFY</code>
     */
    public function get elementHorizontalAlign():String
    public function set elementHorizontalAlign(value:String):void

    //----------------------------------
    //  verticalAlign
    //----------------------------------
    /**
     *  Specifies how to align the elements within the cells in the vertical direction.
     *  Supported values are 
     *  <code>VerticalAlign.TOP</code>, 
     *  <code>VerticalAlign.MIDDLE</code>,
     *  <code>VerticalAlign.BOTTOM</code>,
     *  <code>VerticalAlign.JUSTIFY</code>.
     * 
     *  @default <code>VerticalAlign.JUSTIFY</code>
     */
    public function get elementVerticalAlign():String
    public function set elementVerticalAlign(value:String):void

    //----------------------------------
    //  justifyColumns
    //----------------------------------
    <a href="Inspectable%28category%3D%26quot%3BGeneral%26quot%3B%29">Inspectable(category="General")</a>

    /**
     *  Specifies how to justify the fully visible columns to the container width.
     *  Supported values are "none", "gapSize", "columnSize".
     * 
     *  <p>When set to "none" - turns column justificaiton off, there may 
     *  be partially visible columns or whitespace between the last column and 
     *  the right edge of the container.  This is the default value.</p>
     * 
     *  <p>When set to "gapSize" - the <code>horizontalGap</code> actual value will increase so that
     *  the last fully visible column right edge aligns with the container's right edge.
     *  In case there is only a signle fully visible column, the <code>horizontalGap</code> actual value
     *  will increase so that it pushes any partially visible column just beyond the right edge
     *  of the container.</p>
     * 
     *  <p>When set to "columnSize" - the <code>columnWidth</code> actual value will increase so that
     *  the last fully visible column right edge aligns with the container's right edge.</p>
     * 
     *  @see #horizontalGap
     *  @see #columnWidth
     *  @see #justifyRows
     *  @default "none"
     */
    public function get justifyColumns():String
    public function set justifyColumns(value:String):void

    //----------------------------------
    //  justifyRows
    //----------------------------------
    /**
     *  Specifies how to justify the fully visible rows to the container height.
     *  Supported values are "none", "gapSize", "rowSize".
     * 
     *  <p>When set to "none" - turns column justificaiton off, there may 
     *  be partially visible rows or whitespace between the last row and 
     *  the bottom edge of the container.  This is the default value.</p>
     * 
     *  <p>When set to "gapSize" - the <code>verticalGap</code> actual value will increase so that
     *  the last fully visible row bottom edge aligns with the container's bottom edge.
     *  In case there is only a signle fully visible row, the <code>verticalGap</code> actual value
     *  will increase so that it pushes any partially visible row just beyond the bottom edge
     *  of the container.</p>
     * 
     *  <p>When set to "rowSize" - the <code>rowHeight</code> actual value will increase so that
     *  the last fully visible row bottom edge aligns with the container's bottom edge.</p>
     * 
     *  @see #verticalGap
     *  @see #rowHeight
     *  @see #justifyColumns
     *  @default "none"
     */
    public function get justifyRows():String
    public function set justifyRows(value:String):void

    //----------------------------------
    //  orientation
    //----------------------------------
    /**
     *  Determines whether to lay out elements in rows or in columns.
     *  Supported values are "rows", "columns".
     * 
     *  @default "rows"
     */
    public function get orientation():String
    public function set orientation(value:String):void

    //--------------------------------------------------------------------------
    //
    //  Overriden methods from LayoutBase
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override public function measure():void

    /**
     *  @private
     */
    override public function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
}

B Features


Examples and Usage


Simple way to set a 3×4 TileLayout in a Group

    <Group>
        <layout>
            <TileLayout columnCount="3" rowCount="4"/>
        </layout>
        ...
        <Element1.../>
        <Element2.../>
        ...
    </Group>

Using TileLayout to get a TileList like container in Gumbo:

    <Scroller>
        <DataGroup id="tileContainer"
            itemRenderer="spark.skins.default.DefaultItemRenderer"
            dataProvider="{new ArrayList(
                       'the quick jumped1 fox jumped2 jumped3 lazy jumped4'.split(' '))}">
            <layout>
                <TileLayout/>
            </layout>
        </DataGroup>
    </Scroller>

Additional Implementation Details


Enter implementation/design details for the feature here. This section may be updated after the spec signs off.

Prototype Work


Prototype has been implemented to verify feasibility of bindable properties calculation algorithm.

Compiler Work


None.

Web Tier Compiler Impact


None.

Flex Feature Dependencies


Depends on ILayoutElement exposing APIs to constrain an item size to explicit cell size specified by the user. However explicit cell size should not override explicit minimum / explicit width/height settings.

Backwards Compatibility


Syntax changes

None.

Behavior

None.

Warnings/Deprecation

None.

Accessibility


Describe any accessibility considerations.

Performance


None.

Globalization


No specific globalizaiton issues.

Localization


Compiler Features

None.

Framework Features

List the RTE message strings that will require localization. (They must all come from .properties files.)
None.

List all UI text that will require localization, such as the month and day names in a DateChooser. (All such text must all come from .properties files.)
None.

List all UI images, skins, sounds, etc. that will require localization.
None.

Discuss any locale-specific formatting requirements for numbers, dates, currency, etc.
None.

Discuss any locale-specific sorting requirements.
None.

Issues and Recommendations


Reflow support for TileLayout

When used for a skin part, or a List, the TileLayout can have some seemingly unexpected results:

  • Setting the explicit width on a Group with TileLayout will affect the measured height of the Group.
  • Setting the explicit width on a List with TileLayout will not affect the measured height of the list.

The problem is that in the List case the TileLayout is in a Group that is constrained inside another Group (the skin). Therefore during measure the TileLayout can see only the internal Group and doesn't have access to the List's explicit width setting (measure executes bottom up).

Suggestion: document issue, don't address in the code. In most cases List is expected to either have both dimensions explicitly specified, or none.

Problem with Scroller

The issue is that there are scenarios where changing TileLayout properties will affect content size calculations in such a way that Scroller will try to show/hide the scrollbars, which will affect the content size again and end up in infinite loop. The issue seems to be that the Scroller is using contentWidth, contentHeight from the last layout pass without taking steps to prevent infinite loops.

This should be addressed in Scroller. There may already be a bug on that.

Adding classes with default TileLayout settings, similar to HGroup and VGroup

A question was brought up whether it will be useful to have these convenience classes.

Current recommendation: Don't add anything extra, these are easy enough and if needed we could add them at any point in the future.

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.


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.