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: