This is part 3 in an ongoing series about the Flash Text Engine. Here’s part 1 and part 2.
By now you should know this series isn’t about Adobe’s Text Layout Framework, which is an advanced typography and text layout framework. The Flash Text Engine is the low-level API that TLF is built on. In Flash Player 10, the FTE resides in the flash.text.engine package.
In FP 10 have this great new font rendering library, the Flash Text Engine, but it only fashions characters into TextLines of a specific width. When you think about everything else a TextField does, you begin to understand that rendering the glyphs is only a small (but still important!) percentage of the total work that’s done.
Text Layout
If you recall from my previous overview of the FTE, the main “Controller” class is TextBlock. TextBlock is a factory for TextLines. You supply a ContentElement to the TextBlock, it computes and creates the necessary TextLines to show that content. TextBlock’s rendering algorithms operate on a paragraph level; that is, one TextBlock per paragraph/one paragraph per TextBlock. For example, in the FTE, this paragraph should be represented as a single TextBlock, with multiple ContentElements that define formatting.
Paradigms from a superior layout engine
If you’re familiar with HTML styles, you know that HTML has two distinct layout paradigms: block layout and inline formatting. Block layout affects an entire block of text. Styles like padding, indentation, and margins affect layout on the block level. Inline formatting affects how the characters are rendered within blocks, such as color, size, posture, weight, justification, etc.
TextBlock’s algorithms take care of the inline formatting, because inline formatting affects whether characters flow between TextLines. It’s left up to you to apply any block formatting. For example, when you call the TextBlock’s createTextLine method, you choose the width of the TextLine. This simple option allows us, with some fancy math-e-matics, to achieve many properties of block level layout.
Layout Algorithms
We want some generalized methods for accomplishing various layouts. Everything from simple, single paragraph layouts, to layouts with block formatting, to complex mutli-TextBlock and multi-column (newspaper style) layouts.
I’ve already demonstrated the absolute simplest way: render all the lines in a while loop, finishing once the TextBlock returns null from createTextLine. This is easy and straightforward, and you can apply any kind of block formatting you wish.
var y:Number = 0; var line:TextLine = block.createTextLine(null, 200); while(line) { addChild(line); y += line.ascent; line.y = y; y += line.descent; line = block.createTextLine(line, 200); }
Even though all it seems I’ve done is render a TextField, I’ve accomplished two tasks here: I’ve rendered every TextLine that the TextBlock decides I need, and, by incrementing a counter for the Y dimension, I’ve calculated a rudimentary layout for the TextLines.
Indentation
Now, applying indentation is super easy. Make the first line a little smaller, and change his x position to compensate.
var y:Number = 0; var line:TextLine = block.createTextLine(null, 185); line.x = 15; while(line) { addChild(line); y += line.ascent; line.y = y; y += line.descent; line = block.createTextLine(line, 200); }
Source
Alignment
Center:
var y:Number = 0; var line:TextLine = block.createTextLine(null, 200); while(line) { addChild(line); y += line.ascent; line.y = y; line.x = (200 - line.width) * 0.5; y += line.descent; line = block.createTextLine(line, 200); }
Source
Right alignment is the same, only don’t multiply the calculated x by 0.5.
Source
See? Standard layout practices, ultimately the same math we use every day in component layouts. But, you say, “indentation and alignment are easy, it’s just calculating the x of the lines”. You’re right, it is easy. But so are the other block formatting properties like padding, margins, line spacing, etc. They’re all just calculating the correct x or y and conditionally applying them in the loop.
Multi-TextBlock layout
Ok, now we know how to layout lines from a single TextBlock. With a little code reuse, laying out multiple TextBlocks is a breeze:
var blocks:Vector.<TextBlock> = new <TextBlock>[block1, block2]; var y:Number = 0; for(var i:int = 0; i < blocks.length; ++i) { y = layoutBlock(blocks[i], y); y += 5; } // Returns the aggregate y after this layout operation function layoutBlock(block:TextBlock, y:Number):Number { var line:TextLine = block.createTextLine(null, 185); line.x = 15; while(line) { addChild(line); y += line.ascent; line.y = y; y += line.descent; line = block.createTextLine(line, 200); } return y; }
Source
Multi-Container layout
Ah, now we’re getting to the good stuff. Multi-container layout is really cool, because it allows us to “overflow” text from one DisplayObjectContainer to another, which allows us, among other things, to achieve column layouts.
The general idea is to render as many TextLines into a DisplayObjectContainer (DOC) as possible. When we’ve hit his boundaries, switch to the next available DOC. We can accomplish this with a few modifications to the previous methods. The layout method needs to return the last TextLine that fit in the DOC. That way, we can re-enter the layout routine and pick up with the TextBlock where we left off.
var line:TextLine = layoutBlock(block, null, container1); if(line) line = layoutBlock(block, line, container2); // Returns the last line rendered out of the TextBlock function layoutBlock(block:TextBlock, previousLine:TextLine, container:DisplayObjectContainer):TextLine { var line:TextLine = block.createTextLine(previousLine, container.width); var y:Number = 0; while(line) { container.addChild(line); y += line.ascent; line.y = y; y += line.descent; //If we reached the height boundary, return the last line that fit. if(y + line.height > container.height) return line; line = block.createTextLine(line, container.width); } return line; }
Source
Multi-TextBlock and Multi-Container layout
Here’s the really good stuff! Now we’re going to render multiple TextBlocks into multiple DisplayObjectContainers by merging the two methods above.
To solve this problem, lets identify our list of knowns:
- We have a list of TextBlocks.
- We have a list of DisplayObjectContainers to fit the TextBlocks into.
- We wish to render as many lines into each DOC as possible.
- When the DOC is full, switch to the next one and pick up where we left off.
- We need to keep track of the last TextLine rendered, so we know where we left off.
From our previous experience with rendering multiple TextBlocks, we know that TextBlock will return null when he can render no more lines. And from our previous experience with rendering across DisplayObjectContainers, we know that when a Container is full, we should return the last line that fit. Therefore the logic plays out as such:
- Loop over each TextBlock.
- Render as many TextLines into the DOC as possible, returning the last line rendered.
- If the TextLine is
null, we know the TextBlock ran out of lines and there’s more space in the DOC. Keep the same DOC, but move to the next TextBlock. - If the TextLine is not
null, there are still more lines in the TextBlock, but this DOC ran out of space. Keep the same TextBlock, but move to the next DOC.
- If the TextLine is
- If we reach the end of either list, return.
var blocks:Vector.<TextBlock> = new <TextBlock>[block1, block2, block3, block4]; var containers:Vector.<DisplayObjectContainer> = new <DisplayObjectContainer>[container1, container2, container3]; layout(blocks, containers); function layout(blocks:Vector.<TextBlock>, containers:Vector.<DisplayObjectContainer>):void { var blockIndex:int = 0; var containerIndex:int = 0; var block:TextBlock; var container:DisplayObjectContainer; var line:TextLine; while(blockIndex < blocks.length) { block = blocks[blockIndex]; container = containers[containerIndex]; line = layoutInContainer(container, block, line); if(line && ++containerIndex < containers.length) { container = containers[containerIndex]; containerY = 0; } else if(++blockIndex < blocks.length) block = blocks[blockIndex]; else return; } } var containerY:Number = 0; function layoutInContainer(container:DisplayObjectContainer, block:TextBlock, previousLine:TextLine):TextLine { var line:TextLine = createTextLine(block, previousLine); while(line) { container.addChild(line); containerY += line.ascent; line.y = containerY; containerY += line.descent; if(containerY + line.height > container.height) return line; line = createTextLine(block, line); } //This will be null. return line; } function createTextLine(block:TextBlock, previousLine:TextLine):TextLine { var w:Number = 190; var x:Number = 0; //Apply indention properties here. if(previousLine == null) { w -= 15; x += 15; } var line:TextLine = block.createTextLine(previousLine, w, 0.0, true); if(line) line.x = x; return line; }
Source
Text Source
Woah! Take a breather
It’s a lot to digest, I know. But now you can see that TextLayout isn’t black-magic voodoo, and is entirely achievable in the new Flash Text Engine. If you have any questions, feel free to comment or email me. If you have any techniques for text layout, or have any comments on my techniques, I’d love to hear those too. I took some shortcuts with these demos, but I’ve built out more complete and performance-tuned layouts into tinytlf, the small text layout framework I’ve been working on clandestinely for a few months.
Aside: Text Layout vs. Component Layout
In typical component based layout engines (such as Flex’s), child creation is separate from layout. Usually all the children are added to the display list first, then laid out at some other time. Children aren’t created or destroyed based on their positions or sizes on the screen, and layout doesn’t affect the creation of future children (this is assuming we’re not talking about virtualized layout, which is a special case).
Working with the sizing and layout of TextLines, the block-level layout properties (like padding, indentation, etc.) dictate how each TextLine is created and laid out. This in turn affects how the TextBlock renders the next TextLine, and so on and so forth. I haven’t been able to separate block-level layout properties from the TextLine creation process. This isn’t so bad in practice, but sometimes it rubs me the wrong way, I feel like there should be a better way and I just haven’t found it yet.
Tags: Flash Text Engine, FTE, text layout, tinytlf
