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:
- Add scroll-bars.
- Hook up the scroll-bars with the layout’s horizontalScrollPosition and verticalScrollPosition properties.
- 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!
Wahoo!!! GridBag in Flex! 😉
-James
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
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
Hi Evtim,
yes you can! 😉
Thanks for the update!
Cheers, Sven
Nice job! Pretty work!
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.
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
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)
Hi Tyron,
Perhaps you need to set the gaps to zero (they default to 6)?
http://forums.adobe.com/community/labs/gumbo/).
Thanks,
Evtim
Hi Evtim, I will do thanks
Posted: http://forums.adobe.com/thread/565055
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.
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);
}
}
}
we cant able to view the source code.
Please check this
great approach, but how could i implement rowHeight and a variable width? the items should be scaled to fits the rowHeight.
@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
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!
it doesn’t work with AdvancedDataGrid itemRenderer.
im trying to fix bug now.
Is there a way to have the height of the box grow or shrink to match the content size?