assertTrue is the professional blog of Luke Bayes and Ali Mills

Library Type Assets in ActionScript 3.0 :: using the [Embed] metatdata tag

Posted by Ali Mills Wed, 19 Apr 2006 05:49:00 GMT

In a comment to the previous post ActionScript Projects in Flex Builder 2.0, Ben asked how to get library type assets into Flex Builder 2.0 projects. There are two ways. The first is by using the [Embed] (not the Embed from Object/Embed) metadata tag to include assets at compile time. The second is using the Loader class to load them at run time. What’s interesting is that assets loaded at run time can be created in current releases of Flash Authoring and contain legacy ActionScript. What’s challenging is that loaded SWFs containing legacy ActionScript are extremely limited in how they can communicate with their loading AS3 container. They can only communicate with one another over LocalConnection. LocalConnection is neccessary for communication because AS3 runs in a new and different ActionScript Virtual Machine (AVM) than the legacy AS. The ActionScript 3.0 Overview a talks a little more about how the 8.5 player runs legacy ActionScript with the AVM1 and ActionScript 3.0 with the AVM2.

In this post, I’m only going to discuss the first of these two ways: compile time inclusion with the [Embed] meta tag.

The [Embed] meta tag provides a way to include external resources like images, sounds, and fonts into a finished project by compiling them into the SWF. It can be used to import JPEG, GIF, PNG, SVG, SWF, TTF, and MP3 files. It can also be used to add fonts. The Developing Flex Applications chapter Embedding Resources does a good job of explaining the details of [Embed]. If you’re interested in this topic, you should read it. To demonstrate how [Embed] works, we’ll add a “Remember” button to the AutoComplete project we created in the ActionScript Projects in Flex Builder 2.0 post.

The first step to adding a “Remember” button is to create three PNGs for the button states up, over, and down with the graphics program of your choice. Create them (or download them from here) and place them in a folder called “img” inside you AutoComplete project folder at the same level as AutoComplete.as. Name the PNGs “Remember_up.png”, “Remember_over.png”, and “Remember_down.png”.

The second step is to write the code to embed and use the button art. This code will be added to AutoCompleteRunner.as. At the end of ActionScript Projects in Flex Builder 2.0 AutoCompleteRunner.as looked like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package {
    import flash.display.Sprite;

    public class AutoCompleteRunner extends Sprite {

        public function AutoCompleteRunner() {
            var autoCompTxt:AutoComplete = new AutoComplete("Start typing...");
            autoCompTxt.addToDictionary("Flex");
            autoCompTxt.addToDictionary("Fireworks");
            autoCompTxt.addToDictionary("Dreamweaver");
            autoCompTxt.addToDictionary("Contribute");
            autoCompTxt.addToDictionary("ColdFusion");
            autoCompTxt.addToDictionary("Flash Paper");
            autoCompTxt.addToDictionary("Flash");
            autoCompTxt.addToDictionary("Breeze");
            autoCompTxt.addToDictionary("JRun");
            autoCompTxt.addToDictionary("Director");
            autoCompTxt.addToDictionary("Studio");
            autoCompTxt.addToDictionary("Captivate");
            autoCompTxt.addToDictionary("RoboHelp");
            autoCompTxt.addToDictionary("Authorware");
            autoCompTxt.addToDictionary("Web Publishing System");            

            addChild(autoCompTxt);            
        }
    }
}

According to Using the [Embed] metadata tag, “You must use the [Embed] metadata tag before a variable definition, where the variable is of type Class.” Doing so leaves AutoCompleteRunner.as looking like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package {
    import flash.display.Sprite;

    public class AutoCompleteRunner extends Sprite {
        [Embed(source="/img/Remember_up.png")] 
        private var Remember_UpImage:Class;

        [Embed(source="/img/Remember_over.png")] 
        private var Remember_OverImage:Class;

        [Embed(source="/img/Remember_down.png")] 
        private var Remember_DownImage:Class;

        public function AutoCompleteRunner() {
            var autoCompTxt:AutoComplete = new AutoComplete("Start typing...");
            autoCompTxt.addToDictionary("Flex");
            autoCompTxt.addToDictionary("Fireworks");
            autoCompTxt.addToDictionary("Dreamweaver");
            autoCompTxt.addToDictionary("Contribute");
            autoCompTxt.addToDictionary("ColdFusion");
            autoCompTxt.addToDictionary("Flash Paper");
            autoCompTxt.addToDictionary("Flash");
            autoCompTxt.addToDictionary("Breeze");
            autoCompTxt.addToDictionary("JRun");
            autoCompTxt.addToDictionary("Director");
            autoCompTxt.addToDictionary("Studio");
            autoCompTxt.addToDictionary("Captivate");
            autoCompTxt.addToDictionary("RoboHelp");
            autoCompTxt.addToDictionary("Authorware");
            autoCompTxt.addToDictionary("Web Publishing System");            

            addChild(autoCompTxt);            
        }
    }
}

Or, as I’ve been starting to place the [Embed] and the variable definition on the same line, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package {
    import flash.display.Sprite;

    public class AutoCompleteRunner extends Sprite {
        [Embed(source="/img/Remember_up.png")] private var Remember_UpImage:Class;

        [Embed(source="/img/Remember_over.png")] private var Remember_OverImage:Class;

        [Embed(source="/img/Remember_down.png")] private var Remember_DownImage:Class;

        public function AutoCompleteRunner() {
            var autoCompTxt:AutoComplete = new AutoComplete("Start typing...");
            autoCompTxt.addToDictionary("Flex");
            autoCompTxt.addToDictionary("Fireworks");
            autoCompTxt.addToDictionary("Dreamweaver");
            autoCompTxt.addToDictionary("Contribute");
            autoCompTxt.addToDictionary("ColdFusion");
            autoCompTxt.addToDictionary("Flash Paper");
            autoCompTxt.addToDictionary("Flash");
            autoCompTxt.addToDictionary("Breeze");
            autoCompTxt.addToDictionary("JRun");
            autoCompTxt.addToDictionary("Director");
            autoCompTxt.addToDictionary("Studio");
            autoCompTxt.addToDictionary("Captivate");
            autoCompTxt.addToDictionary("RoboHelp");
            autoCompTxt.addToDictionary("Authorware");
            autoCompTxt.addToDictionary("Web Publishing System");            

            addChild(autoCompTxt);            
        }
    }
}

That looks good; now compile/debug.

Uh oh! The compile fails. We have “Errors in Project”. The error, unfortunately, is due to what seems to be a bug in either Flex Builder 2.0 or the Flex Framework. The error (whose description you can see in the “Problems” view) is that the, “Definition of base class ‘SkinBitmap’ not found”. This seems like a bug because according to documentation the SkinBitmap class is, “A subclass of the Bitmap class that implements the IFlexDisplayObject interface. This class is the base class for Bitmap skin elements.” Why would IFlexDisplayObject , a framework class, be part of including assets in an ActionScript project? On the other hand, maybe the object relationship is by design. In the article Accessing the embedded object, there’s a line that says, “The data type of the class is either SkinBitmap, if the image is a JPEG, GIF, or PNG file, or SkinSprite if it is a SVG or SWF file. If you embed an MP3 file, data type of the class is Sound.”

Why does it matter if we have to implement an IFlexDisplayObject? Why should we care? One reason is the concern of including code in your project that can’t be manipulated. Code that, if broken, you can’t personally fix. The Flex Framework is an example of such code, and to use IFlexDisplayObject we need to include the framework. As long as the framework remains closed-source [Embed] shouldn’t create a class that implements or sub-classes anything in the Flex Framework. If the framework were open-sourced this would be less of an issue (hint, hint)...

A second reason to be concerned is file size. Without the framework our project is 2K. With the framework, it’ll probably be larger. To get the [Embed] meta tag working and find out how much our SWF size grows, let’s include the Flex Framework. To do so, take the following steps:

  • Right-click on your AutoComplete project in the “Navigator” window.
  • Select “Properties”.
  • In the “Properties for AutoComplete” window select “ActionScript Build Path” from the left-hand option list.
  • Select the “Libraries” tab.
  • Press the “Add SWC” button.
  • Browse to the Flex Builder 2.0 installation location which is probably “C:\Program Files\Adobe\Flex Builder 2 Beta 2”.
  • Open the “frameworks” folder.
  • Open the “libs” folder.
  • Select “framework.swc”.

After OKing out of the “Properties for AutoComplete” window, your project is now configured to point at classes in the Flex Framework. Compile/debug again.

Woo Hoo!!! We get a successful compilation! (We’ll get to why the images aren’t showing up in a moment, but for now trust me that they’ve been embedded.) Let’s check AutoCompleteRunner.swf and see how much it’s grown in size. Go into the AutoComplete project’s “bin” folder on your file system (probably located at “C:\Documents and Settings\USER_NAME\My Documents\Flex\AutoComplete”) to find it. Notice that it’s grown to 18K. That’s a 16K size increase. How much of the increase was caused by the framework, and how much was caused by the embeded PNGs? To answer this question, comment out the three [Embed] lines, recompile, and recheck AutoCompleteRunner.swf. It’s now a little smaller. It’s 15K. It appears that the framework added 13K and the three PNGs added 3K (which is magical considering that my three images together total 88K). While 15K isn’t huge, it still seems like a little too much for a project that doesn’t do anything framework related. That said, 15K isn’t horrible either and a lot of appreciation goes to Flex engineers like Gordon Smith and Manish Jethani for their efforts towards Avoiding linker dependencies.

Why aren’t the images showing up? They’re not because we haven’t attached them to the display list. While we could add them directly, we’re dealing with button images so we’ll add them to a SimpleButton instance and add it to the display. Creating instances of the images and assigning them to the button’s states takes some work. The code that does this follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package {
    import flash.display.Bitmap;
    import flash.display.SimpleButton;
    import flash.display.Sprite;
    import flash.util.describeType;
    import flash.util.getClassByName;

    public class AutoCompleteRunner extends Sprite {
        [Embed(source="/img/Remember_up.png")] private var Remember_UpImage:Class;
        [Embed(source="/img/Remember_over.png")] private var Remember_OverImage:Class;
        [Embed(source="/img/Remember_down.png")] private var Remember_DownImage:Class;

        public var className:String;

        public function AutoCompleteRunner() {
            var autoCompTxt:AutoComplete = new AutoComplete("Start typing...");
            autoCompTxt.addToDictionary("Flex");
            autoCompTxt.addToDictionary("Fireworks");
            autoCompTxt.addToDictionary("Dreamweaver");
            autoCompTxt.addToDictionary("Contribute");
            autoCompTxt.addToDictionary("ColdFusion");
            autoCompTxt.addToDictionary("Flash Paper");
            autoCompTxt.addToDictionary("Flash");
            autoCompTxt.addToDictionary("Breeze");
            autoCompTxt.addToDictionary("JRun");
            autoCompTxt.addToDictionary("Director");
            autoCompTxt.addToDictionary("Studio");
            autoCompTxt.addToDictionary("Captivate");
            autoCompTxt.addToDictionary("RoboHelp");
            autoCompTxt.addToDictionary("Authorware");
            autoCompTxt.addToDictionary("Web Publishing System");            

            var rememberBtn:SimpleButton = configureButton(new SimpleButton(), "Remember");

            addChild(rememberBtn).x = addChild(autoCompTxt).width;
        }

        private function configureButton(button:SimpleButton, name:String):SimpleButton {
            button.upState = createImage(name + "_UpImage");
            button.overState = createImage(name + "_OverImage");
            button.downState = createImage(name + "_DownImage");
            button.hitTestState = createImage(name + "_UpImage");
            button.useHandCursor = true;
            return button;
        }

        private function createImage(embedLinkage:String):Bitmap {
            if(className == null) {
                className = describeType(this).@name.toXMLString();
            }
            var imgName:String = className + "_" + embedLinkage;
            var imgRef:Class = getClassByName(imgName);
            var img:Bitmap = new imgRef();
            return img;
        }
    }
}

In the constructor, a new SimpleButton instance called rememberBtn is created and configured with the helper method configureButton(). The configureButton() method in turn calls the workhorse of this entire process – createImage(). Since the reflection utility describeType() is an expensive operation, the first thing createImage() does is cache the className that the combination of describeType() and E4X return. Then, the method uses a little know fact that the [Embed] meta tag creates a class definition whose constructor can be accessed with a string formatted as “ClassNameInWhichTheEmbedTagIsDefined_ClassVariableDefinedAfterTheEmbedTag”. The getClassByName() package-level function uses this string to return the constructor, and the next lines in createImage() create and return an instance of the embedded images.

That’s it for using the [Embed] metadata tag. The project with an added SharedObject based persistence layer follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package {
    import flash.display.Bitmap;
    import flash.display.Sprite;
    import flash.display.SimpleButton;
    import flash.net.SharedObject;
    import flash.util.describeType;
    import flash.util.getClassByName;
    import flash.events.MouseEvent;

    public class AutoCompleteRunner extends Sprite {
        [Embed(source="/img/Remember_up.png")] private var Remember_UpImage:Class;
        [Embed(source="/img/Remember_over.png")] private var Remember_OverImage:Class;
        [Embed(source="/img/Remember_down.png")] private var Remember_DownImage:Class;

        private var savedWords:Array;
        private var persistenceLayer:SharedObject; 
        private var autoCompleteTxt:AutoComplete;
        private var rememberBtn:SimpleButton;

        public var className:String;

        public function AutoCompleteRunner() {                        
            savedWords = new Array();

            configureStage();

            persistenceLayer = SharedObject.getLocal("AutoCompleteRunner"); 
            configureSharedObject(persistenceLayer);

            autoCompleteTxt = new AutoComplete("Start typing...");
            configureAutoComplete(autoCompleteTxt);

            rememberBtn = new SimpleButton();
            configureButton(rememberBtn, "Remember");
            rememberBtn.addEventListener(MouseEvent.CLICK, clickHandler);

            addChild(rememberBtn).x = addChild(autoCompleteTxt).width;
        }

        private function configureStage():void {
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;
        }

        private function configureSharedObject(sharedObject:SharedObject):void {
            if(sharedObject.data.savedWords != undefined) {
                savedWords = sharedObject.data.savedWords;
            }
        }

        private function configureAutoComplete(autoComplete:AutoComplete):void {
            autoComplete.addToDictionary("Flex");
            autoComplete.addToDictionary("Fireworks");
            autoComplete.addToDictionary("Dreamweaver");
            autoComplete.addToDictionary("Contribute");
            autoComplete.addToDictionary("ColdFusion");
            autoComplete.addToDictionary("Flash Paper");
            autoComplete.addToDictionary("Flash");
            autoComplete.addToDictionary("Breeze");
            autoComplete.addToDictionary("JRun");
            autoComplete.addToDictionary("Director");
            autoComplete.addToDictionary("Studio");
            autoComplete.addToDictionary("Captivate");
            autoComplete.addToDictionary("RoboHelp");
            autoComplete.addToDictionary("Authorware");
            autoComplete.addToDictionary("Web Publishing System");    

            var len:uint = savedWords.length;
            for(var i:uint; i < len; i++) {
                autoComplete.addToDictionary(savedWords[i]);
            }
        }

        private function configureButton(button:SimpleButton, name:String):void {
            button.upState = createImage(name + "_UpImage");
            button.overState = createImage(name + "_OverImage");
            button.downState = createImage(name + "_DownImage");
            button.hitTestState = createImage(name + "_UpImage");
            button.useHandCursor = true;
        }

        private function createImage(embedLinkage:String):Bitmap {
            if(className == null) {
                className = describeType(this).@name.toXMLString();
            }
            var imgName:String = className + "_" + embedLinkage;
            var imgRef:Class = getClassByName(imgName);
            var img:Bitmap = new imgRef();
            return img;
        }

        private function clickHandler(event:MouseEvent):void {
            var newWord:String = autoCompleteTxt.text;
            autoCompleteTxt.addToDictionary(newWord);
            savedWords.push(newWord);
            persistenceLayer.data.savedWords = savedWords;
            persistenceLayer.flush();
        }
    }
}

There’s an issue with the 8.5 desktop player where flushing a SharedObject causes it to crash, so the above code needs to run in a browser player. Change the project’s debug profile to open in a browser window instead of the desktop player. Search for, “Now, let’s change our debug profile” in ActionScript Projects in Flex Builder 2.0 for directions to do this.

Tags  | 2 comments

Comments

  1. Ben commented: Very cool. Thanks for taking my request :) I am sure I will need to read this a few more times before it completely sinks in but I think this could be a valuable resource to people making the transition to Flex.
    Posted: about 7 hours later.
  2. Dominick replied: Yes, thanks for the article. Keep it up!
    Posted: about 8 hours later.

Your Reply

Comment Form.

Fields denoted with an "*" are required.