Home > Custom Layout, Spark Layouts > Extending HorizontalLayout to Support Baseline (Align to Text)

Extending HorizontalLayout to Support Baseline (Align to Text)

February 26th, 2010 Leave a comment Go to comments

hlayoutbaselineThe current Spark HorizontalLayout unfortunately doesn’t support baseline alignment. If I want to have, for example, a Label and a Button positioned so that their text aligns, I usualy use absolute position (nudging things up or down) or baseline constraints with BasicLayout/Canvas. But then again, I always find myself needing this feature in a HorizontalLayout and so I finally decided to tackle it on. It turns out it’s pretty easy to extend the stock Spark HorizontalLayout, not to mention using it is a breeze with the Spark layout architecture (yay Spark!).

Baseline Concepts in Flex

  • Each IVisualElement has a baselinePosition. This is the vertical distance from element’s top (origin) to its text base.
  • To align an IVisualElement‘s text to a specific point within its parent, say A=100, we roughly do the following element.y = A – element.baselinePosition.
  • Each IVisualElement has a baseline constraint. How this is going to be used is up to the parent layout. By default BasicLayout/Canvas interpret it as the position within the parent to which the element’s text base should be aligned, meaning that the actual calculation is element.y = element.baseline – element.baselinePosition.

What I Want

  1. Still keep all the cool functionality of the HorizontalLayout without copying code.
  2. Ability to specify a global baseline. The layout should position all the elements such that their text aligns to that global baseline. I also want the ability to individually offset the elements from the global baseline. I want the element’s baseline constraint, if specified, to serve as such an offset.
  3. If I’m too lazy to specify the global baseline, the layout should calculate it for me. In that case there shouldn’t be any extra space around the elements and the layout’s borders.
  4. The global baseline should be [Bindable]. Since the layout is calculating it in certain cases, I want to be able to read it back whenever its value changes.

Here’s an example of the layout in action: hlayoutbaseline-simple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx"
               xmlns:local="*">
 
    <s:BorderContainer horizontalCenter="0" verticalCenter="0"
                       minWidth="0" minHeight="0">
 
        <s:layout>
             <local:HBaselineLayout verticalAlign="baseline"/>
        </s:layout>
 
        <s:CheckBox label="hello"/>
        <s:Button label="button"/>
        <s:Label text="and label"/>
 
    </s:BorderContainer>
 
</s:Application>

Implementation

  1. I extended the HorizontalLayout class and had to override the verticalAlign property getter in order to modify the enum metadata and add “baseline” as an option.
  2. I added a globalBaseline property. I had to override the updateDisplayList() method. After the super.updateDisplayList() has finished with sizing and positioning the elements, I’d iterate over them and position them using something like this element.setLayoutBoundsPosition(element.getLayoutBoundsX(), globalBaseline + element.baseline – element.baselinePosition).
  3. Calculating the globalBaseline is easy enough. If all the elements are aligned at the globalBaseline, then a certain portion of each element will be above that line and the rest will be below the line – top portion and bottom portion. Then I have topPortion = element.baseline – element.baselinePosition and bottomPortion = element.height – element.baseline + element.baselinePosition. Now since I don’t want any extra space around the elements I want the element that sticks out the most above the globalBaseline to touch the top edge of the container, while the element that sticks out the most below the globalBaseline to touch the bottom edge of the container. Therefore the globalBaseline is going to be the maximum of the top portions, while the measured height of the container is going to be the globalBaseline + the maximum of the bottom portions. I created a helper function to calculate the maximums of the top and bottom portions and used it in the overridden measure() and updateDisplayList() methods.
  4. Finally, I added a property actualBaseline and made sure to dispatch an event when it changes. I update the actualBaseline at the end of updateDisplayList() so that at the time of the event it’s certain that all elements are already sized and positioned correctly.

Here’s an example of a working app, to get the source right-click on the application and select “view source”.

  1. February 26th, 2010 at 02:13 | #1

    Really cool. Totally going to use baseline alignment now. Don’t really need the offset for most situations, but that’s handy too!

    Thanks for sharing the code!

  2. Marc
    April 2nd, 2010 at 06:47 | #2

    What is the license of this componet(HBaselineLayout)? Can I use it in commercial application?
    Best regards, Marc

    • Evtim
      April 3rd, 2010 at 19:50 | #3

      Hi Marc,
      I just added a license and a link at the bottom of the right side bar of the blog. All work on this blog, unless stated otherwise, is shared under Creative Commons Attribution 3.0 United States license. So yeah, you can modify, share and use commercially.

  3. September 6th, 2010 at 03:22 | #4

    Several isNaN conditions are reverse but other than that, this code works really well. Thank you!

  1. No trackbacks yet.