16
Apr
09

Flex/Flash Runtime Fonts with HTML

I’m guessing you arrived here because you did a search for “runtime fonts flex” or similar. I assume, therefore, that you know why you’re here. I got there just a couple months ago.

There’s a wealth of blog articles out there on how to dynamically load font libraries at runtime in Flex. here are just a few:
scottmorgan.com, undefined-type.com, nochump.com

What I seemed to find, though, was that these gave you one of two options: use a single font per Text component , TextArea or Label MXML component(in other words, no style control via HTML/CSS), or use the TextField AS class(which means you say “goodbye” to Design View in Flex Builder).

All I want is to be able to do this:


<VBox>
<TextField htmlText="&lt;span class='GothamBold'&gt;Gotham Bold&lt;/span&gt;&lt;span class='GothamBook'&gt;Gotham Book&lt;/span&gt;"/>
</VBox>

No dice.

So here’s how I did it.

I created a custom TextField AS class that can be called as an MXML component. It looks like this:

package com.[EMPLOYER].[CLIENT].decaf.components
{
import com.[CLIENT].services.StyleManager;

import flash.events.Event;
import flash.events.MouseEvent;
import flash.net.URLRequest;
import flash.net.navigateToURL;
import flash.text.AntiAliasType;
import flash.text.Font;
import flash.text.StyleSheet;
import flash.text.TextFieldAutoSize;
import flash.text.TextFormat;

import mx.controls.Text;
import mx.controls.textClasses.TextRange;
import mx.events.FlexEvent;
import mx.utils.StringUtil;
/**
* This TextField differs from the native Text component in three important ways:
* 1. This TextField component intelligently sets the embedFonts attribute based on registered
* fonts and their ability to display all the glyps set to the htmlText attribute.
* 2. This TextField component exposes a styleSheet attribute, and by default will capture
* Decaf's dispatching of the event fired when the CSS document has loaded, and assign that
* document as its own stylesheet.
* 3. This TextField Component handles the oddly complex relationship between setting selectable
* to false, and yet maintaining link functionality with the hand cursor and navigateToURL intact.
* @author Thomas Brady
* @summary An extension of Text that includes embedded fonts, setting the stylesheet, while preserving link functionality.
* */
public class TextField extends Text
{
private var _styleSheet:StyleSheet;
private var styleMan:StyleManager;
private var _style:Boolean = false;
private var _embed:Boolean = false;

/** We subscribe to three event broadcasts in the constructor:
* 1. FlexEvent.UPDATE_COMPLETE - Our override of the native Text event. A key method.
* 2. MouseEvent.CLICK - Essential for preserving the linked HTML anchors while setting
* 3. StyleManager.dispatcher.ON_CSS_LOAD - When the StyleManager has loaded a CSS doc
* this TextField will then immediately set its stylesheet to the StyleManager's
* reference.
* selectable to false.*/
public function TextField()
{
super();
//super.textField.embedFonts = true;
StyleManager.dispatcher.addEventListener("ON_CSS_LOAD",updateCompleteHandler);
addEventListener(FlexEvent.UPDATE_COMPLETE, updateCompleteHandler);
addEventListener(MouseEvent.CLICK,onClick);
}

/** This is a native function of the Text component. By overriding it, we ensure
* that our improvements are not reset by the native method.
*/
private function updateCompleteHandler(e:Event):void {
super.textField.selectable = false;
super.textField.wordWrap = true;
super.textField.multiline = true;
super.textField.mouseWheelEnabled = false;
super.textField.autoSize = TextFieldAutoSize.LEFT;
if (_style) {
super.textField.styleSheet = StyleManager.styleSheet;
} else {
super.textField.styleSheet = null;
}
super.textField.embedFonts = _embed;
}

/*
* get the "class" from an HTML string, if one exists
*/
private function getFontClassName( str:String ):String
{
var _term:String = "class=";
var _classIdx:Number = str.indexOf( _term );
if( _classIdx > - 1 )
{
var _searchTermLength:Number =_term.length;
var _quote:String = str.slice( _classIdx + _searchTermLength, _classIdx + _searchTermLength + 1 );
var _lastQuoteIdx:uint = str.slice( _classIdx + _searchTermLength + 1, str.length ).indexOf( _quote );
var _className:String = str.slice( _classIdx + _searchTermLength + 1, _classIdx + _searchTermLength + 1 + _lastQuoteIdx );
return _className;
}
else
{
return null;
}
}

override protected function childrenCreated():void
{
super.childrenCreated();
}

/** The heart of the TextField class
* The first important action taken in this method is to strip out unneeded newline, carriage
* return, and tab characters that fowl up rendering. The method goes on to determine whether
* there is a stylesheet, whether the font called for in the HTML has been registered and
* whether the font contains all the glyphs necessary to render the HTML before setting the
* values of _style and _embed Booleans, which are used in updateCompleteHandler.
* NOTE: There is a useful debug loop in here (commented out) that can help you locate glyphs
* that are not included in the font. */
override public function set htmlText(value:String):void {
var regEx:RegExp = /[\n\t\r]/g;
value = value.replace(regEx,"");

if (StyleManager.styleSheet) {
_styleSheet = StyleManager.styleSheet;
//trace("assigned StyleSheet");
// get the classname from HTML string
var _className:String = getFontClassName( value );
//trace( "drawText > _className: " + _className );

// get the font-family from the _className
var _fontName:String = StyleManager.styleSheet.getStyle( "."+_className ).fontFamily;
//trace( "drawText > _fontName: " + _fontName );

if( _className != null && _fontName != null )
{
// create a Font reference
var _font:Font;

var _fonts:Array = Font.enumerateFonts();
for( var c:Number=0; c < _fonts.length; c++ )
{
if( _fonts[c].fontName == _fontName )
{
_font = _fonts[c];
break;
}
}

//trace("FONT:"+_font);

//DEBUG LOOP - checks for what glyph is not included

/*
for (var myIndex:uint=0;myIndex<value.length;myIndex++) {
if (!_font.hasGlyphs(value.substr(myIndex,1))) {
trace("FONT DOES NOT HAVE CHARACTER -"+value.substr(myIndex,1)+"- AT INDEX "+myIndex+" WHICH IS CHAR CODE "+value.charCodeAt(myIndex));
}
}
*/

if( _font.hasGlyphs( value ) )
{
trace("EMBEDDING FONT");
super.textField.styleSheet = StyleManager.styleSheet;
super.textField.antiAliasType = AntiAliasType.ADVANCED;
super.textField.embedFonts = true;
_embed = true;
_style = true;
}
else
{
//trace("NOT EMBEDDING FONT FOR ");
trace( "Embedded font does not contain all the necessary chars. using non-embedded version font for following block");
trace(value);
super.textField.styleSheet = _styleSheet;
_embed = false;
_style = true;
}
}
else
{
// there's no class name, so just use some default
trace("NO CLASS NAME");
super.textField.styleSheet = null;
var _tf2:TextFormat = new TextFormat( "_sans", 12 );
super.textField.defaultTextFormat = _tf2;
_embed = false;
_style = false;
}
} else {
trace("NO STYLESHEET");
}
super.htmlText = value;
}

protected function onClick(pEvent:MouseEvent):void
{
// Find the letter under our click
var index:int = textField.getCharIndexAtPoint(pEvent.localX, pEvent.localY);
if (index != -1)
{
// convert the letter to a text range so we can extract the url
var range:TextRange = new TextRange(this, false, index, index + 1);
// make sure it contains a url
if (range.url.length > 0)
{
// The normal click event strips out the 'event;' portion of the url.
// So to be consistent, let's strip it out, too.
var url:String = range.url;
var tlLink:String = String(range.htmlText);
var targetAndRemaining:String = mx.utils.StringUtil.trim(tlLink.slice(tlLink.lastIndexOf(" TARGET=")+9,tlLink.length - 1));
var target:String = targetAndRemaining.slice(0,targetAndRemaining.indexOf('"'));
/*
if (url.substr(0, 6) == 'event:')
{
url = url.substring(6);
}
*/
// Manually dispatch the link event with the url neatly included
//dispatchEvent(new TextEvent(TextEvent.LINK, false, false, url));
navigateToURL(new URLRequest(url),target);
}
}
}

[Bindable("valueCommit")]
[Inspectable(category="General", defaultValue="")]
public function set styleSheet(sh:StyleSheet):void {
_styleSheet = sh;
super.textField.styleSheet = sh;
}

}
}

Extending Text gets us all the real nuts and bolts we need for functionality and use in MXML. Text, however, does not set embedFonts to true, nor does it even expose that attribute (from its super class Label).

So right there in updateCompleteHandler, which is called any time a property of the TextField changes, we go ahead and set the embedFonts to true. In reality, you’ll probably want to do some tests before you go ahead and set that true: you’ll want to check to see that you have loaded the fonts the TextField wants to use and that the fonts you loaded have all the glyphs in your string, for instance. We also set the stylesheet.

The selectable property is also an interesting challenge. Thanks to this Flex Cookbook tip we can turn selectable off and still get anchor tags to properly link.

The stylesheet dependency introduces some interesting timing issues. I’m instantiating this class via MXML, so I can’t count on the stylesheet (the CSS doc) having loaded. Flex is going to scoop up this MXML and start rendering right away, and the first time updateCompleteHandler gets called, we’re not going to have a stylesheet. So I have a static stylesheet class that dispatches an “ON_CSS_LOAD” event, to which the TextField class listens, and fires updateCompleteHandler when it hears it. At that point, and forward, there will be an actual stylesheet.

There’s also the set styleSheet for overrides, which is inspectable, which lets you modify this variable via AS or MXML.

So what you end up with is an MXML component that you can see in design mode. You can, therefore, easily lay things out. You won’t, yet, see any embedded fonts. You’re not at runtime. You also can’t double-click the component in design view to edit the text, the way you can a Text component. That’s something I’d like to look into.


2 Responses to “Flex/Flash Runtime Fonts with HTML”


  1. 1 dexter
    February 5, 2010 at 9:00 am

    Man you’re my hero
    i looked all around for this (cause i’m a total noob in prog 😀 )
    unfortunately it doesn’t work for me

    it throws me the error 1024:Overriding a function that is not marked for override on the line of the “public function set styleSheet”

    if i try to “public override function” i get the 1119: access of possibly undefined property on every line that contains “StyleManager” or “StyleSheet”

    of course because i didn’t understand the first two lines of your package i changed them into

    package dexTextField
    and
    import mx.styles.StyleManager; cause there is where i found it :))
    i’m using the flex 3.5 sdk

    Please Help! a working example would be golden
    thanks in advance
    dexter

    • 2 Thomas
      February 5, 2010 at 4:35 pm

      Yeah, the difference is the SDK you’re using. I’ve had, recently, to add the override in, as well. 3.2 didn’t require that.

      You’ll get errors on the other parts if you don’t have those classes (AS class files), or if they’re in a different package.

      What is a package?
      What is a class?

      StyleManager, as referenced in my code, is a custom class I wrote. You would need a class that provides the same API (for instance, this one pre-loads fonts and stylesheets).


Leave a comment


April 2009
M T W T F S S
 12345
6789101112
13141516171819
20212223242526
27282930