TextField 範例: 報紙樣式文字格式

Flash Player 9 以及更新的版本,Adobe AIR 1.0 以及更新的版本

新聞版面範例會格式化文字,使其外觀看起來就像報紙上的新聞報導一樣。輸入文字可以包含報導的標題、副標題和內文。指定顯示寬度和高度之後,此新聞版面範例就會格式化標題和副標題,以取得顯示區域的完整寬度。新聞報導的內容文字會分佈在兩欄或更多欄中。

此範例會說明下列 ActionScript 程式設計的技巧:

  • 擴充 TextField 類別

  • 載入和套用外部 CSS 檔

  • 將 CSS 樣式轉換為 TextFormat 物件

  • 使用 TextLineMetrics 類別取得文字顯示大小的資訊

若要取得此樣本的應用程式檔案,請參閱 www.adobe.com/go/learn_programmingAS3samples_flash_tw。您可以在 Samples/NewsLayout 的資料夾中找到新聞版面應用程式檔。此應用程式是由下列檔案組成:

檔案

說明

NewsLayout.mxml

NewsLayout.fla

應用程式使用者介面,在 Flash 中為 FLA,在 Flex 中為 MXML。

com/example/programmingas3/newslayout/StoryLayoutComponent.as

放置 StoryLayout 實體的 Flex UIComponent 類別。

com/example/programmingas3/newslayout/StoryLayout.as

主要 ActionScript 類別,可排列新聞報導的所有元件以進行顯示。

com/example/programmingas3/newslayout/FormattedTextField.as

TextField 類別的子類別,用以管理自己的 TextFormat 物件。

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() 方法放大字體,然後再試一次。當再放大 1 點的字體會建立太多的文字行時,此程序就會停止。

讓文字分佈在多欄

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

使用 addChild() 方法將每個 TextField 物件新增至陣列,並新增至顯示清單。

只要 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() 方法接著會將剛好的文字行數分佈至每個欄位,這樣就不需要捲動任何個別欄位,而且每個連續欄位中文字的開始位置就是前一個欄位中文字的結束位置。如果文字對齊樣式已設為「齊行」,則會呼叫 justifyLastLine() 方法來對齊欄位中的最後一行文字。否則,最後一行會視為段落結尾,而不會對齊字行。