Friday, July 30, 2010

Text Layout Framework Basics - Part 1

In this series I am going to explain the basics of using Adobe's new  Text Framework. In this first part you'll learn about the Flash Text Engine.


The Flash Text Engine (FTE) is a set of new classes (found in the flash.text.engine.* package) that support low-level text functionality in Flash Player 10

While you can use the FTE in Flash Builder 4 to render text, you will probably more often use the TLF
(Text Layout Framework), which is a set of classes built on top of the FTE to provide more high-level text functionality, such as scrolling, selection and editing.

But let's not rush things (TLF will be covered in the next part). Right now let's take a quick look at the main classes of the Flash Text Engine and see what we can do with them.
A TextBlock holds a single paragraph of text, built from 3 types of ContentElements:
( ContentElement is the base class for each of the three, but it's an abstract class so you won't be able to create an instance of it by itself) :
  • TextElement , which is simply a string
  • GraphicElement which is any graphic or image that you wish to include in your text.
  • GroupElement which is a just container for the other two classes, but may also contain other (nested) GroupElements.
So I could, for example, create two TextElements, one with the string "hello" and the other with "world", put them inside a GroupElement and then put that group inside a TextBlock, and what I'll have is a nice "helloworld" block of text (with no space between the words, because none of my strings included one).

The only problem with this TextBlock, however, would be that because it's not a DisplayObject, 
no user will ever see it
. And in fact, all of the aforementioned classes are purely logical classes in the sense that they just hold information about the text composition, but not the text itself.

Luckily, the TextBlock has a nice method called createTextLine() which, unsuprisingly, takes the TextBlock and generates  TextLines  from it that can easily be added to the display list (we'll see how it's done in a moment).

One additional class that does not show in the nice diagram above, but is just as important, is the ElementFormat class, which defines the text styling (like the color, alpah, font, etc.) for any ContentElement, via the elementFormat property.

But enough talk. Let's see some things in action! The following example application creates a TextBlock containing the first two sentences from the book "Moby Dick", and displays the first two TextLines from it.

Explanations - after the code (and also inside the code, as comments).


<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
               creationComplete="creationCompleteHandler(event)"
               xmlns:s="library://ns.adobe.com/flex/spark" 
               xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600">
    <fx:Script>
        <![CDATA[
            import flash.text.engine.ContentElement;
            import flash.text.engine.ElementFormat;
            import flash.text.engine.GroupElement;
            import flash.text.engine.TextBlock;
            import flash.text.engine.TextElement;
            import flash.text.engine.TextLine;
            
            import mx.events.FlexEvent;
            
            protected function creationCompleteHandler(event:FlexEvent):void
            {
                // prepare the text
                var str1:String =  "Call me Ishmael. Some years ago - never mind how long precisely - having little or no money in my purse, ";
                var str2:String = "and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world.";
                
                // prepare text formatting
                var format:ElementFormat = new ElementFormat();                
                format.fontSize = 14;
                
                // create two text elements, one for each string
                var te1:TextElement = new TextElement(str1, format);
                var te2:TextElement = new TextElement(str2, format);
                
                // prepare an array containing the two textElements and include them in a GroupElement
                var groupVector:Vector.<ContentElement> = new Vector.<ContentElement>();
                groupVector.push(te1);
                groupVector.push(te2);                
                var ge:GroupElement = new GroupElement(groupVector, format);                
                
                // Create a textblock holding the composed group
                var tb:TextBlock = new TextBlock();
                tb.content = ge;
                
                // Create two TextLines from the TextBlock
                var tl1:TextLine = tb.createTextLine(null550)// first line
                container.addChild(tl1);                
                var tl2:TextLine = tb.createTextLine(tl1, 550)// second line
                container.addChild(tl2);
                tl2.y = tl1.textHeight;
            }
            
        ]]>
    </fx:Script>
    <fx:Declarations>
        <!-- Place non-visual elements (e.g., services, value objectshere -->
    </fx:Declarations>
    
    <s:Group id="group" y="30">
        <s:layout>
            <s:VerticalLayout />
        </s:layout>
        <s:SpriteVisualElement id="container" />            
    </s:Group>
    
</s:Application>


The result :



The first thing you probably notice when you run the application is that the text is not fully shown.
The reason is that when you create a TextLine, you also must specify its desired (max) width. In this case I gave the two TextLines 550 pixels each, and that simply was not enough to hold all of my text.

The second thing to note is that for the first parameter, createTextLine() expects you pass the previously created TextLine (or Null if it's the first). This is required so that the function can calculate the position from which it should continue reading the text.

But what if we wanted to change the code so that ALL of my text will be shown, no matter what?
To do that, we would simply have to keep creating TextLines... until we run out of text :

                var yPos:int = 0;
                var prevTextLine:TextLine = null;
                while (true)
                {
                    yPos += (prevTextLine == null : prevTextLine.textHeight);
                    prevTextLine = tb.createTextLine(prevTextLine, 550);
                    if (prevTextLine == null)
                            break;
                    
                    container.addChild(prevTextLine);  
                    prevTextLine.y = yPos;
                }

createTextLine() returns Null when the end of the text is reached, so at that point we exit the loop.

**

In the next part I'll talk about the more useful Text Layout Framework (TLF) that simplifies things and adds some powerful functionality. I'll demonstrate how you can use it to enable mouse interactions on rendered text (i.e. show a tooltip on a word or phrase on mouseover).

2 comments:

  1. thank you, the explanation was very clear.

    ReplyDelete
  2. Hi,

    for chemical formulars in my application I need to convert the sub tag.
    I get the formula from the DataBase. I am able to display the formula but not to edit, because every part stays single.
    I would need to merge them to a group but I don't know how. I tried some different ways but nothing works.

    // variable with content from DataBase
    var txtFormulaXMLFOTemp:String = "";

    while(txtFormulaXMLFOTemp.length > 0){

    formulaXMLFOSubStart = txtFormulaXMLFOTemp.search("lt sub gt" );
    formulaXMLFOSubEnd = txtFormulaXMLFOTemp.search("lt/sub gt");

    if(formulaXMLFOSubStart > -1 && formulaXMLFOSubEnd > -1 && (formulaXMLFOSubEnd > formulaXMLFOSubStart)){

    txtFormulaXMLFO = txtFormulaXMLFOTemp.substring(0, formulaXMLFOSubStart);
    txtFormulaXMLFOStart = txtFormulaXMLFO;
    txtFormulaXMLFOSub = txtFormulaXMLFOTemp.substring((formulaXMLFOSubStart + 5), formulaXMLFOSubEnd);
    txtFormulaXMLFOTemp = txtFormulaXMLFOTemp.substring((formulaXMLFOSubEnd + 6), txtFormulaXMLFOTemp.length);

    var richTxtFormulaFOStart:RichEditableText;
    richTxtFormulaFOStart = new RichEditableText();
    richTxtFormulaFOStart.text = txtFormulaXMLFOStart;
    hGroupEditView.addElement(richTxtFormulaFOStart);

    var richTxtFormulaFOSub:RichEditableText;
    richTxtFormulaFOSub = new RichEditableText();
    richTxtFormulaFOSub.text = txtFormulaXMLFOSub;
    richTxtFormulaFOSub.textFlow.baselineShift = "subscript";
    richTxtFormulaFOSub.textFlow.dominantBaseline = "ascent";
    richTxtFormulaFOSub.validateNow();
    hGroupEditView.addElement(richTxtFormulaFOSub);
    }
    else{
    txtFormulaXMLFO = txtFormulaXMLFOTemp;
    txtFormulaXMLFOEnd = txtFormulaXMLFO;
    var richTxtFormulaFOEnd:RichEditableText;
    richTxtFormulaFOEnd = new RichEditableText();
    richTxtFormulaFOEnd.text = txtFormulaXMLFOEnd;
    hGroupEditView.addElement(richTxtFormulaFOEnd);
    }







    My future goal:
    Below "editorFO" is a ButtonBarButton with some further possibilities to change the formula (bold, underline, add greek letters).

    That works like shown in other examples:
    protected function subBtn_clickHandler(evt:MouseEvent):void {
    var txtLayFmt:TextLayoutFormat = editor.getFormatOfRange(null,
    editor.selectionAnchorPosition,
    editor.selectionActivePosition);
    txtLayFmt.baselineShift = (txtLayFmt.baselineShift == BaselineShift.SUBSCRIPT ? BaselineShift.SUBSCRIPT : BaselineShift.SUBSCRIPT);
    editor.setFormatOfRange(txtLayFmt,
    editor.selectionAnchorPosition,
    editor.selectionActivePosition);
    editor.setFocus();
    }

    Any help is most appreciated.
    Thank you
    Ellen

    ReplyDelete

DermaRoller