This is part four in an ongoing series about the Flash Text Engine. You can see the previous entries here.
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.
Advanced Text Rendering
The FTE may be low level, but that affords developers like you and me serious control over the presentation of our text fields. The FTE renders glyphs into a series of TextLines, but forces us to handle the sizing and layout of the lines. Today, I’ll cover how to efficiently create and position lines. For simplicity’s sake, I’ll assume the TextLines’ widths extend to the edges of the Container they are rendered into.
In this post I’ll cover:
- text flow across containers
- container resizing
- TextLine positioning, invalidation, and reuse
Caveats
In order to keep this post relatively short, I’ve simplified the algorithms here down to the basic principles. This is a general description of the layout algorithms in tinytlf, though they are getting more complex by the day.
Example
Here’s the final product of what I’m going to walk through. Click on the text to resize the columns:
Defining the Problem
From a layout perspective, we have a number of paragraphs that need to be rendered into a list of DisplayObjectContainers. When we run out of DisplayObjectContainers, we’re going to call it quits. What if we run out of lines before we’re done rendering containers? Good! It’s easy to quit when you don’t have any content left to render.
So what we need is a layout algorithm that will break TextLines from a Vector of TextBlocks across a list of DisplayObjectContainers (DOCs).
Text Layout
In order to coordinate the layout between paragraphs and containers, I’ve defined a controller I call TextLayout. TextLayout’s job is to run through the list of TextBlocks, rendering as many lines as possible into the available DOCs. When the DOC is full, TextLayout moves to the next container. If there is no next container, TextLayout breaks out of the rendering algorithm. Similarly with TextBlocks, if we run out paragraphs to render, TextLayout breaks out of the layout algorithm.
/** * Renders as many lines from the list of TextBlocks into the specified * conatiners as possible. */ public function render(blocks:Vector.<TextBlock>):void { if (!containers || !containers.length || !blocks || !blocks.length) return; containers.forEach(function(c:TextContainer, ...args):void{ c.preLayout(); }); var block:TextBlock = blocks[0]; var i:int = 0; var container:TextContainer = containers[0]; while (block && container) { container = renderBlockAcrossContainers(block, container); block = ++i < blocks.length ? blocks[i] : null; } }
TextContainer
TextContainer is a single DOC that renders TextLines. It is a layout controller that determines the positions and sizes of the TextLines inside himself.
TextContainer maintains a very important relationship with TextLayout’s rendering algorithm. TextLayout repeatedly calls the TextContainer.layout() method, passing in a TextBlock to render lines from, and optionally, the previous line that was rendered from the block. It is TextContainer’s responsibility to render as many lines from the TextBlock into itself until either, 1. the TextBlock has no lines left to render, or 2. the TextContainer is full and has no more room for additional TextLines.
TextContainer.layout() should return the last line rendered from the TextBlock. Null is a valid value to return. If a (non-null) TextLine was returned, TextLayout assumes the TextContainer is full, finished rendering lines, and moves to the next TextContainer, keeping the same TextBlock. If TextContainer returns null, TextLayout assumes there’s still room in the TextContainer for lines, and TextLayout passes the next TextBlock to the TextContainer.
public function layout(block:TextBlock, previousLine:TextLine):TextLine { _y = measuredHeight; var line:TextLine = createTextLine(block, previousLine); while(line) { addChild(line); lines.push(line); position(line); //If there's no room, return the last line broken. if(checkConstraints(line)) { return line; } line = createTextLine(block, line); } //This will be null here. return line; }
Line layout is straightforward. I’m sure you know what the position() and checkConstraints() methods do, and if not, the source is available here.
This is good enough to work for the first layout pass, but resizing introduces a bit more complexity.
TextLineValidity
We can take advantage of an FTE TextBlock and TextLine feature to get resizing.
When the ContentElement “model” is updated, the TextLine “views” should updated to reflect the changes. But we as FTE users have no concept of the data behind each individual TextLines, so if we updated the screen, we’d have to start from the first line and re-create each one. This is extremely inefficient, especially if the change only reflected in one actual line update.
Luckily, the TextBlock can track and resolve such changes for you. Whenever anything in the TextBlock’s content member changes, the TextBlock marks relevant lines “invalid” (TextLineValidity.INVALID). The TextBlock exposes a pretty handy property called firstInvalidLine, which points to the first line in the TextBlock that needs updating. Look mom, no searching!
You can check the status of individual lines simply by reading the TextLine.validity property. Luckily, validity is also writeable, and the TextBlock respects our decision if we choose to mark certain lines “invalid” ourselves. I guess we know best!
Resizing
When a TextContainer is resized, we need to know about it and invalidate our TextLine children.
private var explicitWidth:Number = NaN; override public function get width():Number { return explicitWidth; } override public function set width(value:Number):void { if(value == explicitWidth) return; explicitWidth = value; invalidateLines(); } private function invalidateLines():void { lines.forEach(function(l:TextLine, ...args):void{ l.validity = TextLineValidity.INVALID; }); }
Line Reuse
During the first layout pass, we use the TextBlock.createTextLine method exclusively. But TextLines are expensive to create, so Flash Player 10.1 introduced the TextBlock.recreateTextLine method. Allowing us to recreate TextLines means that the Flash Player can re-jigger the TextLine’s contents internally, without the overhead of creating a new TextLine instance.
During a resize operation, lines will be either created or destroyed. If you set the width smaller, the TextField will be forced to render new TextLines. If you set the width wider, the TextField can remove TextLines at the end. In the first case, we’ll have to make extra calls to TextBlock.createTextLine. In the second case, we can remove the TextLines from the display list and remove all references. In this case, the lines have been orphaned.
But with the introduction of recreateTextLine, it’s optimal to cache orphaned lines, at least for a little while. It’s possible that we will resize the TextField smaller again, which will require the creation of new lines. But if we’ve previously created lines, why not reuse them instead of creating new ones? Good thinking you.
However, this changes the meaning of the line argument in the TextContainer.layout method. Now, the line can either be:
- A valid and successfully broken TextLine, which should be used as the previousLine argument to create or re-create a TextLine.
- An invalid TextLine that needs to be re-created. This should only happen in the case that the line is the TextBlock.firstLine value, because there is no previously broken valid TextLine. Since we don’t want to wipe out our orphan cache looking for a line, we can detect this case and recreate the line.
This introduces just a bit more complexity, but luckily we can encapsulate it in the TextContainer.createTextLine method.
/** * Creates or recreates a given TextLine. */ private function createTextLine(block:TextBlock, line:TextLine):TextLine { removeOrphanedLines(); if(line) { if(line.validity === TextLineValidity.INVALID) { return block.recreateTextLine(line, null, width, 0.0, true); } else if(orphans.length) { var orphan:TextLine = getFirstOrphan(line); if(orphan) { return block.recreateTextLine(orphan, line, width, 0.0, true); } } } return block.createTextLine(line, width, 0.0, true); } private function removeOrphanedLines():void { var line:TextLine; var n:int = lines.length; for(var i:int = 0; i < n; ++i) { line = lines[i]; if(line.validity === TextLineValidity.VALID) continue; if(contains(line)) removeChild(line); lines.splice(i, 1); orphans.push(line); n = lines.length; } } //Static so it's shared between instances of TextContainer private static const orphans:Vector.<TextLine> = new <TextLine>[]; /** * Returns the first invalid orphan that also isn't the input line. */ private static function getFirstOrphan(exceptForMe:TextLine):TextLine { if(orphans.length == 0) return null; var orphan:TextLine = orphans.pop(); while(orphan == exceptForMe) orphan = orphans.pop(); while(orphan && orphan.validity == TextLineValidity.VALID) orphan = orphans.pop(); return orphan; }
Because we can possibly recreate TextLines without calling getFirstOrphan, sometimes a TextLine in the orphan list is recreated but not removed from the list of possible orphans. The two checks in this method ensure that we don’t hit this case.
And there you have it! That’s the basic gist of TextLine rendering, resizing, and reuse across multiple DisplayObjectContainers. You can see all the source for the demo here, and be sure to check out tinytlf, the small text layout framework I’m writing. I described the basic line rendering algorithm in tinytlf (with enhancements of course). Though I’m currently working on a more iterative version. If I’m successful, it’ll be the topic of a future blog post. Cheers!
Tags: Flash Text Engine, FTE, text layout




