Site search

Categories

February 2008
M T W T F S S
    Nov »
 123
45678910
11121314151617
18192021222324
2526272829  

Friends

Overlapping TabBar in Flex

On a recent project I needed an overlapping tab bar, something that the default Flex TabBar doesn’t implement. While you can can easily change the spacing between tabs using a negative horizontalGap, you cannot retain a top-of-the-pile-like status for the selected tab. They are permanently stacked left-to-right on top of each other.

There’s a solution out there that relies on clever skinning techniques, but it’s a little time-consuming and inflexible. So after some poking around inside the Flex framework I discovered the underlying problem and came up with an alternate solution.

The Problem

The example above shows the default TabBar functionality. Notice the right-hand side each tab is obscured by the next tab along, no matter which tab you select. The problem lies in the way TabBar arranges it’s children buttons. TabBar ultimately derives from Box, which uses the BoxLayout class to order sprites left-to-right based on their index in the display list. This means if you change the depth of a sprite in a Box it’s corresponding horizontal position will also change. So in our situation, changing the selected tab’s depth to n-1 causes the tab to be moved to the far-right of the box.

The Solution

Based on what’s overridable and manipulable in TabBar, the best solution is to subclass it and manage the position of the tabs in the display list ourselves. To do this we need to store a fixed reference of the tabs in an array and dictionary, and return these positions when the layout class arranges everything. This satisfies the layout routine’s desire to order everything left-to-right on depth. This then leaves us the ability to manage the depths ourselves afterwards. Hopefully this code will explain it better:

public class OverlappingTabBar extends TabBar
{
   // Used to store local reference to tabs
   private var _tabs:Array;
   private var _tabsDict:Dictionary;
   
   public function OverlappingTabBar()
   {
      super();
      _tabs = new Array();
      _tabsDict = new Dictionary(true);
   }
   
   override protected function createNavItem(label:String, icon:Class = null):IFlexDisplayObject
   {
      var tabIndex:int = numChildren;
      var navItem:IFlexDisplayObject = super.createNavItem(label, icon);
      // Store tab refs in our array and dictionary
      _tabs.push(navItem);
      _tabsDict[navItem] = tabIndex;
      return navItem;
   }
   
   override public function getChildAt(index:int):DisplayObject
   {
      return _tabs[index];
   }
   
   override public function getChildIndex(child:DisplayObject):int
   {
      return _tabsDict[child];
   }
   
   override public function removeChildAt(index:int):DisplayObject
   {
      // ensures tabs are cleaned up from our
      // array and dictionary when removed
      var c:DisplayObject = super.removeChild(getChildAt(index));
      _tabsDict[getChildAt(index)] = null;
      _tabs.splice(index, 1);
      return c;
   }
}

We’re simply storing the original position for each tab when it’s created in createNavItem(), then we return the stored position everytime getChildAt() and getChildIndex() is called (as it will be by the layout class). The overridden removeChildAt() ensures that our array and dictionary is kept up to date if tabs are removed from the bar, which occurs whenever the TabBar.dataProvider is changed.

Finally we need organise the tabs in the display list ourselves. The best location I found to do this was in hiliteSelectedNavItem(), as it is passed the index of the selected tab.

override protected function hiliteSelectedNavItem(index:int):void
{
   super.hiliteSelectedNavItem(index);
   if (index != -1)
   {
      // z-order tabs
      var l:int = _tabs.length - 1;
      for (var i:int = 0; i <= l; i++) {
         setChildIndex(_tabs[i], (l-i));
      }
      // put our selected tab at the top
      setChildIndex(_tabs[index], l);
   }
}

The finished result looks like this:

It looks nicer with some decoratively sloped tab edges, but hopefully you get the idea. I admit that the code is a bit of a hack, and I can’t guarantee it won’t cause problems somewhere down the line, but it’s proved robust so far. You should be able to swap out your original TabBar instances with OverlappingTabBar just by importing the class and renaming the MXML tag.

Download the source code here: OverlappingTabBar.as