Sunday, March 18, 2012

Adobe Flex Flow Tabs Layout

Today we are going to create flow layout for spark tabs. When we have large amount of tabs, there appears a problem to layout them into small horizontal width. Default spark TabBar layout just resizes them to smaller width and trims buttons labels. It looks like this:


To solve this problem we can implement html-like custom spark flow layout. Create LayoutBase class and override two methods:
  • measure() - this method measures layout component size, based on its elements size. In our case this is height, which depends on component width.
  • updateDisplayList(width:Number, height:Number) - method to position elements. Elements position depends on the component width
 Lets see updateDisplayList. Method comes through all elements. Measures its width and height. If element size overcomes container width, we go to the next row. Then we position element by estimated horizontal and vertical coordinates.


override public function updateDisplayList(width:Number, height:Number):void {
 super.updateDisplayList(width,height);
 var currentElement:ILayoutElement;
 var elementHeight:Number;
 var elementWidth:Number;
 var rowHeight:Number = 0;
 var verticalPosition:Number = 0;
 var horizontalPosition:Number = 0;
 if (!target) return;
 var childrenCount:int = target.numElements;
 for (var i:int = 0; i< childrenCount; i++) {
  currentElement = target.getElementAt(i);
  //Reset element size to retrieve its original one
  currentElement.setLayoutBoundsSize(NaN,NaN);
  elementHeight = currentElement.getPreferredBoundsHeight();
  elementWidth = currentElement.getPreferredBoundsWidth();
  //Last element in a row
  if(horizontalPosition + elementWidth >
  width ) {
   horizontalPosition = 0;
   verticalPosition += rowHeight;
   rowHeight = 0;
  }
  rowHeight = Math.max(elementHeight, rowHeight);
  //Set element position
  currentElement.setLayoutBoundsPosition(horizontalPosition, verticalPosition);
  horizontalPosition += elementWidth;
 }
 //Invalidate targetr measuredHeight, if it is incorrect
 if(target.measuredHeight != verticalPosition + rowHeight) {
  target.invalidateSize();
 }
}
In the end we check, if target measuredHeight is not the same with estimated elements height, and if so we cal invalidateSize() method on target. This method call measure() to update measured size - measuredHeight in our case.


override public function measure():void
  {
   super.measure();
   if (!target) return;
   
   var childrenCount:int = target.numElements;
   
   var currentElement:ILayoutElement;
   var maxWidth:Number = 0;
   var maxHeight:Number = 0;
   var elementMaxX:Number;
   var elementMaxY:Number;
   
   for(var i:int = 0; i< childrenCount; i++)
   {    
    currentElement = target.getElementAt(i);
    elementMaxX = currentElement.getLayoutBoundsX() + currentElement.getPreferredBoundsWidth();
    elementMaxY = currentElement.getLayoutBoundsY() + currentElement.getPreferredBoundsHeight();
    if(maxWidth < elementMaxX) maxWidth = elementMaxX;
    if(maxHeight < elementMaxY) maxHeight = elementMaxY;
   }
   target.measuredWidth = maxWidth;
   target.measuredHeight = maxHeight;
  }
As we see  - this is very easy. Measure method goes through all elements and just retrievs maximum size, which is necessary to allocate this elements.

So here is a final demo:
 

4 comments:

  1. Thanks you for the hint to invalidate the size, I almost went nuts until I found this page!

    ReplyDelete
  2. Thanks for the Layout component, it is really helpful. But i wonder if you'd consider adding some extra functionality to it like gap and padding.

    ReplyDelete
  3. Hi Ermal!

    Actually I gave up with Flex about a year ago, but I think it is not hard to add this functionality.

    To add verticalPadding and verticalGap, you should do something like this:
    1) in updateDisplayList add verticalPadding before and after for cycle
    2) in updateDisplayListadd verticalGap on every new row iteration
    3) in measure add 'bottom' verticalPadding after maxHeight calculation


    var verticalPadding: Number = 0;
    var verticalGap: Number = 0;

    override public function updateDisplayList(width:Number, height:Number):void {
    super.updateDisplayList(width,height);
    var currentElement:ILayoutElement;
    var elementHeight:Number;
    var elementWidth:Number;
    var rowHeight:Number = 0;
    var verticalPosition:Number = 0;
    var horizontalPosition:Number = 0;

    if (!target) return;
    var childrenCount:int = target.numElements;
    verticalPosition += verticalPadding;
    for (var i:int = 0; i< childrenCount; i++) {
    currentElement = target.getElementAt(i);
    //Reset element size to retrieve its original one
    currentElement.setLayoutBoundsSize(NaN,NaN);
    elementHeight = currentElement.getPreferredBoundsHeight();
    elementWidth = currentElement.getPreferredBoundsWidth();
    //Last element in a row
    if(horizontalPosition + elementWidth >
    width ) {
    horizontalPosition = 0;
    verticalPosition += rowHeight + verticalGap;
    rowHeight = 0;
    }
    rowHeight = Math.max(elementHeight, rowHeight);
    //Set element position
    currentElement.setLayoutBoundsPosition(horizontalPosition, verticalPosition);
    horizontalPosition += elementWidth;
    }
    verticalPosition += rowHeight + verticalPadding;
    //Invalidate target measuredHeight, if it is incorrect
    if(target.measuredHeight != verticalPosition) {
    target.invalidateSize();
    }
    }


    override public function measure():void
    {
    super.measure();
    if (!target) return;

    var childrenCount:int = target.numElements;

    var currentElement:ILayoutElement;
    var maxWidth:Number = 0;
    var maxHeight:Number = 0;
    var elementMaxX:Number;
    var elementMaxY:Number;

    for(var i:int = 0; i< childrenCount; i++)
    {
    currentElement = target.getElementAt(i);
    elementMaxX = currentElement.getLayoutBoundsX() + currentElement.getPreferredBoundsWidth();
    elementMaxY = currentElement.getLayoutBoundsY() + currentElement.getPreferredBoundsHeight();
    if(maxWidth < elementMaxX) maxWidth = elementMaxX;
    if(maxHeight < elementMaxY) maxHeight = elementMaxY;
    }

    maxHeight += verticalPadding

    target.measuredWidth = maxWidth;
    target.measuredHeight = maxHeight;
    }

    For horizontal padding and gaps it is quite similar

    ReplyDelete
  4. Hey, you used to write wonderful, but the last few posts have been kinda boring… I miss your tremendous writings. Past few posts are just a little out of track! But this one about Adobe Flex Flow Tabs Layout is a wonderful read, I really like the way you've described it. Thumbs up for you.

    Also I want to share with you a best Medical websites design company America, Their design are unique, custom and limited to your area.

    ReplyDelete