Ejemplo de TextField: Formato de texto con estilo periodístico

Flash Player 9 y posterior, Adobe AIR 1.0 y posterior

El ejemplo News Layout aplica formato a texto para que parezca una noticia de un periódico impreso. El texto de entrada puede contener un titular, un subtí­tulo y el cuerpo de la noticia. Dadas la anchura y la altura de visualización, el ejemplo News Layout aplica formato al titular y al subtítulo para que ocupen toda la anchura del área de visualización. El texto de la noticia se distribuye en dos o más columnas.

En este ejemplo se ilustran las siguientes técnicas de programación en ActionScript:

  • Ampliación de la clase TextField

  • Cargar y aplicar un archivo CSS externo

  • Convertir estilos de CSS en objetos TextFormat

  • Utilizar la clase TextLineMetrics para obtener información sobre el tamaño de la visualización de texto

Para obtener los archivos de la aplicación de este ejemplo, consulte www.adobe.com/go/learn_programmingAS3samples_flash_es. Los archivos de la aplicación News Layout se encuentran en la carpeta Samples/NewsLayout. La aplicación consta de los siguientes archivos:

Archivo

Descripción

NewsLayout.mxml

o

NewsLayout.fla

La interfaz de usuario de la aplicación para Flex (MXML) o Flash (FLA).

com/example/programmingas3/newslayout/StoryLayoutComponent.as

Una clase UIComponent de Flex que coloca la instancia de StoryLayout.

com/example/programmingas3/newslayout/StoryLayout.as

La clase principal de ActionScript que dispone todos los componentes de una noticia para mostrarla.

com/example/programmingas3/newslayout/FormattedTextField.as

Una subclase de la clase TextField que administra su propio objeto TextFormat.

com/example/programmingas3/newslayout/HeadlineTextField.as

Una subclase de la clase FormattedTextField que ajusta el tamaño de las fuentes a una anchura deseada.

com/example/programmingas3/newslayout/MultiColumnTextField.as

Una clase de ActionScript que divide el texto en dos o más columnas.

story.css

Un archivo CSS que define estilos de texto para el diseño.

Lectura del archivo CSS externo

La aplicación News Layout empieza por leer en un archivo XML local el texto de la noticia. A continuación, lee el archivo CSS externo que proporciona la información de formato para el titular, el subtí­tulo y el texto principal.

El archivo CSS define tres estilos, un estilo de párrafo estándar para la noticia y los estilos h1 y h2 para el titular y el subtítulo respectivamente.

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

La técnica usada para leer el archivo CSS externo es la misma que la descrita en Carga de un archivo CSS externo. Cuando el archivo CSS se ha cargado, la aplicación ejecuta el método onCSSFileLoaded(), tal y como se muestra a continuación.

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

El método onCSSFileLoaded() crea un objeto StyleSheet y hace que analice los datos CSS de entrada. El texto principal de la historia se muestra en un objeto MultiColumnTextField que puede utilizar un objeto StyleSheet directamente. No obstante, los campos de titular usan la clase HeadlineTextField, que utiliza un objeto TextFormat para su formato.

El método onCSSFileLoaded() llama dos veces a getTextStyle() para convertir una declaración de estilos CSS en un objeto TextFormat para utilizarlo con cada uno de los dos objetos 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; 
}

Los nombres de propiedad y el significado de los valores de propiedad difieren entre las declaraciones de estilos CSS y los objetos TextFormat. El método getTextStyle() traduce los valores de propiedades CSS en los valores que espera el objeto TextFormat.

Disposición de los elementos de la noticia en la página

La clase StoryLayout aplica formato a los campos de titular, subtí­tulo y texto principal y los dispone como en un periódico. El método displayText() crea y sitúa inicialmente los distintos campos.

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

Cada uno de los campos se coloca bajo el anterior estableciendo su propiedad y en un valor igual al de la propiedad y del campo anterior más su altura. Este cálculo dinámico de la posición es necesario, ya que los objetos HeadlineTextField y MultiColumnTextField pueden modificar su altura para ajustarla a su contenido.

Modificación del tamaño de fuente para ajustar el tamaño de un campo

Dados una anchura en píxeles y un número máximo de líneas para mostrar, HeadlineTextField modifica el tamaño de fuente para ajustar el texto al campo. Si el texto es corto, el tamaño de fuente es muy grande, y se creará un titular de tipo tabloide. Si el texto es largo, tamaño de fuente es más pequeño.

El método HeadlineTextField.fitText() que se muestra a continuación ajusta el tamaño de fuente:

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

El método HeadlineTextField.fitText() utiliza una técnica recursiva sencilla para ajustar el tamaño de la fuente. En primer lugar calcula un número de píxeles por carácter en el texto y con ese dato calcula un tamaño de punto inicial. A continuación cambia el tamaño de la fuente y comprueba si el texto se ha ajustado para crear un número de líneas de texto superior al máximo. Si hay demasiadas lí­neas, llama al método shrinkText() para reducir el tamaño de fuente y lo intenta de nuevo. Si no hay demasiadas lí­neas, llama al método growText() para aumentar el tamaño de fuente y lo intenta de nuevo. El proceso se detiene en el punto en el que, al aumentar un punto el tamaño de fuente, se crearán demasiadas líneas.

División del texto en varias columnas

La clase MultiColumnTextField divide el texto en varios objetos TextField que se disponen como las columnas de un periódico.

El constructor MultiColumnTextField() crea primero un conjunto de objetos TextField, uno por cada columna, tal y como se muestra a continuación:

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

Todos los objetos TextField se añaden al conjunto y a la lista de visualización con el método addChild().

Siempre que cambien las propiedades text o styleSheet del objeto StoryLayout, se llama al método layoutColumns() para volver a mostrar el texto. El método layoutColumns() llama al método getOptimalHeight() method, para calcular la altura en pí­xeles necesaria para ajustar todo el texto en la anchura de diseño especificada.

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

En primer lugar, el método getOptimalHeight() calcula la anchura de cada columna. Posteriormente se establece la anchura y la propiedad htmlText del primer objeto TextField en el conjunto. El método getOptimalHeight() utiliza ese primer objeto TextField para descubrir el número total de líneas ajustadas en el texto y, a partir de ese número, determina cuántas líneas debe haber en cada columna. Posteriormente llama al método TextField.getLineMetrics() para recuperar un objeto TextLineMetrics que contiene detalles sobre el tamaño del texto en la primera lí­nea. La propiedad TextLineMetrics.height representa la altura total de una ­línea de texto, en píxeles, incluidos los valores ascendente, descendente y de interlineado. La altura óptima para el objeto MultiColumnTextField es la altura de línea multiplicada por el número de líneas por columna, más 4 para tener en cuenta el borde de dos píxeles que hay por encima y por debajo de un objeto TextField.

A continuación se muestra el código del método 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); 
        } 
    } 
    } 
}

Una vez establecida la propiedad preferredHeight al llamar al método getOptimalHeight(), el método layoutColumns() recorre los objetos TextField y establece la altura de cada uno de ellos en el valor de preferredHeight. Posteriormente el método layoutColumns() distribuye a cada campo las líneas de texto suficientes para que no se produzca desplazamiento en ningún campo y el texto de cada campo empiece donde acabó el texto del campo anterior. Si el estilo de alineación del texto está establecido en "justify" (justificado), se llama al método justifyLastLine() para justificar la última lí­nea del texto de un campo. De lo contrario, esa última lí­nea se tratará como una línea de final de párrafo y no se justificará.