Extending HorizontalLayout to Support Baseline (Align to Text)
The 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
- Still keep all the cool functionality of the HorizontalLayout without copying code.
- 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.
- 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.
- 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.
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>
- 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.
- 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).
- 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.
- 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”.