16 February 2009
You should be familiar with Flex, XML, and ActionScript 3.0 as well as Flex Builder to make the most of this article.
Intermediate
The goal of this article is to introduce you to the Text Layout Framework library for Adobe Flash Player 10 and Adobe AIR 1.5 and to demonstrate how you can use it to display rich text inside of the Flash Player 10 and Adobe AIR 1.5 runtimes. I will not cover all the possibilities of this framework, but I will give you enough of a push to help
The Flash Text Engine, available in Flash Player 10 and Adobe AIR 1.5, supports many new text capabilities. Although you have access to an API that provides low-level access to this engine, if you wanted to use the API you would have to write a lot of code. Instead, the Flash Text Engine is intended to provide the foundation for libraries that leverage these capabilities and thus make life easier for developers.
This is where the Text Layout Framework comes in. It is a library written in pure ActionScript, and thus can be used in Flash CS4 Professional, Flex 3.2, "Gumbo" (codename for the next generation of Flex), and Adobe AIR 1.5.
The Text Layout Framework library provides support for:
Figures 1 through 6 show screen shots of the Text Layout Framework library in action, taken from the samples available on Adobe Labs.
The Text Layout Framework library comprises three components and ten packages. All the packages are subpackages of the flashx.textlayout package. Since the library is implemented using pure ActionScript 3.0, it can be used in Flash CS4, Flex 3.2, Gumbo (the library is part of Gumbo), AIR 1.5, and, of course, Flash Player 10. Here is a short description of the three components that are part of the Text Layout Framework library:
One way to look at the Text Layout Framework library, is by comparing it to the MVC pattern:
The model uses a hierarchical tree to represent text. Each element of the tree is a class from the package flashx.textlayout.elements. The root element is always an instance of the TextFlow class, and conceptually represents an entire story of text (the term story comes from DTP, and means a collection of text that should be treated as one unit). For example, the article you are reading could be a story.
The rest of the elements are:
div : A division of text, can contains only div or p elementsp : A paragraph, can contain any element but diva : A link; can contain tcy, span, img, tab, brtcy : A run of horizontal text, used in vertical text; for example in Japanese you can have this type of element; can contain a, span, img, tab, brspan : A run of text in a paragraph; can contain only textimg : An image in a paragraphtab : A tab characterbr : A break character; text will continue on the next line, but it doesn't start a new paragraphThe TextFlow class can have only these two elements as children: div and p. Figure 7 illustrates how the model may look for a story.
This hierarchy translates to an XML document, using Text Layout Framework Markup. Basically the nodes can be: TextFlow, div, p, a, img, span, tcy, br, and tab. At the same time, each node has an ActionScript class implementation: TextFlow, DivElement, ParagraphElement, LinkElement, TCYElement, SpanElement, InlineGraphicElement, TabElement, and BreakElement. All these classes inherit directly or indirectly from the class FlowElement.
You have two ways to create a TextFlow element: you can use an XML object, or you can create the nodes and assembling them together in a tree (similar to creating an XML object using DOM).
The following code demonstrates how to create a TextFlow element using an XML object:
private static const textInput:XML = <TextFlow xmlns="http://ns.adobe.com/textLayout/2008">
<div>
<p>
<img source="air.png"/>
<span>Flex is a highly productive, free open
source framework for building and maintaining expressive web
applications...</span>
<br/>
</p>
</div>
</TextFlow>;
private var textFlow:TextFlow = TextFilter.importToFlow(textInput, TextFilter.TEXT_LAYOUT_FORMAT);
Note that the TextFilter class is used for importing the XML object; it creates an instance of TextFlow. The second parameter of the import method determines what format the XML is written in. In this case I am using Text Layout Framework Markup.
The following snippet demonstrates how to create a TextFlow element using the FlowElement classes:
var textFlow:TextFlow = new TextFlow();
var p:ParagraphElement = new ParagraphElement();
var span:SpanElement = new SpanElement();
span.text = "Hello, World!";
p.addChild(span);
textFlow.addChild(p);
As noted earlier, the model also stores the formatting information. If you choose to create the TextFlow element using XML, then you can add the properties like attributes on the node you want:
var text:XML = <TextFlow xmlns="http://ns.adobe.com/textLayout/2008"
fontSize="14" textIndent="15" marginBottom="15"
paddingTop="4" paddingLeft="4">"
<p>
<span>There are many </span>
<span fontStyle="italic">such</span>
</p>
</TextFlow>;
Or, if you create the TextFlow element out of FlowElement classes, you can do it like this:
var cf:CharacterFormat = new CharacterFormat();
cf.fontSize = 14;
textFlow.characterFormat = cf;
The properties that can be set for all the nodes are grouped into three types: container, paragraph, and character properties. You can see the API for the flashx.textlayout.formats package in the documentation.
When you apply a format to a FlowElement, you have two options: To overwrite the existing formats, or to keep the existing ones and add the new format. The following code illustrates both options—to keep the existing format, you pass an instance of that format to the constructor when you create the new format:
//overwrite the characterFormat for the TextFlow
var cf:CharacterFormat = new CharacterFormat();
cf.fontSize = 14;
textFlow.characterFormat = cf;
//keeps the existent characterFormat, and change only the font size
var cf:CharacterFormat = new CharacterFormat(textFlow.characterFormat);
cf.fontSize = 14;
textFlow.characterFormat = cf;
If you apply a font-size change to the TextFlow element, then this change will be applied to all its children that don't have a font-size explicitly set on them.
Important: Every time you apply changes to a TextFlow object that has been displayed, you have to call the method updateAllContainers() on the flowComposer property of the TextFlow object to trigger the update of the display:
textFlow.flowComposer.updateAllContainers();
You've learned how to create the model for storing the text using the TextFlow class. Now it is time to see how you can display this text. Basically, you have two options depending on what level of control you need to have on the text. Both methods convert the TextFlow class into TextLine instances, which are part of the new Flash Text Engine. In order to display the TextLine instances you have to add it to a control that is a subclass of DisplayObjectContainer, such as Sprite.
If you just want to display the text and you don't want to interact with it (for example, by selecting parts of it), then you can use TextLineFactory. This class has two static methods, createTextLineFromTextFlow and createTextLinesFromString, which create TextLine objects out of a TextFlow or a string. Here is an example of how to use it:
var sprite:Sprite = new Sprite();
//this serves as the bound for the text
var bounds:Rectangle = new Rectangle(0,0,300,100);
var txtStr:String = "This is sample text showing lines created by TextLineFactory.";
var characterFormat:CharacterFormat = new CharacterFormat();
characterFormat.fontSize = 48;
TextLineFactory.createTextLinesFromString(callback, txtStr, bounds, characterFormat);
//this is the callback function that will be called by
createTextLinesFromString()
//method for each TextLine
function callback(tl:TextLine):void{
sprite.addChild(tl);
}
If you want to be able to select or edit, then you have to use the Flow Composer. Every TextFlow instance has an object that implements the IFlowComposer interface. You can use the property flowComposer of the TextFlow to access this object. This object has the methods to associate the text with one or more containers and prepare the text for display.
For the container you can use any instance of a DisplayObjectContainer, such as Sprite, for example. In order to link a container to another one (when you do this, if the text overflows the first container, then it will flow into the second container) to support scrolling or container formatting, the DisplayObjectContainer is wrapped inside an instance of DisplayObjectContainerController.
Here is how you can add the container to a TextFlow object, and then trigger the formatting and display of the text:
var sprite:Sprite = new Sprite();
canvas.rawChildren.addChild(sprite);
var controller:IContainerController = new DisplayObjectContainerController(sprite, 600, 400);
textFlow.flowComposer.addController(controller);
textFlow.flowComposer.updateAllContainers();
If you want to make the text selectable, you have to use a manager, SelectionManager and associate it with the interactionManager property of the TextFlow class. You set up the manager like this:
textFlow.interactionManager = new SelectionManager();
After the manager is assigned to the TextFlow class's interactionManager, the TextFlow class has access to the event handlers of the manager. For example, it knows when a key is pressed, when the container loses or gains focus, and when text is selected.
If you want to enable editing features on top of selecting features, then you would use EditManager instead of SelectionManager:
textFlow.interactionManager = new EditManager();
If you want to enable Undo/Redo commands, you use UndoManager, like this:
textFlow.interactionManager = new EditManager(new UndoManager());
For import and export operations, you use the TextFilter object from the flashx.textlayout.conversion package. You've already seen one way to import XML to the TextFlow. What you have to do is this:
var textInput:XML = <TextFlow><div><span>Some text here.</span></div></TextFlow>;
var textFlow:TextFlow = TextFilter.importToFlow(textInput, TextFilter.TEXT_LAYOUT_FORMAT);
You can also import plain text (you set the convertor to parse string, by setting the second argument to TextFilter.PLAIN_TEXT_FORMAT):
var textInput:String = "Hello World, this is plain text";
var textFlow:TextFlow = TextFilter.importToFlow(textInput, TextFilter.PLAIN_TEXT_FORMAT);
The Text Layout Framework can export text in any one of three formats: plain text, FXG, or Text Layout Format. For example, this is how you export XML text from an existing TextFlow instance using Text Layout Format:
var out:XML = TextFilter.export(textFlow, TextFilter.TEXT_LAYOUT_FORMAT, ConversionType.XML_TYPE );
This sections demonstrates how to use the Text Layout Framwork library in Flex and AIR applications.
This example uses flowComposer. I use a single container, and the textFlow is created using an XML file. In this XML file I have four paragraphs in four languages, two of them use left-to-right, and two use right-to-left flow. I also added some controls to change the font size, the number of columns, and the direction of text for the first two paragraphs.
You can click the text, scroll, edit, delete, insert, undo, copy/paste and so on.
Here is the source code (don't forget that you need Flex SDK 3.2 and the three SWC libraries of the framework, which are part of the ZIP archive that accompanies this article):
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:local="*" layout="absolute" creationComplete="init()"
horizontalScrollPolicy="off"
viewSourceURL="srcview/index.html">
<mx:Script>
<![CDATA[
import mx.controls.CheckBox;
import mx.collections.ArrayCollection;
import flashx.textLayout.formats.Direction;
import flashx.textLayout.elements.InlineGraphicElement;
import flashx.textLayout.events.StatusChangeEvent;
import flashx.textLayout.formats.ContainerFormat;
import flashx.textLayout.formats.ICharacterFormat;
import flashx.textLayout.formats.CharacterFormat;
import mx.events.SliderEvent;
import flashx.textLayout.edit.UndoManager;
import flashx.textLayout.edit.EditManager;
import flashx.textLayout.container.DisplayObjectContainerController;
import flashx.textLayout.conversion.TextFilter;
import flashx.textLayout.elements.TextFlow;
publicvar directions:ArrayCollection = new ArrayCollection(
[
{label:"Left-to-Right", data:Direction.LTR},
{label:"Right-to-Left", data:Direction.RTL}
]
);
[Embed(source="air.png")]
[Bindable]
staticpublicvar imgClass:Class;
privatevar _textContainer:Sprite = null;
privatestaticconst textInput:XML = <TextFlow xmlns="http://ns.adobe.com/textLayout/2008">
<div>
<p><span>and the text goes here ...</span></p>
</div>
</TextFlow>;
privatevar _textFlow:TextFlow;
privatefunction init():void {
_textContainer = new Sprite();
canvas.rawChildren.addChild(_textContainer);
_textFlow = TextFilter.importToFlow(textInput, TextFilter.TEXT_LAYOUT_FORMAT);
_textFlow.flowComposer.addController(new DisplayObjectContainerController(_textContainer, canvas.width-40, canvas.height));
_textFlow.addEventListener(StatusChangeEvent.INLINE_GRAPHIC_STATUS_CHANGED, picLoaded);
//adding Select/Edit/Copy/Paste/Undo features
_textFlow.interactionManager = new EditManager(new UndoManager());
// initialize with a selection before the first character
_textFlow.interactionManager.setSelection(0,0);
_textFlow.flowComposer.updateAllContainers();
}
privatefunction picLoaded(event:StatusChangeEvent):void {
var image:InlineGraphicElement = event.element as InlineGraphicElement;
_textFlow.flowComposer.updateAllContainers();
}
privatefunction changeFontSize(event:SliderEvent):void {
var cf:CharacterFormat = new CharacterFormat(_textFlow.characterFormat);
cf.fontSize = event.value;
_textFlow.characterFormat = cf;
_textFlow.flowComposer.updateAllContainers();
}
privatefunction changeNoColumns(event:SliderEvent):void {
var cf:ContainerFormat = new ContainerFormat(_textFlow.containerFormat);
cf.columnCount = event.value;
cf.columnGap = 15;
_textFlow.containerFormat = cf;
_textFlow.flowComposer.updateAllContainers();
}
privatefunction changeTextDirection(event:Event):void {
_textFlow.direction = (event.target as ComboBox).selectedItem.data;
_textFlow.flowComposer.updateAllContainers();
}
]]>
</mx:Script>
<mx:VBox x="20" y="20">
<mx:Canvas id="canvas" width="600" height="400" backgroundColor="#ffffff" verticalScrollPolicy="auto"/>
<mx:HBox width="100%">
<mx:HSlider labels="Font size:" minimum="10" maximum="22" snapInterval="1" change="changeFontSize(event)" enabled="true" />
<mx:HSlider labels="No of Columns:" minimum="1" maximum="2" snapInterval="1" change="changeNoColumns(event)" enabled="true" />
<mx:Label text="Text Direction:"/>
<mx:ComboBox change="changeTextDirection(event)" dataProvider="{directions}"/>
</mx:HBox>
</mx:VBox>
</mx:Application>
If you want to use the Text Layout Framework library in an AIR application, then follow these steps in order to adapt the above example:
mx:Application nodes into the MXML file from the AIR project.xmlns:local="*" creationComplete="init()" to the mx:WindowApplication node. If you want to learn more about the Text Layout Framework library, go to Adobe Labs, where you can take a look at the demo application (the one from which I took the screen shots on page 2 of this article). When you click the small arrow at the top right of this app, you can download the source code for the current panel and see for yourself how it is done. You can also check out this demo, which lets you explore many of the typographic and text layout capabilities of the Text Layout Framework.
Adobe Labs also provides access to the APIs as well as samples for Flash CS4, Flex 3.2, and Gumbo.