TextField の例:新聞形式のテキストフォーマット

Flash Player 9 以降、Adobe AIR 1.0 以降

ここで示す News Layout という例では、紙面版の新聞の記事と同様に見えるようにテキストをフォーマットします。 入力テキストには記事のヘッドライン、サブタイトル、および本文を含めることができます。 この News Layout という例は、表示領域の幅と高さが指定されると、表示領域の幅いっぱいに表示されるようにヘッドラインとサブタイトルをフォーマットします。記事の本文テキストは 2 段組みまたは 3 段組みで配置されます。

この例では、ActionScript の以下のプログラミング手法について説明します。

  • TextField クラスの拡張

  • 外部 CSS ファイルのロードと適用

  • CSS スタイルから TextFormat オブジェクトへの変換

  • TextLineMetrics クラスによるテキスト表示サイズ情報の取得

このサンプルのアプリケーションのファイルを入手するには、 www.adobe.com/go/learn_programmingAS3samples_flash_jp を参照してください。 News Layout アプリケーションのファイルは、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

TextField クラスのサブクラスであり、それ自体の TextFormat オブジェクトを管理します。

com/example/programmingas3/newslayout/HeadlineTextField.as

FormattedTextField クラスのサブクラスであり、適切な幅に合わせてフォントサイズを調整します。

com/example/programmingas3/newslayout/MultiColumnTextField.as

テキストを複数の段に分割する ActionScript クラスです。

story.css

レイアウトのテキストスタイルが定義されている CSS ファイルです。

外部 CSS ファイルの読み取り

News Layout アプリケーションは、最初にローカル XML ファイルから記事のテキストを読み取ります。 次に、ヘッドライン、サブタイトル、および本文テキストのフォーマット情報が含まれている外部 CSS ファイルを読み取ります。

CSS ファイルには 3 つのスタイルが定義されています。記事本文用の標準段落スタイル、およびヘッドラインとサブタイトルにそれぞれ対応する 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() メソッドを 2 回呼び出して CSS スタイル宣言を TextFormat オブジェクトに変換し、2 つの HeadlineTextField オブジェクトのそれぞれで TextFormat オブジェクトを使用できるようにします。

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 では、幅(ピクセル単位)と最大表示行数が指定されると、テキストがフィールドいっぱいに表示されるようにフォントサイズが変更されます。 テキストが短い場合には、フォントサイズが非常に大きくなり、タブロイドスタイルのヘッドラインが作成されます。テキストが長い場合は、フォントサイズは小さくなります。

次に示す 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() メソッドでは、単純な再帰的手法によってフォントサイズが変更されます。最初にテキストの 1 文字あたりの平均ピクセル数が推定され、それに基づいて開始ポイントサイズが算出されます。 次に、フォントサイズが変更されます。その後、テキストが折り返されたされたためにテキスト行の最大数を超えて行が生成されていないかどうかが確認されます。行数が多すぎる場合には、 shrinkText() メソッドが呼び出されて、フォントサイズを小さくする操作が何回か試行されます。最大行数を超えていない場合には、 growText() メソッドが呼び出されて、フォントサイズを大きくする操作が何回か試行されます。フォントサイズを現在の値よりも 1 ポイント大きくすると最大行数を超えるような状態に達したときに、処理が終了します。

複数の段へのテキストの分割

MultiColumnTextField クラスは、複数の TextField オブジェクトにテキストを配分します。これらのオブジェクトは、後で新聞の段組みのように配置されます。

まず、 MultiColumnTextField() コンストラクターによって、次のように TextField オブジェクトの配列が作成されます。1 つのオブジェクトが 1 つの段に対応します。

    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 オブジェクトを使用してテキストの折り返された総行数を調べ、その総行数に基づいて、各段に配分する行数を特定します。続いて、最初の行のテキストのサイズの詳細情報を含む TextLineMetrics オブジェクトを取得するために、 TextField.getLineMetrics() メソッドが呼び出されます。 TextLineMetrics.height プロパティは、アセント、ディセント、および行送りを含めたテキスト行全体の高さを表します(ピクセル単位)。MultiColumnTextField オブジェクトの最適な高さは、この行全体の高さと 1 段の行数を乗算し、さらに 4 ピクセルを加算したものになります。この 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() メソッドが呼び出され、フィールド内のテキストの最後の行がジャスティファイされます。それ以外の場合は、最後の行は段落の終わりの行として扱われ、ジャスティファイされません。