TextField Example: Newspaper-style text formatting
The News Layout example formats text to look something like a story in a printed newspaper. The input text can contain a headline, a subtitle, and the body of the story. Given a display width and height, this News Layout example formats the headline and the subtitle to take the full width of the display area. The story text is distributed across two or more columns.
This example illustrates the following ActionScript programming techniques:
Extending the TextField class
Loading and applying an external CSS file
Converting CSS styles into TextFormat objects
Using the TextLineMetrics class to get information about text display size
To get the application files for this sample, see FlashPlatformAS3DevGuideExamples.zip. The News Layout application files can be found in the folder Samples/NewsLayout. The application consists of the following files:
File | Description |
---|---|
NewsLayout.mxml or NewsLayout.fla | The user interface for the application for Flex (MXML) or Flash (FLA). |
com/example/programmingas3/newslayout/StoryLayoutComponent.as | A Flex UIComponent class that places the StoryLayout instance. |
com/example/programmingas3/newslayout/StoryLayout.as | The main ActionScript class that arranges all the components of a news story for display. |
com/example/programmingas3/newslayout/FormattedTextField.as | A subclass of the TextField class that manages its own TextFormat object. |
com/example/programmingas3/newslayout/HeadlineTextField.as | A subclass of the FormattedTextField class that adjusts font sizes to fit a desired width. |
com/example/programmingas3/newslayout/MultiColumnTextField.as | An ActionScript class that splits text across two or more columns. |
story.css | A CSS file that defines text styles for the layout. |
Reading the external CSS file
The News Layout application starts by reading story text from a local XML file. Then it reads an external CSS file that provides the formatting information for the headline, subtitle, and main text.
The CSS file defines three styles, a standard paragraph style for the story, and the h1 and h2 styles for the headline and subtitle respectively.
p {
font-family: Georgia, "Times New Roman", Times, _serif;
font-size: 12;
leading: 2;
text-align: justify;
indent: 24;
}
h1 {
font-family: Verdana, Arial, Helvetica, _sans;
font-size: 20;
font-weight: bold;
color: #000099;
text-align: left;
}
h2 {
font-family: Verdana, Arial, Helvetica, _sans;
font-size: 16;
font-weight: normal;
text-align: left;
}
The technique used to read the external CSS file is the same as the technique
described in
Loading an external CSS file.
When the CSS file has been loaded the application executes the
onCSSFileLoaded()
method, shown below.
public function onCSSFileLoaded(event:Event):void
{
this.sheet = new StyleSheet();
this.sheet.parseCSS(loader.data);
h1Format = getTextStyle("h1", this.sheet);
if (h1Format == null)
{
h1Format = getDefaultHeadFormat();
}
h2Format = getTextStyle("h2", this.sheet);
if (h2Format == null)
{
h2Format = getDefaultHeadFormat();
h2Format.size = 16;
}
pFormat = getTextStyle("p", this.sheet);
if (pFormat == null)
{
pFormat = getDefaultTextFormat();
pFormat.size = 12;
}
displayText();
}
The onCSSFileLoaded()
method creates a StyleSheet object and has it parse the
input CSS data. The main text for the story is displayed in a
MultiColumnTextField object, which can use a StyleSheet object directly.
However, the headline fields use the HeadlineTextField class, which uses a
TextFormat object for its formatting.
The onCSSFileLoaded()
method calls the getTextStyle()
method twice to
convert a CSS style declaration into a TextFormat object for use with each of
the two HeadlineTextField objects.
public function getTextStyle(styleName:String, ss:StyleSheet):TextFormat
{
var format:TextFormat = null;
var style:Object = ss.getStyle(styleName);
if (style != null)
{
var colorStr:String = style.color;
if (colorStr != null && colorStr.indexOf("#") == 0)
{
style.color = colorStr.substr(1);
}
format = new TextFormat(style.fontFamily,
style.fontSize,
style.color,
(style.fontWeight == "bold"),
(style.fontStyle == "italic"),
(style.textDecoration == "underline"),
style.url,
style.target,
style.textAlign,
style.marginLeft,
style.marginRight,
style.indent,
style.leading);
if (style.hasOwnProperty("letterSpacing"))
{
format.letterSpacing = style.letterSpacing;
}
}
return format;
}
The property names and the meaning of the property values differ between CSS
style declarations and TextFormat objects. The getTextStyle()
method
translates CSS property values into the values expected by the TextFormat
object.
Arranging story elements on the page
The StoryLayout class formats and lays out the headline, subtitle, and main text
fields into a newspaper-style arrangement. The displayText()
method initially
creates and places the various fields.
public function displayText():void
{
headlineTxt = new HeadlineTextField(h1Format);
headlineTxt.wordWrap = true;
headlineTxt.x = this.paddingLeft;
headlineTxt.y = this.paddingTop;
headlineTxt.width = this.preferredWidth;
this.addChild(headlineTxt);
headlineTxt.fitText(this.headline, 1, true);
subtitleTxt = new HeadlineTextField(h2Format);
subtitleTxt.wordWrap = true;
subtitleTxt.x = this.paddingLeft;
subtitleTxt.y = headlineTxt.y + headlineTxt.height;
subtitleTxt.width = this.preferredWidth;
this.addChild(subtitleTxt);
subtitleTxt.fitText(this.subtitle, 2, false);
storyTxt = new MultiColumnText(this.numColumns, 20,
this.preferredWidth, 400, true, this.pFormat);
storyTxt.x = this.paddingLeft;
storyTxt.y = subtitleTxt.y + subtitleTxt.height + 10;
this.addChild(storyTxt);
storyTxt.text = this.content;
...
Each field is placed below the previous field by setting its y
property to
equal the y
property of the previous field plus its height. This dynamic
placement calculation is needed because HeadlineTextField objects and
MultiColumnTextField objects can change their height to fit their contents.
Altering font size to fit the field size
Given a width in pixels and a maximum number of lines to display, the HeadlineTextField alters the font size to make the text fit the field. If the text is short, the font size is large, creating a tabloid-style headline. If the text is long, the font size is smaller.
The HeadlineTextField.fitText()
method shown below does the font sizing work:
public function fitText(msg:String, maxLines:uint = 1, toUpper:Boolean = false, targetWidth:Number = -1):uint
{
this.text = toUpper ? msg.toUpperCase() : msg;
if (targetWidth == -1)
{
targetWidth = this.width;
}
var pixelsPerChar:Number = targetWidth / msg.length;
var pointSize:Number = Math.min(MAX_POINT_SIZE, Math.round(pixelsPerChar * 1.8 * maxLines));
if (pointSize < 6)
{
// the point size is too small
return pointSize;
}
this.changeSize(pointSize);
if (this.numLines > maxLines)
{
return shrinkText(--pointSize, maxLines);
}
else
{
return growText(pointSize, maxLines);
}
}
public function growText(pointSize:Number, maxLines:uint = 1):Number
{
if (pointSize >= MAX_POINT_SIZE)
{
return pointSize;
}
this.changeSize(pointSize + 1);
if (this.numLines > maxLines)
{
// set it back to the last size
this.changeSize(pointSize);
return pointSize;
}
else
{
return growText(pointSize + 1, maxLines);
}
}
public function shrinkText(pointSize:Number, maxLines:uint=1):Number
{
if (pointSize <= MIN_POINT_SIZE)
{
return pointSize;
}
this.changeSize(pointSize);
if (this.numLines > maxLines)
{
return shrinkText(pointSize - 1, maxLines);
}
else
{
return pointSize;
}
}
The HeadlineTextField.fitText()
method uses a simple recursive technique to
size the font. First it guesses an average number of pixels per character in the
text and from there calculates a starting point size. Then it changes the font
size and checks whether the text has word wrapped to create more than the
maximum number of text lines. If there are too many lines it calls the
shrinkText()
method to decrease the font size and try again. If there are not
too many lines it calls the growText()
method to increase the font size and
try again. The process stops at the point where incrementing the font size by
one more point would create too many lines.
Splitting text across multiple columns
The MultiColumnTextField class spreads text among multiple TextField objects which are then arranged like newspaper columns.
The MultiColumnTextField()
constructor first creates an array of TextField
objects, one for each column, as shown here:
for (var i:int = 0; i < cols; i++)
{
var field:TextField = new TextField();
field.multiline = true;
field.autoSize = TextFieldAutoSize.NONE;
field.wordWrap = true;
field.width = this.colWidth;
field.setTextFormat(this.format);
this.fieldArray.push(field);
this.addChild(field);
}
Each TextField object is added to the array and added to the display list with
the addChild()
method.
Whenever the StoryLayout text
property or styleSheet
property changes, it
calls the layoutColumns()
method to redisplay the text. The layoutColumns()
method calls the getOptimalHeight()
method, to figure out the correct pixel
height that is needed to fit all of the text within the given layout width.
public function getOptimalHeight(str:String):int
{
if (field.text == "" || field.text == null)
{
return this.preferredHeight;
}
else
{
this.linesPerCol = Math.ceil(field.numLines / this.numColumns);
var metrics:TextLineMetrics = field.getLineMetrics(0);
this.lineHeight = metrics.height;
var prefHeight:int = linesPerCol * this.lineHeight;
return prefHeight + 4;
}
}
First the getOptimalHeight()
method calculates the width of each column. Then
it sets the width and htmlText
property of the first TextField object in the
array. The getOptimalHeight()
method uses that first TextField object to
discover the total number of word-wrapped lines in the text, and from that it
identifies how many lines should be in each column. Next it calls the
TextField.getLineMetrics()
method to retrieve a TextLineMetrics object that
contains details about size of the text in the first line. The
TextLineMetrics.height
property represents the full height of a line of text,
in pixels, including the ascent, descent, and leading. The optimal height for
the MultiColumnTextField object is then the line height multiplied by the number
of lines per column, plus 4 to account for the two-pixel border at the top and
the bottom of a TextField object.
Here is the code for the full layoutColumns()
method:
public function layoutColumns():void
{
if (this._text == "" || this._text == null)
{
return;
}
var field:TextField = fieldArray[0] as TextField;
field.text = this._text;
field.setTextFormat(this.format);
this.preferredHeight = this.getOptimalHeight(field);
var remainder:String = this._text;
var fieldText:String = "";
var lastLineEndedPara:Boolean = true;
var indent:Number = this.format.indent as Number;
for (var i:int = 0; i < fieldArray.length; i++)
{
field = this.fieldArray[i] as TextField;
field.height = this.preferredHeight;
field.text = remainder;
field.setTextFormat(this.format);
var lineLen:int;
if (indent > 0 && !lastLineEndedPara && field.numLines > 0)
{
lineLen = field.getLineLength(0);
if (lineLen > 0)
{
field.setTextFormat(this.firstLineFormat, 0, lineLen);
}
}
field.x = i * (colWidth + gutter);
field.y = 0;
remainder = "";
fieldText = "";
var linesRemaining:int = field.numLines;
var linesVisible:int = Math.min(this.linesPerCol, linesRemaining);
for (var j:int = 0; j < linesRemaining; j++)
{
if (j < linesVisible)
{
fieldText += field.getLineText(j);
}
else
{
remainder +=field.getLineText(j);
}
}
field.text = fieldText;
field.setTextFormat(this.format);
if (indent > 0 && !lastLineEndedPara)
{
lineLen = field.getLineLength(0);
if (lineLen > 0)
{
field.setTextFormat(this.firstLineFormat, 0, lineLen);
}
}
var lastLine:String = field.getLineText(field.numLines - 1);
var lastCharCode:Number = lastLine.charCodeAt(lastLine.length - 1);
if (lastCharCode == 10 || lastCharCode == 13)
{
lastLineEndedPara = true;
}
else
{
lastLineEndedPara = false;
}
if ((this.format.align == TextFormatAlign.JUSTIFY) &&
(i < fieldArray.length - 1))
{
if (!lastLineEndedPara)
{
justifyLastLine(field, lastLine);
}
}
}
}
After the preferredHeight
property has been set by calling the
getOptimalHeight()
method, the layoutColumns()
method iterates through the
TextField objects, setting the height of each to the preferredHeight
value.
The layoutColumns()
method then distributes just enough lines of text to each
field so that no scrolling occurs in any individual field, and the text in each
successive field begins where the text in the previous field ended. If the text
alignment style has been set to "justify" then the justifyLastLine()
method is
called to justify the final line of text in a field. Otherwise that last line
would be treated as an end-of-paragraph line and not justified.