I’d like to share an ActionScript 3.0 technique I’ve been using on a recent project. As any Flash developer will tell you, it’s always a huge time-saver to use formatted HTML text over multiple text fields laid out by hand. However, when you’re creating objects by hand and listening to click events for each one, you can also associate some data with them. I’m sure you’ve done this before: you keep a reference to something you want to select or activate or pass in each movieclip/sprite, and when it receives a click event, the receiver of the event is that individual clip which has the associated reference. You can simulate multiple clickable text clips with one HTML text field, but you lose that one-to-one association of clip to object.
The technique I want to explain lets you store arbitrary objects inside HTML links. Even though you only have one instance that contains the HTML text field, and one kind of event that’s fired from an HTML link, you can interpret the href of the clicked link and retrieve any kind of object back out of it, even custom class instances. I use this to create “links” which navigate within the flash movie: a Link object encodes the destination, is encoded into an HTML link, and placed in an HTML text field; the click triggers an event handler which interprets the link back into the original Link object and then passes it to a navigation class. Read how it’s done below.
The technique is very simple. If you create a link with the event: pseudo-protocol, it will be handled by Flash, and anything after event: will be stored in the TextEvent event object. So all we have to do is encode the object in a way that doesn’t break out of the surrounding HTML:
<a href="event:[[Object is encoded here]]">...</a>
So the object must be encoded in a string that must not contain double quotes, or other HTML entities that could even potentially interfere with parsing like < and >.
Base 64 encoding is the natural way to go. It’s made to stay within the most normal of ASCII ranges: A-Z, 0-9, + and /. So all we have to do is encode the object in binary, and convert the binary to Base 64. Flex comes with Base 64 encoding classes built-in, fortunately. Getting the object to binary just involves making sure it can be serialized with AMF3. In short, to make your class eligible for automatic serialization and deserialization by AMF3:
- The constructor must take no arguments
- Fields must be public or they won’t be saved
- You must register it with a class alias by calling
flash.net.registerClassAlias(aliasString, class).
If this is unacceptable, you can easily or implement custom serialization/deserialization functions by making your custom class implement IExternalizable.
Of course, if you’re just saving a basic type, this is overkill — but it works — so you gain a lot by generality.
The Code
In HTMLSerializer.as:
package com.partlyhuman.util
{
import flash.events.TextEvent;
import flash.utils.ByteArray;
import mx.utils.Base64Decoder;
import mx.utils.Base64Encoder;
public class HTMLSerializer
{
public static function makeObjectLink(object:*, text:String, styleClass:String = null):String
{
var bytes:ByteArray = new ByteArray();
bytes.writeObject(object);
bytes.position = 0;
var b:Base64Encoder = new Base64Encoder();
b.encode(bytes.readUTFBytes(bytes.length));
var href:String = "event:" + b.drain();
return HTMLGenerator.a(href, text, styleClass);
}
public static function retrieveObjectFromLinkEvent(linkEvent:TextEvent):*
{
if (linkEvent.type != TextEvent.LINK)
{
throw new Error("Inconsistent event type received. Expected TextEvent.LINK.");
return null;
}
var b:Base64Decoder = new Base64Decoder();
b.decode(linkEvent.text);
var bytes:ByteArray = b.drain();
return bytes.readObject();
}
}
}
In HTMLGenerator.as (just a utility class so I can be lazy):
package com.partlyhuman.util
{
public class HTMLGenerator
{
public static function a(href:String, text:String, styleClass:String = null):String
{
return '<a href="' + href + '"'
+ ((styleClass)? ' class="'+styleClass+'"' : "")
+ ">" + text + "</a>";
}
public static function img(src:String, alt:String = "image", width:int = 0, height:int = 0):String
{
return '<img src="' + src + '" alt="' + alt + '"'
+ ((width > 0)? ' width="' + width.toString() + '"' : "")
+ ((height > 0)? ' height="' + height.toString() + '"' : "")
+ '/>';
}
}
}
Testing it
Here’s an object I will serialize, called TestObject:
package com.partlyhuman.test
{
public class TestObject
{
public var a:Array;
public var s:String;
public var i:int;
public function toString():String
{
return "[TestObject\n"
+ "\ta = " + a.join(", ") + "\n"
+ "\ts = " + s + "\n"
+ "\ti = " + i.toString() + "\n"
+ "]";
}
}
}
And here’s a simple test case:
package com.partlyhuman.test
{
import com.partlyhuman.util.HTMLGenerator;
import com.partlyhuman.util.HTMLSerializer;
import flash.display.Sprite;
import flash.events.TextEvent;
import flash.net.registerClassAlias;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
public class TestHTMLSerializer extends Sprite
{
public function TestHTMLSerializer()
{
registerClassAlias("com.partlyhuman.test.TestObject", TestObject);
var obj1:TestObject = new TestObject();
obj1.a = ("This is object number one, hello!").split(" ");
obj1.s = "Atari";
obj1.i = 2600;
var obj2:TestObject = new TestObject();
obj2.a = ("Hello from object numero dos!").split(" ");
obj2.s = "Nintendo";
obj2.i = 64;
var tf:TextField = new TextField();
tf.width = 200;
tf.multiline = true;
tf.wordWrap = true;
tf.autoSize = TextFieldAutoSize.LEFT;
tf.htmlText = "Testing " + HTMLSerializer.makeObjectLink(obj1, "Link to object 1")
+ " And also " + HTMLSerializer.makeObjectLink(obj2, "Link to object 2")
+ " And why not " + HTMLSerializer.makeObjectLink("simple string", "A simple string");
addChild(tf);
tf.addEventListener(TextEvent.LINK, onLink);
}
protected function onLink(event:TextEvent):void
{
trace(HTMLSerializer.retrieveObjectFromLinkEvent(event).toString());
}
}
}
You can see the results below (if you have flash player 9 and javascript):
So there you have it!
Pingback: dispatchEvent » Blood, Sweat, Tears, but Mostly Cupcakes