Home > Custom Layout > FlowLayout Part 2 – Gap, VerticalAlign and Scrolling

FlowLayout Part 2 – Gap, VerticalAlign and Scrolling

In part one here, the FlowLayout had really basic functionality.  I decided to demonstrate how to add layout properties to the custom Spark layouts and how to enable scrolling.

Let me show off the end result first:

And the source code is available here.

Update: The source has been updated to compile with Flash Builder / Flex SDK beta 2. I also added a horizontalAlign property, just because I can =)

Adding a Layout Property

I want to implement two properties – a horizontalGap for between the elements within a row, and verticalAlign which will control how the elements within the row are aligned. For the latter, I’ll have “top”, “middle” and “bottom” as options, and of course I’ll have to beef up the FlowLayout to support elements with various heights.

I’ll add the properties directly to the FlowLayout class. When the value changes, I need to invalidate the container which will cause the layout to run its updateDisplayList() and at that point I’ll take into account the new values:

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    //---------------------------------------------------------------
    //  horizontalGap
    //---------------------------------------------------------------
 
    private var _horizontalGap:Number = 10;
 
    public function set horizontalGap(value:Number):void
    {
        _horizontalGap = value;
 
        // We must invalidate the layout
        var layoutTarget:GroupBase = target;
        if (layoutTarget)
            layoutTarget.invalidateDisplayList();
    }
 
    //---------------------------------------------------------------
    //  verticalAlign
    //---------------------------------------------------------------
 
    private var _verticalAlign:String = "bottom";
 
    public function set verticalAlign(value:String):void
    {
        _verticalAlign = value;
 
        // We must invalidate the layout
        var layoutTarget:GroupBase = target;
        if (layoutTarget)
            layoutTarget.invalidateDisplayList();
    }

Then I will add a drop-down and a slider in the flowTest.mxml to configure the the properties like this:

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
    <!-- The slider to control the horizontal gap -->
    <s:HSlider id="hGapSlider" minimum="0" maximum="50"
                         value="10" liveDragging="true"/>
 
    <!-- The drop-down to select vertical alignment -->
    <s:DropDownList id="vAlign" requiresSelection="true">
        <s:ArrayCollection>
            <fx:String>bottom</fx:String>
            <fx:String>middle</fx:String>
            <fx:String>top</fx:String>
        </s:ArrayCollection>
    </s:DropDownList>                         
 
    <!-- A Spark List -->
    <s:List id="list1" width="{widthSlider.value}"
            selectedIndex="7"
            dataProvider="{new ArrayCollection(
            'The quick fox jumped over the lazy sleepy\n\dog'.split(' '))}">
 
        <!-- Configure the layout to be the FlowLayout -->
        <s:layout>
            <my:FlowLayout1 verticalAlign="{vAlign.selectedItem}"
                            horizontalGap="{hGapSlider.value}"/>
        </s:layout>
    </s:List>

Then the harder part – actually implementing the support for these properties in the FlowLayout’s updateDisplayList() method.  Since I’ll need to know the row height before I can decide how to position the elements, I’ll have to replace my original loop with a little more complicated code, something along the lines of:

79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
        // loop through the elements
        // while we can start a new row
        var rowStart:int = 0;
        while (rowStart < count)
        {
            // The row always contains the start element
            element = layoutTarget.getElementAt(rowStart);
            var rowWidth:Number = element.getPreferredBoundsWidth();
            var rowHeight:Number =  element.getPreferredBoundsHeight();
 
            // Find the end of the current row
            var rowEnd:int = rowStart;
            while (rowEnd + 1 < count)
            {
                element = layoutTarget.getElementAt(rowEnd + 1);
 
                // Since we haven't resized the element just yet, get its preferred size
                elementWidth = element.getPreferredBoundsWidth();
                elementHeight = element.getPreferredBoundsHeight();
 
                // Can we add one more element to this row?
                if (rowWidth + hGap + elementWidth > containerWidth)
                    break;
 
                rowWidth += hGap + elementWidth;
                rowHeight = Math.max(rowHeight, elementHeight);
                rowEnd++;
            }
 
            // Layout all the elements within the row
            for (var i:int = rowStart; i <= rowEnd; i++)
            {
                element = layoutTarget.getElementAt(i);
                ...
                // Position the element
                element.setLayoutBoundsPosition(x, y + elementY);
            }
 
            // Next row will start with the first element after the current row's end
            rowStart = rowEnd + 1;
 
            // Update the position to the beginning of the row
            ...
        }

Enabling Scrolling

Now I’m almost done, but I want the FlowLayout to be scrollable. In theory this would mean that I need do the following steps:

  1. Add scroll-bars.
  2. Hook up the scroll-bars with the layout’s horizontalScrollPosition and verticalScrollPosition properties.
  3. Calculate the ranges for the scroll-bars and keep them in sync whenever the values change.

In practice, I can save a lot of work by using a ready stock Spark component – the Scroller. Without going into too much details, the Scroller can wrap a container and automatically put up scroll-bars, keeping everything in sync with the container’s layout.  And since in my flowTest.mxml I’m using a List, the Scroller is already in there!!! It’s a part of the List default skin :)  Now all that’s left to do for me is actually calculate the ranges for the scroll bars. This is basically the total width and height of the scrollable area of the container a.k.a. the “content size”.

Calculating the content size is easy enough – I’ll just find the maximum extents of all the children while I’m resizing and arranging them in the FlowLayout’s updateDisplayList(). At the very end of that method I’ll set the updated content size like this:

144
145
146
147
        // Set the content size which determines the scrolling limits
        // and is used by the Scroller to calculate whether to show up
        // the scrollbars when the the scroll policy is set to "auto"
        layoutTarget.setContentSize(maxRowWidth, y);

That’s it!

  1. June 19th, 2009 at 19:01 | #1

    Wahoo!!! GridBag in Flex! ;)

    -James

  2. October 26th, 2009 at 13:20 | #2

    Hi Evtim,

    i’am running into several errors with the public FB4 Beta build.
    Did you check this with the actual sdk?
    I can only use the class on groups but not with list demoed in your example.

    Thanks for any help!

    Cheers, Sven

  3. Evtim
    October 26th, 2009 at 22:33 | #3

    Hi Sven,
    Yes, I originally did the example with an older build. I just updated the source and the swf to Beta 2, and additionally added a horizontalAlign property to the layout =)

    The reason it broke before is that with the latest SDK to get an element, you should check whether the target is virtualized (by default it is for List). So the code would look something like this:
    element = useVirtualLayout ? target.getVirtualElementAt(i) : target.getElementAt(i).
    This is just a side-note though, I plan to dedicate a separate post to virtual layouts.

    Cheers,
    Evtim

  4. October 27th, 2009 at 11:23 | #4

    Hi Evtim,

    yes you can! ;)

    Thanks for the update!

    Cheers, Sven

  5. November 16th, 2009 at 12:49 | #5

    Nice job! Pretty work!

  6. Jochen Szostek
    December 15th, 2009 at 10:28 | #6

    Bonjour Evtim!

    Do you have an idea why adding the ‘ dragEnabled=”true” dropEnabled=”true” dragMoveEnabled=”true” ‘ properties on the list don’t seem to work correctly? (while it does work when using a normal TileLayout)

    I guess there are some more methods that should be overwritten to make this work though I have no idea how to find out which ones.

    Kind regards,

    Jochen

    ps: Thanks a lot for the interesting articles in this blog! Appreciate your effort a lot.

    • Evtim
      December 16th, 2009 at 00:21 | #7

      Hi Jochen,

      You are right, a custom layout needs to provide support for drag and drop. In general the layout needs to calculate the index for the drop, the positioning and sizing of the drop indicator and whether there should be any drag scrolling. Take a look at these three protected methods in LayoutBase: calculateDropIndex(), calculateDropIndicatorBounds(), calculateDragScrollDelta(). Typically a custom layout would need to override these three methods.
      You can find more detailed explanation in the publicly posted List Drag and Drop spec http://opensource.adobe.com/wiki/display/flexsdk/List+DragDrop+Specification.

      -Evtim

  7. January 26th, 2010 at 02:12 | #8

    Hi Jochen,

    I am having different issues with the TileLayout, I have a few list’s ontop of each other and there is a huge gap between each list as if the content in the list is not measuring the height correctly, this only happens when the list Orientation is TileOrientation.Rows, have you had the same problems?

    Thanks

    (using SDK build 10485)

    • Evtim
      January 27th, 2010 at 23:53 | #9

      Hi Tyron,

      Perhaps you need to set the gaps to zero (they default to 6)?
      http://forums.adobe.com/community/labs/gumbo/).

      Thanks,
      Evtim

  8. January 28th, 2010 at 03:42 | #10

    Hi Evtim, I will do thanks

  9. January 28th, 2010 at 04:27 | #11
  10. JamesLyon
    May 12th, 2010 at 10:19 | #12

    I noticed a peculiar measurement bug when I decided to use this on a DropDownList skin’s datagroup.

    I don’t suppose anyone else has run into it. But, if you can tell me why a VerticalLayout wouldn’t have this problem when the FlowLayout does, I can try to work a fix into it.

  11. WebTrauma
    July 24th, 2010 at 12:12 | #13

    Here is a version with vertical spacing and a ‘vertical compress’ horizontal alignment option for those times when you can’t have white space:

    package components
    {
    import mx.core.ILayoutElement;

    import spark.components.supportClasses.GroupBase;
    import spark.layouts.supportClasses.LayoutBase;

    public class FlowLayout extends LayoutBase
    {
    //—————————————————————
    //
    // Class properties
    //
    //—————————————————————

    //—————————————————————
    // horizontalGap
    //—————————————————————

    private var _horizontalGap:Number = 10;

    public function set horizontalGap(value:Number):void
    {
    _horizontalGap = value;

    // We must invalidate the layout
    var layoutTarget:GroupBase = target;
    if (layoutTarget)
    layoutTarget.invalidateDisplayList();
    }

    //—————————————————————
    // verticalGap
    //—————————————————————

    private var _verticalGap:Number = 20;

    public function set verticalGap(value:Number):void
    {
    _verticalGap = value;

    // We must invalidate the layout
    var layoutTarget:GroupBase = target;
    if (layoutTarget)
    layoutTarget.invalidateDisplayList();
    }

    //—————————————————————
    // verticalAlign
    //—————————————————————

    private var _verticalAlign:String = “bottom”;

    public function set verticalAlign(value:String):void
    {
    _verticalAlign = value;

    // We must invalidate the layout
    var layoutTarget:GroupBase = target;
    if (layoutTarget)
    layoutTarget.invalidateDisplayList();
    }

    //—————————————————————
    // horizontalAlign
    //—————————————————————

    private var _horizontalAlign:String = “left”; // center, right

    public function set horizontalAlign(value:String):void
    {
    if (_verticalAlign != “compress”)
    {
    _horizontalAlign = value;
    }

    // We must invalidate the layout
    var layoutTarget:GroupBase = target;
    if (layoutTarget)
    layoutTarget.invalidateDisplayList();
    }

    //—————————————————————
    //
    // Class methods
    //
    //—————————————————————

    override public function updateDisplayList(containerWidth:Number,
    containerHeight:Number):void
    {
    var element:ILayoutElement;
    var layoutTarget:GroupBase = target;
    var count:int = layoutTarget.numElements;
    var hGap:Number = _horizontalGap;
    var vGap:Number = _verticalGap;

    // The position for the current element
    var x:Number = 10;
    var y:Number = 0;
    var elementWidth:Number;
    var elementHeight:Number;
    var vAlign:Number = 0;

    switch (_verticalAlign)
    {
    case “middle” : vAlign = 0.5; break;
    case “bottom” : vAlign = 1; break;
    }

    // Keep track of per-row height, maximum row width
    var maxRowWidth:Number = 0;

    // variables used for compressed layout
    var firstRow:Boolean = true;
    var elementsPerRow:int = 1;

    // loop through the elements
    // while we can start a new row
    var rowStart:int = 0;
    while (rowStart < count)
    {
    // The row always contains the start element
    element = useVirtualLayout ? layoutTarget.getVirtualElementAt(rowStart) :
    layoutTarget.getElementAt(rowStart);

    var rowWidth:Number = element.getPreferredBoundsWidth();
    var rowHeight:Number = element.getPreferredBoundsHeight();

    // Find the end of the current row
    var rowEnd:int = rowStart;
    while (rowEnd + 1 containerWidth)
    break;
    //
    if (firstRow)
    {
    elementsPerRow ++;
    }
    //
    rowWidth += hGap + elementWidth;
    rowHeight = Math.max(rowHeight, elementHeight);
    rowEnd++;
    }

    x = 10;
    switch (_horizontalAlign)
    {
    case “center” : x = Math.round(containerWidth – rowWidth) / 2; break;
    case “right” : x = containerWidth – rowWidth;
    }

    // Keep track of the maximum row width so that we can
    // set the correct contentSize
    maxRowWidth = Math.max(maxRowWidth, x + rowWidth);

    // Layout all the elements within the row
    for (var i:int = rowStart; i <= rowEnd; i++)
    {
    element = useVirtualLayout ? layoutTarget.getVirtualElementAt(i) :
    layoutTarget.getElementAt(i);

    // Resize the element to its preferred size by passing
    // NaN for the width and height constraints
    element.setLayoutBoundsSize(NaN, NaN);

    // Find out the element's dimensions sizes.
    // We do this after the element has been already resized
    // to its preferred size.
    elementWidth = element.getLayoutBoundsWidth();
    elementHeight = element.getLayoutBoundsHeight();

    // Calculate the position within the row
    var elementY:Number = Math.round((rowHeight – elementHeight) * vAlign);

    // Compress the vertical space between elements in the same column
    if (_verticalAlign == "compress" && !firstRow)
    {
    var elementAbove:ILayoutElement = layoutTarget.getElementAt(i – elementsPerRow);
    y = elementAbove.getLayoutBoundsY() + elementAbove.getPreferredBoundsHeight() + vGap;
    }
    // Position the element
    element.setLayoutBoundsPosition(x, y + elementY + vGap);

    x += hGap + elementWidth;
    }

    // Next row will start with the first element after the current row's end
    rowStart = rowEnd + 1;
    firstRow = false;

    // Update the position to the beginning of the row
    x = 10;
    y += vGap + rowHeight;
    }

    // Set the content size which determines the scrolling limits
    // and is used by the Scroller to calculate whether to show up
    // the scrollbars when the the scroll policy is set to "auto"
    layoutTarget.setContentSize(maxRowWidth, y);
    }
    }
    }

  12. Nithiyananthan
    September 1st, 2010 at 22:09 | #14

    we cant able to view the source code.
    Please check this

  13. pete
    February 17th, 2011 at 10:23 | #15

    great approach, but how could i implement rowHeight and a variable width? the items should be scaled to fits the rowHeight.

    • Evtim
      April 19th, 2011 at 19:54 | #16

      @pete
      Hi Pete,
      What do you mean by “variable width”? For the rowHeight, you can either set the height of all the elements to be the height of the row (where you calculate the height of the row probably as the max preferred height of all elements on that row) or another option, if you need scaling specifically, is to use the ILayoutElement::transformAround() method – see here

  14. Robert
    May 3rd, 2012 at 07:48 | #17

    Hi Jochen,

    I was wondering if you (or anyone else!) got any further with your quest to add drag drop functionality to a custom layout.
    I have created my own custom layout similar to a tile layout except a list item can have different widths according to a setting within the list, based on the column count.

    For example, an item with a ‘cellAllocation’ of 2 will take up 2 columns whereas an item with ‘cellAllocation’ set to 1 will take up 1. If the column count is set to 3, they will all fit on one line.

    This has all worked well up till now where I have to be able to move the ‘pods’ around on screen and so I was wondering if you could post an example of how you overcame this problem.

    Anybody else who could help would be a life saver!

    Thanks!

  15. lennox
    August 16th, 2012 at 14:18 | #18

    it doesn’t work with AdvancedDataGrid itemRenderer.
    im trying to fix bug now.

  16. Josh
    June 4th, 2013 at 13:02 | #19

    Is there a way to have the height of the box grow or shrink to match the content size?

  1. June 19th, 2009 at 14:56 | #1
  2. November 26th, 2009 at 14:20 | #2