TextField 示例:报纸风格的文本格式设置

Flash Player 9 和更高版本,Adobe AIR 1.0 和更高版本

“新闻布局”示例设置文本的格式,使文本的外观看起来有点象印刷报纸中的素材。输入文本可以包含标题、副标题和素材正文。在给定显示宽度和高度的情况下,此“新闻布局”示例将会设置标题和副标题的格式,使其占据整个显示区域的宽度。素材文本分布在两列或更多列中。

此示例演示以下 ActionScript 编程技巧:

  • 扩展 TextField 类

  • 加载并应用外部 CSS 文件

  • 将 CSS 样式转换为 TextFormat 对象

  • 使用 TextLineMetrics 类获取有关文本显示大小的信息

若要获取此示例的应用程序文件,请参阅 www.adobe.com/go/learn_programmingAS3samples_flash_cn。“新闻布局”应用程序文件位于 Samples/NewsLayout 文件夹中。该应用程序包含以下文件:

文件

说明

NewsLayout.mxml

NewsLayout.fla

适用于 Flex (MXML) 或 Flash (FLA) 的应用程序的用户界面。

com/example/programmingas3/newslayout/StoryLayoutComponent.as

放置 StoryLayout 实例的 Flex UIComponent 类。

com/example/programmingas3/newslayout/StoryLayout.as

排列用于显示的所有新闻素材组件的主要 ActionScript 类。

com/example/programmingas3/newslayout/FormattedTextField.as

管理本身的 TextFormat 对象的 TextField 类的子类。

com/example/programmingas3/newslayout/HeadlineTextField.as

调整字体大小以适合需要的宽度的 FormattedTextField 类的子类。

com/example/programmingas3/newslayout/MultiColumnTextField.as

在两列或多列之间拆分文本的 ActionScript 类。

story.css

为布局定义文本样式的 CSS 文件。

读取外部 CSS 文件

“新闻布局”应用程序开始时读取本地 XML 文件中的素材文本。然后,它读取提供标题、副标题和主体文本的格式设置信息的外部 CSS 文件。

CSS 文件定义三种样式:用于素材的标准段落样式和分别用于标题和副标题的 h1 和 h2 样式。

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; 
}

用于读取外部 CSS 文件的方法与加载外部 CSS 文件中所述的方法相同。加载 CSS 文件后,应用程序执行 onCSSFileLoaded() 方法,如下所示。

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(); 
}

onCSSFileLoaded() 方法创建一个 StyleSheet 对象并使之分析输入 CSS 数据。素材的主体文本将显示在 MultiColumnTextField 对象中,该对象可以直接使用 StyleSheet 对象。不过,标题字段使用 HeadlineTextField 类,该类使用 TextFormat 对象进行格式设置。

onCSSFileLoaded() 方法调用两次 getTextStyle() 方法,将 CSS 样式声明转换为 TextFormat 对象,以便与两个 HeadlineTextField 对象中的每个对象配合使用。

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; 
}

CSS 样式声明和 TextFormat 对象的属性名称和属性值的含义不同。getTextStyle() 方法可以将 CSS 属性值转换为 TextFormat 对象预期的值。

在页面上排列素材元素

StoryLayout 类可以设置标题、副标题和主体文本字段的格式并将这些字段的布局排列为报纸样式。displayText() 方法一开始会创建并放置各个字段。

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; 
...

将一个字段的 y 属性设置为等于上一个字段的 y 属性加上其自身高度,这样即可将每个字段放置在上一个字段的下面。由于 HeadlineTextField 对象和 MultiColumnTextField 对象可以更改其高度以适应内容,因此需要进行这种动态位置计算。

更改字体大小以适合字段大小

在给定要显示内容的宽度(以像素为单位)和最多行数的情况下,HeadlineTextField 会更改字体大小以使文本适合字段大小。如果文本短,字体大小很大,就会生成 tabloid 样式的标题。如果文本很长,那么字体大小较小。

下面所示的 HeadlineTextField.fitText() 方法的作用就是调整字体大小:

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; 
    } 
}

HeadlineTextField.fitText() 方法使用简单的递归技术来调整字体大小。首先,该方法推测文本中每个字符的平均像素数,并据此计算起始点大小。然后,它更改字体大小并检查文本中的文字是否换行,从而产生比最大值更多的文本行。如果文本行过多,则它会调用 shrinkText() 方法减小字体大小并重试。如果文本行不太多,则它会调用 growText() 方法增大字体大小并重试。当字体大小再增加一点即会产生过多行时,该过程即会停止。

在多列之间拆分文本

MultiColumnTextField 类会在多个 TextField 对象中分布文本,然后将这些对象排列成报纸专栏的样式。

MultiColumnTextField() 构造函数首先创建一个由 TextField 对象构成的数组,每列一个对象,如下所示:

    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); 
    }

每个 TextField 对象均使用 addChild() 方法添加到该数组中,并添加到显示列表中。

只要 StoryLayout 的 text 属性或 styleSheet 属性发生更改,该对象就会调用 layoutColumns() 方法来重新显示文本。layoutColumns() 方法会调用 getOptimalHeight() 方法,以计算在给定布局宽度内适合所有文本所需的正确像素高度。

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; 
    } 
}

首先,getOptimalHeight() 方法计算每列的宽度。然后设置数组中第一个 TextField 对象的宽度和 htmlText 属性。getOptimalHeight() 方法使用第一个 TextField 对象计算文本中自动换行文本的总行数,并据此确定每列中应有多少行。接下来,它调用 TextField.getLineMetrics() 方法以检索 TextLineMetrics 对象,该对象包含有关第一行的文本大小的详细信息。TextLineMetrics.height 属性用像素表示文本行的完整高度,包括上缘、下缘和前导。MultiColumnTextField 对象的最佳高度即为行高度乘以每列行数再加上 4(TextField 对象顶部边框和底部边框各 2 个像素)。

以下是完整 layoutColumns() 方法的代码:

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); 
        } 
    } 
    } 
}

通过调用 getOptimalHeight() 方法设置 preferredHeight 属性后,layoutColumns() 方法会循环访问各个 TextField 对象,将每个对象的高度设置为 preferredHeight 值。然后,layoutColumns() 方法会为每个字段分布刚好足够的文本行,以使每个字段中都不会发生滚动,并且每个连续字段中的文本均会从前一个字段的文本结束处开始。如果文本对齐样式已设置为“justify”,则会调用 justifyLastLine() 方法以对齐字段中最后一行文本。否则,最后一行将被视为段落结束行,而不予对齐。