TextField Example: Newspaper-style text formatting
Flash Player 9 and later, Adobe AIR 1.0 and
later
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
www.adobe.com/go/learn_programmingAS3samples_flash
.
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.
|
|
|
|
|