Пример класса TextField: форматирование текста в газетном стиле

Flash Player 9 и более поздних версий, Adobe AIR 1.0 и более поздних версий

В примере News Layout текст форматируется в виде газетной статьи. Вводимый текст может содержать заголовок, подзаголовок и тело статьи. Используя заданные ширину и высоту отображения, пример News Layout форматирует заголовок и подзаголовок так, чтобы они занимали всю ширину области отображения. Текст статьи распределяется по двум или более столбцам.

В этом примере демонстрируется использование следующих приемов программирования на ActionScript:

  • расширение класса TextField;

  • загрузка и применение внешнего CSS-файла;

  • преобразование стилей CSS в объекты TextFormat;

  • использование класса TextLineMetrics для получения информации о размере области отображения текста.

Получить файлы приложения для этого примера можно на странице www.adobe.com/go/learn_programmingAS3samples_flash_ru. Файлы приложения News Layout находятся в папке Samples/NewsLayout. Приложение состоит из следующих файлов.

File

Описание

NewsLayout.mxml

или

NewsLayout.fla

Пользовательский интерфейс приложения для Flex (MXML) или Flash (FLA).

com/example/programmingas3/newslayout/StoryLayoutComponent.as

Класс UIComponent Flex, который размещает экземпляр StoryLayout.

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-файл определяет три стиля: стандартный стиль абзаца для текста статьи и стили 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 изменяет размер шрифта так, чтобы текст поместился в поле. Если текст короткий, используется большой размер шрифта, в результате чего заголовок приобретает стиль таблоида. Если текст длинный, то размер шрифта становится меньше.

Метод 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().

Каждый раз, когда изменяется свойство text или styleSheet StoryLayout, этот класс вызывает метод 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() вычисляет ширину каждого столбца. Затем он устанавливает ширину и свойство htmlText для первого объекта TextField в массиве. Метод getOptimalHeight() использует первый объект TextField для получения общего числа строк с переносом слов в тексте, в соответствии с которым он затем определяет количество строк в каждом столбце. После этого вызывается метод TextField.getLineMetrics() для получения объекта TextLineMetrics, содержащего сведения о размере текста в первой строке. Свойство TextLineMetrics.height представляет полную высоту строки текста в пикселах, включая надстрочные и подстрочные выносные элементы и междустрочный пробел. Таким образом, оптимальная высота объекта MultiColumnTextField равняется высоте строки, умноженной на количество строк в столбце, плюс 4 пиксела для рамки вверху и внизу объекта TextField, равной двум пикселам.

Ниже приводится код полного метода 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); 
        } 
    } 
    } 
}

После определения свойства preferredHeight с помощью метода getOptimalHeight() метод layoutColumns() выполняет повторное прохождение через все объекты TextField, устанавливая значение preferredHeight в качестве высоты для каждого из них. Затем метод layoutColumns() распределяет каждому текстовому полю достаточное количество строк, чтобы в них не требовалась прокрутка, и текст в каждом последующем столбце начинался с того места, на котором закончился предыдущий. Если стилю выравнивания текста задано значение justify, то вызывается метод justifyLastLine() для выравнивания последней строки текста в поле. В противном случае последняя строка обрабатывается как последняя строка абзаца и не выравнивается.