Events

EventDispatcher is the base class for all objects that can be a target for events. Many native classes inherit from this class with their own appropriate events:

[code]

import flash.display.Sprite;
import flash.events.MouseEvent;
// this draws a button listening to a mouse event
var sprite:Sprite = new Sprite();
sprite.addEventListener(MouseEvent.CLICK, onClick);
import flash.utils.Timer;
import flash.events.TimerEvent;
// this creates a timer listening to a timer event
var timer:Timer = new Timer(1000);
timer.addEventListener(TimerEvent.TIMER , onTimer);
timer.start();

[/code]

event.target is the object associated with the listening function, or its scope:

[code]

function onClick(event:MouseEvent):void {
trace(event.target); // Sprite
}
function onTimer(event:TimerEvent):void {
trace(event.target); // Timer
}

[/code]

The listener object, also known as the event handler, is called when the event happens (the onClick or onTimer functions in the preceding example). It takes a single event instance as a parameter.

The dispatcher, the mechanism that monitors the event, saves a reference of the listener to call back when an event occurs, and its target, in a queue.

You must use removeEventListener to remove these references from memory. Removing the target will not do it:

[code]

sprite.removeEventListener(MouseEvent.CLICK, onClick);
timer.removeEventListener(TimerEvent.TIMER , onTimer);

[/code]

Event Propagation

For display objects, an event starts from the root parent, travels down the tree to the object that is registered, and travels back up.

Event propagation has three phases. The capturing phase occurs when the propagated event, after being received by the system, starts its search from the root parent. The targeting phase occurs when the event reaches the object registered for it. The bubbling phase is the event following the path in reverse.

Note that the event only needs to traverse until it finds the object that is registered. Regardless, you can see how keeping your displayList shallow would have a significant impact on performance.

If you want to stop the event during its journey, you can use the following methods. The latter method prevents other remaining event listeners from executing. This is a good technique for optimizing your mobile application:

[code]

function eventVisiting(event:MouseEvent):void {
if (event.target) {
event.stopPropagation();
// OR
event.stopImmediatePropagation();
}
}

[/code]

To prevent objects from being targeted, you can use:

[code]

someObject.mouseEnabled = false;
someParent.mouseChildren = false;

[/code]

Events for objects that are not on the displayList flow directly to the target.

One Listener for Many Children

Let’s demonstrate the benefit of the event model. Imagine that you have many objects that need to transform in response to a mouse event. In this example, their scale changes upon a click.

Create the objects:

[code]

import flash.display.Sprite;
var sw:int = stage.stageWidth;
var sh:int = stage.stageHeight;
for (var i:int = 0; i < 20; i++) {
var temp:Sprite = createSprite();
}
function createSprite():Sprite {
var temp:Sprite = new Sprite();
var g:Graphics = temp.graphics;
g.beginFill(0x3399FF, 1);
g.drawRect(0, 0, 100, 75);
g.endFill();
temp.x = Math.random()*sw;
temp.y = Math.random()*sh;
}

[/code]

Put them in a container instead of adding them to the stage:

[code]

var container:Sprite = new Sprite;
addChild(container);
for (var i:int = 0; i < 20; i++) {
var temp:Sprite = createSprite();
addChild(temp);
}

[/code]

Add a single listener to the container instead of creating a listener for each object:

[code]

import flash.events.MouseEvent;
var objectClicked:Sprite;
container.addEventListener(MouseEvent.CLICK, onObjectClick, false);
function onObjectClick(event:MouseEvent):void {
event.stopPropagation();
objectClicked = event.target as Sprite;
objectClicked.scaleX = objectClicked.scaleY = Math.random();
}

[/code]

Generic and Custom Events

In some situations, you may need to send an event that is not built in. In such cases, make your class a subclass of the EventDispatcher or create a member variable as an EventDispatcher.

In this example, we load an external opening animation. When it reaches its last frame, we are notified so that we can unload it and continue with the application.

Add an EventListener and use dispatchEvent to trigger the event:

[code]

package {
public class myClass extends EventDispatcher {
public function myClass() {
loadOpening();
}
function loadOpening():void {
// load a swf with a timeline animation
background.addEventListener(“OpeningDone”, removeBackground);
}
function removeBackground(event:Event):void {
removeChild(background);
}
}
}

[/code]

The code on the last frame of the opening .swf is:

[code]

stop();
dispatchEvent(new Event(“OpeningDone”));

[/code]

In the current event model, the listener function only receives one parameter: the event. If you want to pass additional parameters, you need to write your own custom class that inherits from the Event class. In the following example, the CustomEvent class has an event of type CONNECTED. It passes a parameter called parameters of type Object:

[code]

package {
import flash.events.Event;
final public class CustomEvent extends Event {
public static const CONNECTED:String = “connected”;
public var parameters:Object;
public function CustomEvent(type:String, parameters:Object = null) {
// type, bubbles, cancelable
super(type, true, true);
this.parameters = parameters;
}
// this is needed if the event is re-dispatched
public override function clone():Event {
return new CustomEvent(this.type, this.parameters);
}
}
}
addEventListener(CustomEvent.CONNECTED, onConnected);
dispatchEvent(new CustomEvent(CustomEvent.CONNECTED, {name:”somebody”});

[/code]

 

Memory

Let’s say it one more time: the memory on mobile devices is limited. If you exceed it, Android will terminate your application.

Creating Objects

Choose the appropriate object. For instance, use a Sprite instead of a MovieClip if you don’t need multiple frames. Use a Vector, which is more efficient than an Array, to store objects. Call the getSize() function to determine the memory footprint for a particular data type.

Allocating new blocks of memory is costly. Create your objects at an idle time, ideally when your application first initializes, and reuse them throughout your application.

Object pooling creates a pool to store objects and re-use them over time. In this example, we pop an object, or create one if none exists, and then push it back when done until the next use:

[code]

import flash.display.Sprite;
var pool:Vector.<Sprite>();
// get a new Sprite
var sprite:Sprite = popSprite();
sprite.init(); // set properties as needed
addChild(sprite);
function popSprite():Sprite {
var sprite:Sprite;
if (pool.length > 0) {
sprite = pool.pop(); } else {
sprite = new Sprite();
}
return sprite;
}

[/code]

When the object is no longer needed, return it:

[code]

removeChild(sprite);
pushSprite(sprite);
function pushSprite(sprite:Sprite):void {
pool.push(sprite);
}

[/code]

This approach is organic. The application creates objects as needed until it reaches a sufficient quantity to recycle them. Another method is to create all the objects initially, up to a maximum amount.

Removing Objects

Memory is dynamic. As objects are deleted, memory is released via a process called garbage collection.

Only primitive types, such as String, Number, and Boolean, are used directly. All other types are used using a reference, which must be removed to clear them from memory. This includes removing a child from a displayList, splicing an Array array or a Vector, stopping and deleting a Timer, removing an EventListener, or nulling a reference from another object.

Once this is all done, set the object to null to help the garbage collector work more quickly.

Use the disposeXML method for an XML object. It traverses the tree and sets all pointers between parents and children to null to make the object available for immediate garbage collection:

[code]

import flash.system.System;
function onXMLLoaded(event:Event):void {
var xml:XML = event.target.data;
// sudo code to parse and store data
var dataStorage = parseXML(XML);
System.disposeXML(xml);
xml = null;
}

[/code]

Use the dispose method for a BitmapData object:

[code]

var bitmapData:BitmapData = new BitmapData(480, 800);
bitmapData.dispose();
bitmapData = null;

[/code]

One of the principal reasons for memory leaks is lingering event listeners. A common recommendation today is to set weak event listeners as shown in the code below. Weak references are not counted by the garbage collector as references, a topic we will cover next:

[code]

var sprite:Sprite = new Sprite();
// strongly referenced listeners
sprite.addEventListener(MouseEvent.CLICK, onClick);
// weakly referenced listeners
// eventName, listener, capturePhase, priority, useWeakReference
sprite.addEventListener(MouseEvent.CLICK, onClick, false, 1, true);

[/code]

Use this approach with caution. Too many developers tend to rely on it instead of removing listeners in code. As a responsible coder, you should be diligent about doing the proper housekeeping.

If your code is large, or difficult to maintain, you can create an array to store listeners or a mechanic to automate their removal.

Create a destroy method to use in your classes to remove listeners and objects. To enforce the practice, use an IDestroy interface in which all classes need to have the destroy method but can implement it according to their individual needs:

[code]

// interface
public interface IDestroy {
function destroy():void;
}
// class A
public class ClassA implements IDestroy {
public function ClassA() {
}
public function destroy():void {
// remove listeners
// clear display list
while (numChildren > 0) {
removeChildAt(0);
}
}
}
// class B
public class ClassB implements IDestroy {
public function ClassB() {
}
public function destroy(event:):void {
// empty arrays
// set variables to null
// call garbage collection
System.gc();
}
}

[/code]

If one of your classes implements the interface but erroneously does not have a destroy method, you will see the following error:

[code]

Interface method destroy in namespace com:IDestroy not implemented by class
com.ClassC

[/code]

Garbage Collection

Garbage collection is an automatic mechanism that deallocates the memory used by objects when they are no longer referenced in the application. It is important because it releases memory for future use, and particularly so for mobile devices with limited memory.

The garbage collector is not predictable, but you can sometimes monitor it by watching your memory consumption using the System class. totalMemory refers to the portion of memory allocated to your code. privateMemory is the entire memory consumption of the application. All functions return bytes; divide by 1,024 to obtain kilobytes:

[code]

import flash.system.System;
System.privateMemory/1024;
System.totalMemory/1024;
System.totalMemoryNumber/1024 // returned as NUMBER for greater precision
System.freeMemory/1024; // memory unused

[/code]

You can call the garbage collector directly in AIR, but it requires significant memory and can result in a noticeable slowdown in the application. For this reason, reuse objects or clean them up quickly and frequently as described earlier:

[code]

System.gc();

[/code]

The AIR runtime uses two different methods for best results.

Reference count keeps track of the object and marks it for deletion when it has a value of zero if it has no reference:

[code]

var a:Object = new Object(); count is 1 for a;
var b:Object = a; 2 for b;
a = null; 1 for a;
b = null; 0 for b;

[/code]

This method fails when two objects reference each another:

[code]

var a:Object = new Object(); is 1 for a;
var b:Object = new Object(); is 1 for b;
a.b = b; 2 for b;
b.a = a; 2 for a;
a = null; 1 for a;
b = null; 1 for b;

[/code]

Mark sweeping handles this issue in a more accurate way but takes longer and does not happen as often. It traverses the entire application tree and marks an object that has a reference, marks its children, and so on, recursively. Objects that are not marked can be assumed to be eligible for collection.

In AIR for Android, the garbage collector is more iterative and objects are collected more frequently because memory is in short supply. For this reason, be attentive to making sensors and objects with listener member variables, not local variables.

Breadcrumb Navigation

You can use different approaches for the user to navigate between screens.

AIR does not have access to the Options menu triggered by pressing the Menu key. If you want to create a native-like look and feel, you can design a similar-looking menu in your application for the application’s key views. The advantage of this approach is that the navigation menu does not take real estate away from your application. It is made visible when pressing the Menu key and collapses after a selection is made.

Another approach is to use breadcrumb navigation. This is a common and intuitive type of interface. It registers the user’s selection as he goes from screen to screen and stores his history. A persistent button gives the user the option to go back. Figure 16-5 shows a breadcrumb navigation flow and view stack.

We only need to make a few changes to add this functionality.

Let’s add the functionality to the BaseView class to include a back button to the dis playList and a callback function to dispatch a goBack event when the back button is pressed. In the onShow function, the onBack method of the super class is called last so that the back button is always at the top level of the display list.

This is the BaseView class:

[code]

import flash.events.Event;
protected function onBack():void {
var backButton:BackButton = new BackButton();
backButton.addEventListener(MouseEvent.CLICK, onGoBack);
addChild(backButton);
}
private function onGoBack(event:MouseEvent):void {
event.stopPropagation();
event.currentTarget.removeEventListener(MouseEvent.CLICK, onGoBack);
removeChild(event.currentTarget as MovieClip);
dispatchEvent(new Event(“goBack”));
}

[/code]

This is the SessionsView class modified to call the onBack method of the BaseView class:

[code]

public function onShow():void {
container = new Sprite();
// request list of sessions if not acquired yet
if (list == null) {
list = Sessions.sessionList;
}
// display sessions
showSessions();
// add back button
super.onBack();
}

[/code]

Figure 16-5. Breadcrumb navigation flow and view stack
Figure 16-5. Breadcrumb navigation flow and view stack

The ViewManager class also needs to be edited. The visited views are stored in a Vec tor. In the setCurrentView function, the new view is pushed to the stack:

[code]

private currentView:BaseView;
private var viewStack:Vector.<BaseView>;
private function goTo(event:ClickEvent):void {
viewStack.push(event.data);
setCurrentView(event.data);
}
private function setCurrentView(object:Object):void {
if (currentView) {
currentView.removeEventListener(ViewEvent.CLICK, goTo);
IView(currentView).onHide();
currentView.removeEventListener(“goBack”, goBack);
timeline.removeChild(currentView);
}
currentView = viewList[object.view];
if (object.id != undefined) {
currentView.setID(object.id);
}
timeline.addChild(currentView);
currentView.addEventListener(ClickEvent.NAV_EVENT, goTo, false, 0, true);
currentView.addEventListener(“goBack”, goBack, false, 0, true);
IView(currentView).onShow();
}

[/code]

When the back button is pressed, the current view is removed from the stack first. The setCurrentView method is then called and the last view stored is passed as an argument:

[code]

private function goBack(e:Event):void {
viewStack.pop();
setCurrentView(viewStack[viewStack.length – 1]);
}

[/code]

You can now easily add views to your application. If you want to add a transition between views, the simplest solution is to take a screen grab of the current view and tween it on a top layer while you are revealing the destination view.

Once this navigation functionality is in place, you can now focus on other aspects, perhaps more abstract or challenging, of your project.

Navigation

Unlike desktop applications, mobile applications only display one screen, or view, at a time. Each screen should be designed for a single task with clear and focused content. Intuitive single-click navigation to move to previous and next steps is key.

We will explore two approaches in this chapter. The first is a navigation system I wrote for my mobile applications using pure ActionScript. The second approach takes advantage of the ViewNavigator in the Flex Hero framework for mobile applications. It includes a few additional features such as transitions and a global ActionBar.

Navigation

You want your audience to be able to move forward between screens without re-creating the same steps over and over. You may need to provide the logic to navigate back to previous screens.

Google discourages the use of the physical back button for anything but going back to the previous application. However, the functionality exists, and some applications, as well as Flash Builder Mobile, use it to go through the stack of views. In my example, I create a back button within the application instead.

ViewManager

I originally developed this code for a conference scheduler application.

Attendees can carry the scheduler in their pocket and organize and save their own schedules. The application’s business logic is fairly simple, but all the screens are interconnected and can be accessed from several different points. For instance, from the session view, the user can access the session’s speaker(s), and from the speaker view, the user can access one of the sessions he is speaking at.

Creating views

The ViewManager creates the different views and manages them during the life of the application.

The document class creates an instance of the ViewManager. It calls its init function and passes a reference of the timeline:

[code]

import view.ViewManager;
// member variable
private var viewManager:ViewManager;
viewManager = new ViewManager();
viewManager.init(this);

[/code]

The ViewManager class stores a reference to the timeline to add and remove views from the display list:

[code]

private var timeline:MovieClip;
public function init(timeline:MovieClip):void {
this.timeline = timeline;
}

[/code]

The ViewManager creates an instance of each view and stores them in a viewList object. The following code assumes the MenuView, SessionsView, and SessionView classes exist.

The initialization process for each view, creation of the view’s member variables and references to other objects, only needs to happen once.

Note that the views are of data type BaseView.

[code]

private var currentView:BaseView;
private viewList:Object = {};
public function init(timeline:MovieClip):void {
this.timeline = timeline;
createView(“menu”, new MenuView());
createView(“sessions”, new SessionsView());
createView(“session”, new SessionView());
}
private function createView(name:String, instance:BaseView):void {
viewList[name] = instance;
}

[/code]

The initial view display

When the application first starts, the document class loads an XML document that contains all the data regarding the conference, such as the list of sessions and speakers. While this is taking place, the ViewManager displays an introductory view without any interactivity. Let’s modify the init method to add this functionality. The setCurrent View method will be discussed in the next paragraph:

[code]

public function init(timeline:MovieClip):void {
this.timeline = timeline;
createView(“intro”, new IntroView());
createView(“menu”, new MenuView());
createView(“sessions”, new SessionsView());
createView(“session”, new SessionView());
setCurrentView({view:”intro”});
}

[/code]

The current view display

Once the data is loaded, parsed, and stored in the model part of the application, the document class calls the onDataReady method on the ViewManager:

[code]

// set up application, model and get data from external xml
viewManager.onDataReady();

[/code]

In turn, the ViewManager defines the new view by calling the setCurrentView method and passes an object with the property view to define the view to display:

[code]

public function onDataReady():void {
setCurrentView({view:”menu”});
}

[/code]

The setCurrentView method removes the previous view if there is one. It then stores the view in the currentView variable and adds it to the display list. Two methods, onHide and onShow, are called via the IView interface, discussed next. Each view uses the methods to clear or add from the display list and destroy objects.

The method also registers the view for a custom ClickEvent.NAV_EVENT with the setCur rentView method of the ViewManager as the callback function.

[code]

import view.ClickEvent;
private var currentView:BaseView;
private function setCurrentView(object:Object):void {
// remove current view
if (currentView) {
currentView.removeEventListener(ClickEvent.NAV_EVENT, goTo);
IView(currentView).onHide();
timeline.removeChild(currentView);
currentView = null;
}
// add new view
currentView = viewList[object.view];
if (object.id != undefined) {
currentView.setID(object.id);
}
currentView.addEventListener(ClickEvent.NAV_EVENT, goTo, false, 0, true);
IView(currentView).onShow();
timeline.addChild(currentView);
}
// pass event data object
private function goTo(event:ClickEvent):void {
setCurrentView(event.data);
}

[/code]

The IView interface

It is imperative that all views have the two methods, onHide and onShow, so we use an IView interface. Each view also needs a method—here it is clickAway—to navigate to the next view. In our application, this always occurs upon user interaction. We will therefore use a MouseEvent:

[code]

package view {
import flash.events.MouseEvent;
public interface IView
{
function onShow():void
function onHide():void
function clickAway(event:MouseEvent):void
}
}

[/code]

Creating a custom event

A custom event is used to pass the destination view and additional data, if needed, from the current view. For example, if the user is looking at the screen displaying all the conference sessions and clicks on a specific session, we use the event object to pass the session ID to the Session view, as illustrated in Figure 16-1:

[code]

{view:”session”, id:5}

[/code]

Figure 16-1. The mechanism to send destination and data from the current view to the destination view via the ViewManager
Figure 16-1. The mechanism to send destination and data from the current view to the destination view via the ViewManager

The custom class is as follows. Its data property is an object, so we can add additional parameters as needed:

[code]

import flash.events.Event;
import events.ClickEvent;
final public calls ClickEvent extends Event {
public static const NAV_EVENT:String = “NavEvent”;
public var data:Object;
public function ClickEvent(type:String, data:Object = null) {
super(type, true, true);
this.data = data;
}
public override function clone():Event {
return new ClickEvent(this.type, this.data);
}
}

[/code]

Individual Views

Let’s examine some of the views.

Inheritance

Some functionality is identical for all views. For this reason, they all inherit from the same super class, called BaseView. Here we declare two member variables, id and con tainer, and a function, setID. It is a simple class initially. We will develop it further in this chapter to add a button and interactivity to navigate in reverse:

[code]

package view {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
public class BaseView extends Sprite
{
protected var id:int;
protected var container:Sprite;
protected function SeID(id:int):void {
this.id = id;
}
}
}

[/code]

The following code is for the MenuView class. It adds three buttons—for sessions, speakers, and a custom schedule—and their listeners on onShow. It clears listeners and empties the display list on onHide. Figure 16-2 shows the Menu view of the AIR scheduler application for the 2010 GoogleIO conference:

[code]

package view {
import flash.events.MouseEvent;
import view.ClickEvent;
final public class MenuView extends BaseView implements IView() {
public function MenuView(){}
public function onShow():void {
var sessions:sessionsBut = new sessionsBut();
sessions.view = “sessions”;
sessions.addEventListener(MouseEvent.CLICK, onClickAway);
var speakers:speakersBut = new speakersBut();
speakers.view = “speakers”;
speakers.addEventListener(MouseEvent.CLICK, onClickAway);
var schedule:scheduleBut = new scheduleBut();
schedule.view = “schedule”;
schedule.addEventListener(MouseEvent.CLICK, onClickAway);
addChild(sessions);
addChild(speakers);
addChild(schedule);
}
public function onHide():void {

while(numChildren > 0) {
getChildAt(0).
removeEventListener(MouseEvent.CLICK, onClickAway);
removeChildAt(0);
}
}
public function onClickAway(event:MouseEvent):void {
dispatchEvent(new ClickEvent(ClickEvent.NAV_EVENT,
{view:event.currentTarget.view});
}
}
}

[/code]

Figure 16-2. The Menu view of the AIR scheduler application for the 2010 GoogleIO conference
Figure 16-2. The Menu view of the AIR scheduler application for the 2010 GoogleIO conference

The next example shows the SessionsView code. Note that the code is minimal to keep the focus on navigation. For instance, the scrolling mechanism was left out.

As mentioned before, the initialization process for each view only needs to happen once for the life of the application. Here the SessionsView class acquires the data for the sessions just once if its list variable is null.

The sessions data is stored in a Vector object called sessionList of the static class Sessions (not covered here) and is populated with objects of data type Session. It is already sorted and organized by day and time:

[code]

package view {
import events.ClickEvent;
import flash.events.MouseEvent;
import model.Sessions; // static class holds list of all Sessions
import model.Session; // class holds individual session data
final public class SessionsView extends BaseView implements IView() {
private var list:Vector.<Session>;
public function SessionsView (){}
]
public function onShow():void {
container = new Sprite();
// request list of sessions if not acquired yet
if (list == null) {
list = Sessions.sessionList;
}
// display sessions
showSessions();
}
}

[/code]

We traverse the list of sessions and display all the sessions with the hardcoded date of 19. Again, this is to keep this example simple. The conference took place over two days and would, in a full example, require a UI to choose between one of the two dates:

[code]

private showSessions():void {
var timeKeeper:String = “0”;
var bounds:int = list.length;
var ypos:int = 50;
for (var i:int = 0; i < bounds; i++) {
var session:Session = list[i];
// display a blue time indicator if it is a new time
if (session.time > timeKeeper) {
timeKeeper = session.time;
ypos += 15;
// TimeBar is a movieclip
var bar:TimeBar = new TimeBar();
bar.y = ypos;
bar.timeInfo.text = timeKeeper;
container.addChild(bar);
ypos += 60;
}
// load the individual session
// it returns its height to position the next element below it
var newPos = loadSession(session, ypos);
ypos =+ (newPos + 10);
}
addChild(container);
}
private loadSession(session:Session, ypos:int):int {
// SessionSmall is a movieclip
var mc:SessionSmall = new SessionSmall();
mc.y = ypos;
mc.id = session.id;
mc.what.autoSize = TextFieldAutoSize.LEFT;
mc.what.text = “+ ” + session.title;
mc.back.height = mc.what.height;
mc.addEventListener(MouseEvent.CLICK, clickAway, false, 0, true);
container.addChild(mc);
// return the session movie clip height
return mc.what.height;
}

[/code]

When the user chooses a session, its ID is passed along with the destination view:

[code]

public function clickAway(event:MouseEvent):void {
dispatchEvent(new ClickEvent(ClickEvent.NAV_EVENT,
{view:”session”, id:event.currentTarget.id}));
}

[/code]

In the onHide method, all the children of the Sprite container are removed as well as their listeners if they have one. Then the container itself is removed. Figure 16-3 shows the sessions broken down by day and time:

[code]

public function onHide():void {
while (container.numChildren > 0) {
var child:MovieClip = container.getChildAt(0) as MovieClip;
if (child.id != null) {
child.removeEventListener(MouseEvent.CLICK, clickAway);
}
container.removeChild(child);
}
removeChild(container);
container = null;
}

[/code]

Here is the SessionView code. The method displays all the data related to a session. This includes the session title, a description, the speakers involved, and the room, category, and rank:

[code]

package view {
import events.ClickEvent;
import flash.events.MouseEvent;
import model.Sessions;
import model.Speakers; // static class that holds Speakers data
final public class SessionView extends BaseView implements IView() {
public function SessionView(){}
public function onShow():void {
// search Sessions by id
var data:Object = Sessions.getItemByID(id);
container = new Sprite();
addChild(container);
// display title and description
// SessionMovie is a movieclip
var session:SessionMovie = new SessionMovie();
session.title.autoSize = TextFieldAutoSize.LEFT;
session.title.text = data.title;
session.body.text = data.description;
container.addChild(session);
// display list of speakers
for (var i:int; i < data.speakers.length; i++) {
var bio:Bio = new Bio();
bio.id = data.speakers[i];
// search list of speakers by id
var bioData:Object = Speakers.getItemByID(bio.id);
bio.speaker.text = bioData.first + ” ” + bioData.last;
bio.addEventListener(MouseEvent.CLICK,
clickAway, false, 0, true);
}
// display category, level, rank and room number
// Border is a movieClip
var border:Border = new Border();
// categories is a movieclip with frame labels matching category
border.categories.gotoAndStop(data.tag);
// rank is a movieclip with a text field
border.rank.text = String(data.type);
// room is a movieclip with a text field
border.room.text = String(data.room);
container.addChild(border);
}

[/code]

Figure 16-3. The sessions broken down by day and time
Figure 16-3. The sessions broken down by day and time

Clicking on one of the speakers takes the user to a new speaker destination view defined by an ID:

[code]

public function clickAway(event:MouseEvent):void {
dispatchEvent(new ClickEvent(ClickEvent.NAV_EVENT,
{view:”speaker”, id:event.currentTarget.id}));
}
}

[/code]

In the onHide method, all the children of the Sprite container are removed as well as their listeners if they have one. Then the container itself is removed:

[code]

public function onHide():void {
while(container.numChildren > 0) {
var child:container.getChildAt(0);
if (child.id != null) {
child removeEventListener(MouseEvent.CLICK, clickAway);
}
container.removeChild(child);
}
removeChild(container);
container = null;
}

[/code]

Figure 16-4 shows a subset of information for a session.

Figure 16-4. A subset of information for a session, including the speaker(s) name, category, level of expertise, and room number
Figure 16-4. A subset of information for a session, including the speaker(s) name, category, level of expertise, and room number

P2P Over a Local Network

If your local network supports broadcasting, you can create peer-to-peer direct routing. All the clients need to be on the same subnet, but you do not need to manage them. Verify that your devices have WiFi enabled and are using the same network.

The code to create a peer-to-peer application with RTMFP is quite simple but introduces new concepts. Let’s go over all the steps one at a time.

The connection is established using the flash.net.NetConnection class. Set a listener to receive a NetStatusEvent event. Create the connection by calling the connect function and passing rtmfp as an argument:

[code]

import flash.net.NetConnection;
import flash.events.NetStatusEvent;
var connection:NetConnection = new NetConnection();
connection.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
connection.connect(“rtmfp:”);

[/code]

Wait for the connection to be established. Then several objects need to be created:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetConnection.Connect.Success” :
trace(“I am connected”);
// object creation can now happen
break;
}
}

[/code]

NetGroup is the group of peers. Its capabilities are defined in the GroupSpecifier. The IPMulticastAddress property stores the IPv4 multicast address. It needs to be in the range 224.0.0.0 through 239.255.255.25. The UDP port should be higher than 1024. A group name is passed in its constructor. Try to make it unique. The IPMulticast MemberUpdatesEnabled property must be set to true for clients to receive updates from other clients on a LAN. The postingEnabled property allows clients to send messages to the group:

[code]

import flash.net.GroupSpecifier;
var groupName:String = “com.veronique.simple/”;
var IPMulticastAddress:String = “230.0.0.1:3000″;
var groupSpec:GroupSpecifier = new GroupSpecifier(groupName);
groupSpec.addIPMulticastAddress(IPMulticastAddress);
groupSpec.ipMulticastMemberUpdatesEnabled = true;
groupSpec.postingEnabled = true;

[/code]

Now create the NetGroup. Pass the connection and the GroupSpecifier in its construction. The latter is passed with an authorization property to define the communication allowed: groupspecWithAuthorizations to post and multicast, or groupspecWithout Authorizations to only receive messages. Note that this setting is only relevant if a posting password is set (as defined by your application):

[code]

import flash.net.NetGroup;
var netGroup = new NetGroup
(connection, groupSpec.groupspecWithAuthorizations());
netGroup.addEventListener(NetStatusEvent.NET_STATUS, onStatus);

[/code]

The group is composed of neighbors, you as well as others. Using the same Net StatusEvent event, check for its info.code. Wait to receive the NetGroup.Connect.Suc cess event before using the functionality of NetGroup to avoid getting an error.

When a user joins or leaves the group, the code is as follows:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case ” NetGroup.Connect.Success” :
trace(“I joined the group”);
break;
case “NetGroup.Connect.Rejected” :
case “NetGroup.Connect.Failed” :
trace(“I am not a member”);
break;
}
}

[/code]

Others in the group receive the following events. Note that if the group is large, only a subset of members is informed that a new peer has joined or left the group:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetGroup.Neighbor.Connect” :
trace(“neighbor has arrived”, neighborCount);
break;
case “NetGroup.Neighbor.Disconnect” :
trace(“neighbor has left”);
break;
}
}

[/code]

To send a message, use the NetGroup.post method. It takes an Object as an argument. Messages are serialized in AMF (binary format for serialized ActionScript objects), so a variation of data types can be used, such as Object, Number, Integer, and String types:

[code]

var message:Object = new Object();
message.type = “testing”;
message.body = {name:”Véronique”, greeting:”Bonjour”};
group.post(message);

[/code]

To receive messages, check for an info.code equal to a NetGroup.Posting.Notify event. The message is received as event.info.message. The message is not distributed to the sender:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetGroup.Posting.Notify” :
trace(event.info.message); // [Object]
trace(event.info.message.body.greeting); // Bonjour
break;
}
}

[/code]

Identical messages are not re-sent. To make each message unique, store the current time as a property of the object:

[code]

var now:Date = new Date();
message.time = now.getHours() + “_” + now.getMinutes() +
“_” + now.getSeconds();
group.post(message);

[/code]

If the message only goes in one direction and there will be no overlap between clients, you could use a counter that gets incremented with every new message:

[code]message.count = count++;[/code]

When disconnecting, it is important to remove all objects and their listeners:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetConnection.Connect.Rejected” :
case “Connect.AppShutdown” :
trace(“I am not connected”);
onDisconnect();
break;
}
}
function onDisconnect():void {
group = null;
netGroup.removeEventListener(NetStatusEvent.NET_STATUS, onStatus);
netGroup = null;
connection.removeEventListener(NetStatusEvent.NET_STATUS, onStatus);
connection = null;
}

[/code]

Color Exchange

Let’s create a simple example. The hueMe application starts with a shape of a random color. Each client can send a color value to the other client’s application. On the receiving end, the shape changes to the new color (see Figure 15-1).

Figure 15-1. The hueMe application
Figure 15-1. The hueMe application

Draw the initial colored sprite:

[code]

var sprite:Sprite = new Sprite();
var g:Graphics = sprite.graphics;
g.beginFill(Math.round(Math.random()*0xFFFFFF));
g.drawRect(20, 20, 200, 150);
g.endFill();

[/code]

Create the connection for the P2P communication:

[code]

var connection:NetConnection = new NetConnection();
connection.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
connection.connect(“rtmfp:”);

[/code]

Once the connection is established, create the group and check that the user has successfully connected to it:

[code]

function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetConnection.Connect.Success”) {
var groupSpec:GroupSpecifier = new GroupSpecifier(“colorGroup”);
groupSpec.addIPMulticastAddress(“225.0.0.1:4000”);
groupSepc.postingEnabled = true;
groupSepc.ipMulticastMemberUpdatesEnabled = true;
group = new NetGroup(connection,
groupSpec.groupspecWithAuthorizations());
group.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
} else if (event.info.code == “NetGroup.Connect.Success”) {
trace(“I am part of the group “);
}
}

[/code]

Send a random color value to the group when clicking the sprite:

[code]

g.addEventListener(MouseEvent.CLICK, hueYou);
function hueYou(event:MouseEvent):void {
var randomHue:int = Math.round(Math.random()*0xFFFFFF);
var object:Object = {type:”color”, hue:randomHue};
group.post(object);
}

[/code]

Finally, add the functionality to receive the value from other members of the group and color the sprite:

[code]

import flash.geom.ColorTransform;
function onStatus(event:NetStatusEvent):void {

if (event.info.code == “NetGroup.Posting.Notify”) {
if (event.info.message.type == “color”) {
applyColor(Number(event.info.message.hue));
}
}
}
function applyColor(hue:int):void {
var colorTransform:ColorTransform = new ColorTransform();
colorTransform.color = hue;
sprite.transform.colorTransform = colorTransform;
}

[/code]

Companion AIR Application

To make your application unidirectional, as in a remote control-style application, have one client sending messages and the other receiving messages. Only the networked clients registered for the NetGroup.Posting.Notify event receive data.

Mihai Corlan developed an Android remote control for a desktop MP3 player; read about it at http://corlan.org/2010/07/02/creating-multi-screen-apps-for-android-and -desktop-using-air/.

Tom Krcha created a remote controller to send accelerometer, speed, and brake information to a car racing game (see http://www.flashrealtime.com/game-remote-device-con troller/).

The Display List

The structure of your display list is fundamental in this process for three reasons: memory consumption, tree traversal, and node hierarchy.

Memory Consumption

Memory consumption is the trade-off for better performance in GPU rendering because every off-screen bitmap uses memory. At the same time, mobile development implies less GPU memory caching and RAM.

To get a sense of the memory allocated, you can use the following formula:

[code]

// 4 bytes are required to store a single 32 bit pixel
// width and height of the tile created
// anti-aliasing defaults to high or 4 in Android
4 * width * height * anti-aliasFactor
A 10 × 10 image represents 1600 bytes

[/code]

Be vigilant about saving memory in other areas.

Favor the DisplayObject types that need less memory. If the functionality is sufficient for your application, use a Shape or a Sprite instead of a MovieClip. To determine the size of an object, use the following:

[code]

import flash.sampler.*;
var shape:Shape = new Shape();
var sprite:Sprite = new Sprite();
var mc:MovieClip = new MovieClip();
trace(getSize(shape), getSize(sprite), getSize(mc));
// 224, 412 and 448 bytes respectively in the AIR runtime

[/code]

The process of creating and removing objects has an impact on performance. For display objects, use object pooling, a method whereby you create a defined number of objects up front and recycle them as needed. Instead of deleting them, make them invisible or remove them from the display list until you need to use them again.

You should give the same attention to other types of objects. If you need to remove objects, remove listeners and references so that they can be garbage-collected and free up precious memory.

Tree Structure

Keep your display list fairly shallow and narrow.

The renderer needs to traverse the display list and compute the rendering output for every vector-based object. Matrices on the same branch get concatenated. This is the expected management of nested objects: if a Sprite contains another Sprite, the child position is set in relation to its parent.

Node Relationship

This is the most important point for successful use of caching. Caching the wrong objects may result in confusingly slow performance.

The cacheAsBitmapMatrix property must be set on the moving object, not on its container. If you set it on the parent, you create an unnecessarily larger bitmap. Most importantly, if the container has other children that change, the bitmap needs to be redrawn and the caching benefit is lost.

Let’s use an example. The parent node, the black box shown in the following figures, has two children, a green circle and a red square. They are all vector graphics as indicated by the points.

In the first scenario (depicted in Figure 14-2), cacheAsBitmapMatrix is set on the parent node. The texture includes its children. A bitmap is created and used for any transformation, like the rotation in the figure, without having to perform expensive vector rasterization. This is a good caching practice:

[code]

var box:Sprite = new Sprite();
var square:Shape = new Shape();
var circle:Shape = new Shape();
// draw all three items using the drawing API
box.cacheAsBitmap = true;
box.cacheAsBitmapMatrix = new Matrix();
box.rotation = 15;

[/code]

Figure 14-2. Caching and transformation on the parent only
Figure 14-2. Caching and transformation on the parent only

In the second scenario (depicted in Figure 14-3), cacheAsBitmapMatrix is still on the parent node. Let’s add some interactivity to make the circle larger when clicked. This is a bad use of caching because the circle needs to be rerasterized along with its parent and sibling because they share the same texture:

[code]

// change datatype so the display object can receive a mouse event
var circle:Sprite = new Sprite();
// draw items using the drawing API
circle.addEventListener(MouseEvent.CLICK, bigMe);
function bigMe(event:MouseEvent):void {
var leaf:Sprite = event.currentTarget as Sprite;
leaf.scaleX += .20;
leaf.scaleY += .20;
}

[/code]

Figure 14-3. Caching on the parent, but transformation on the children
Figure 14-3. Caching on the parent, but transformation on the children

In the third scenario (depicted in Figure 14-4), cacheAsBitmapMatrix is set, not on the parent, but on the children. When the circle is rescaled, its bitmap copy can be used instead of rasterization. In fact, both children can be cached for future animation. This is a good use of caching:

[code]

// change datatype so they can receive mouse events
var square:Sprite = new Sprite();
var circle:Sprite = new Sprite();
// draw items using the drawing API
square.addEventListener(MouseEvent.CLICK, bigMe);
circle.addEventListener(MouseEvent.CLICK, bigMe);
var myMatrix:Matrix = new Matrix();
square.cacheAsBitmap = true;
square.cacheAsBitmapMatrix = myMatrix;
circle.cacheAsBitmap = true;
circle.cacheAsBitmapMatrix = myMatrix;
function bigMe(event:MouseEvent):void {
var leaf:Sprite = event.currentTarget as Sprite;
leaf.scaleX += .20;
leaf.scaleY += .20;
}

[/code]

Figure 14-4. Caching and transformation on each individual child
Figure 14-4. Caching and transformation on each individual child

The limitation with using GPU rendering occurs when a parent and its children need to have independent animations as demonstrated earlier. If you cannot break the parent- child structure, stay with vector rendering.

MovieClip with Multiple Frames

Neither cacheAsBitmap nor cacheAsBitmapMatrix works for a MovieClip with multiple frames. If you cache the art on the first frame, as the play head moves, the old bitmap is discarded and the new frame needs to be rasterized again. This is the case even if the animation is a rotation or a position change.

GPU rendering is not the technique for such situations. Instead, load your MovieClip without adding it to the display list. Traverse through its timeline and copy each frame to a bitmap using the BitmapData.draw method. Then display one frame at a time using the BitmapData.copyPixels method.

Interactivity

Setting cacheAsBitmapMatrix to true does not affect the object’s interactivity. It still functions as it would in the traditional rendering model both for events and for function calls.

Multiple Rendering Techniques

On Android devices, you could use traditional rendering along with cacheAsBitmap and/ or cacheAsBitmapMatrix. Another technique is to convert your vector assets as bitmaps, in which case no caching is needed. The technique you use may vary from one application to the next.

Remember that caching is meant to be a solution for demanding rendering. It is helpful for games and certain types of animations (not for traditional timeline animation). If there is no display list conflict, as described earlier, caching all assets makes sense. There is no need to use caching for screen-based applications with fairly static UIs.

At the time of this writing, there seems to be a bug using filters on a noncached object while the GPU mode is set in the application descriptor (as in the example below). It should be fixed in a later release:

[code]

var sprite:Sprite = new Sprite();
sprite.graphics.beginFill(0xFF6600, 1);
sprite.graphics.drawRect(0, 0, 200, 100);
sprite.graphics.endFill();
sprite.filters = [new DropShadowFilter(2, 45, 0x000000, 0.5, 6, 6, 1, 3)];
addChild(sprite);

[/code]

Maximum Texture Memory and Texture Size

The maximum texture size supported is 1,024×1,024 (it is 2,048×2,048 for iPhone and iPad). This dimension represents the size after transformation. The maximum memory is not part of the memory consumed by the application, and therefore is not accessible.

2.5D Objects

A 2.5D object is an object with an additional z property that allows for different types of transformation.

If an object has cacheAsBitmapMatrix on and a z property is added, the caching is lost. A 2.5D shape does not need cacheAsBitmapMatrix because it is always cached for motion, scaling, and rotation without any additional coding. But if its visibility is changed to false, it will no longer be cached.

How to Test the Efficiency of GPU Rendering

There are various ways to test application performance beyond the human eye and perception. Testing your frame rate is your best benchmark.

GPU Rendering on Android

 

The rendering mode used for Android is called GPU Vector. Both the creation of individual pixel buffers and scene compositing are done by the GPU. The GPU is particularly useful in this performance-constrained environment.

The GPU rendering technique only benefits vector art. It reduces the time needed to rasterize vector graphics, especially complex ones, to a bitmap. A bitmap is already a pixel buffer, also called a texture. There is no need to duplicate it. In fact, doing so would waste precious memory.

To perform GPU rendering in Android using Flash Professional, go to File→AIR Android settings. Select the General tab, and under Render Mode, select GPU.

In Flash Builder, set the renderMode node of the initialWindow node of your application descriptor to gpu:

[code]

<initialWindow>
<renderMode>gpu</renderMode>

[/code]

If the user has a device that does not perform hardware acceleration, or if it does not work reliably with its drivers, AIR ignores the setting. No message is dispatched.

The cacheAsBitmap Property

The cacheAsBitmap property was added in Flash 8 with the Surface Renderer. Its purpose is to optimize the animation of vector graphics along the x- and y-axes, a process called translation.

When the cacheAsBitmap property is set to true, the display object rendered as a bitmap is cached in memory for reuse, as shown in Figure 14-1.

Figure 14-1. Taking vector graphics and rendering them as one bitmap via the cacheAsBitmap property
Figure 14-1. Taking vector graphics and rendering them as one bitmap via the cacheAsBitmap property

Because vector graphics use subpixels and bitmaps don’t, objects snap to the full pixel and may look different from what you intended. To avoid this, apply the vector image on a full whole-number pixel.

You can set the cacheAsBitmap property in Flash Professional under Properties→Cache As Bitmap. You can only apply it to symbols. The art will then be converted to a bitmap at runtime, but it will not be modified in the IDE.

You can also set the property in code:

[code]

var myVector:Sprite = new Sprite();
myVector.cacheAsBitmap = true;

[/code]

An object is only cached when it has its visible property set to true. Additionally, if its visibility is turned off, its bitmap representation is discarded.

The following example puts 100 circles on the stage. They animate frantically. Try the code on your device. Click on the stage to toggle the cacheAsBitmap property to true or false and notice the difference in performance. Note that this example is intended to illustrate a point regarding the use of many display objects. If an element always looks the same, as in the example, the best approach would be to create it once and copy it:

[code]

import flash.display.Shape;
import flash.events.Event;
import flash.events.MouseEvent;
var container:Vector.<Shape>;
const MAX:int = 100;
var boundsX:int;
var boundsY:int;
var cacheIndicator:Shape;
var toggleCache:Boolean = true;
container = new Vector.<Shape>;
boundsX = stage.stageWidth;
boundsY = stage.stageHeight;
// to keep track of when cacheAsBitmap is on
cacheIndicator = new Shape();
cacheIndicator.graphics.beginFill(0x999999, 1);
cacheIndicator.graphics.drawRect(5, 5, 50, 50);
cacheIndicator.graphics.endFill();
addChild(cacheIndicator);

[/code]

Create the circles and store them:

[code]

for (var i:int = 0; i < MAX; i++) {
var temp:Shape = createCircle();
container[i] = temp;
addChild(temp);
}
// to toggle between cacheAsBitmap true or false
stage.addEventListener(MouseEvent.CLICK, toggle);
// animate all circles on EnterFrame
stage.addEventListener(Event.ENTER_FRAME, move);

[/code]

Create individual circles with cacheAsBitmap set to true. Also give them an alpha to make the renderer work harder for testing purposes:

[code]

function createCircle():Shape {
var shape:Shape = new Shape();
shape.graphics.beginFill(Math.random()*0xFFFFFF, Math.random()*1);
shape.graphics.drawCircle(0, 0, 50);
shape.graphics.endFill(); shape.x = Math.floor(Math.random()*boundsX);
shape.y = Math.floor(Math.random()*boundsY);
shape.cacheAsBitmap = toggleCache;
return shape;
}
// purposely didn’t optimize code to make it work harder
function move(event:Event):void {
for (var i:int = 0; i < MAX; i++) {
var mc:Shape = container[i];
mc.x += (Math.random()-Math.random())*25;
mc.y += (Math.random()-Math.random())*25;
if (mc.x < 0 || mc.x > boundsX) {
mc.x = boundsX/2;
}
if (mc.y < 0 || mc.y > boundsY) {
mc.y = boundsY/2;
}
}
}

[/code]

Turn cacheAsBitmap on and off to test performance on the device:

[code]

function toggle(event:MouseEvent):void {
toggleCache = !toggleCache;
cacheIndicator.visible = toggleCache;
for (var i:int = 0; i < MAX; i++) {
var mc:Shape = container[i];
mc.cacheAsBitmap = toggleCache;
}
}

[/code]

Toggling the cacheAsBitmap property in a live application is not recommended, but it is a good development and debugging technique.

If the display object is scaled, skewed, or rotated, its bitmap copy needs to be updated over and over. You would therefore lose all the benefits of caching.

The cacheAsBitmapMatrix Property

The cacheAsBitmapMatrix property is a new DisplayObject property. It must always be set, along with cacheAsBitmap, equal to true. There is no option to set it manually in the Flash IDE.

You need to create a Matrix and apply it to the object’s cacheAsBitmapMatrix property:

[code]

import flash.geom.Matrix;
myVector.cacheAsBitmap = true;
myVector.cacheAsBitmapMatrix = new Matrix();

[/code]

The added benefit of using cacheAsBitmapMatrix is that you can also change its alpha, scale it, skew it, and rotate it. And it will stay cached, so you can keep the cached element and make more use of it. You do lose some visual quality, but on devices with very high PPI, the quality loss is typically not noticeable.

Another important new feature is the ability to apply the matrix while the display object is not visible so that you have more control over monitoring the initial caching and its performance hit. Furthermore, once your object is cached, you can change its visibility property and it will stay cached. Be careful not to forget invisible objects on the stage:

[code]

myVector.cacheAsBitmap = true;
myVector.cacheAsBitmapMatrix = new Matrix();
myVector.visible = false;
// object is still cached

[/code]

Let’s try another example, but this time, with cacheAsBitmapMatrix, the shapes can rotate and scale without losing the performance benefit of caching. Again, try it on your device:

[code]

import flash.geom.Matrix;
var container:Vector.<MovieClip>;
var toggleCache:Boolean = true;
const MAX:int = 200;
var boundsX:int;
var boundsY:int;
var myMatrix:Matrix;
container = new Vector.<MovieClip>;
boundsX = stage.stageWidth;
boundsY = stage.stageHeight;

[/code]

Create the Matrix only once and apply it to all the MovieClips:

[code]

myMatrix = new Matrix();
for (var i:int = 0; i < MAX; i++) {
var temp:MovieClip = createCircle();
container[i] = temp;
addChild(temp);
}

[/code]

Animate all the MovieClips on EnterFrame:

[code]stage.addEventListener(Event.ENTER_FRAME, move);[/code]

Create a square-shaped MovieClip. Give it a random alpha, scale, and direction:

[code]

function createCircle():MovieClip {
var mc:MovieClip = new MovieClip();
mc.graphics.beginFill(Math.random()*0x09FFFF, Math.random()*1);
mc.graphics.drawRect(-30, -30, 60, 60);
mc.graphics.endFill();
mc.x = Math.random()*stage.stageWidth;
mc.y = Math.random()*stage.stageHeight;
var scale:Number = Math.random()*1;
mc.scaleX = mc.scaleY = scale;
mc.dir = 1;
// cache it for transformation
mc.cacheAsBitmap = toggleCache;
mc.cacheAsBitmapMatrix = myMatrix;
return mc;
}

[/code]

Scale and rotate individual MovieClips:

[code]

function move(event:Event):void {
for (var i:int = 0; i < MAX; i++) {
var mc:MovieClip = container[i];
mc.scaleX += 0.05*mc.dir;
mc.scaleY += 0.05*mc.dir;
mc.rotation += 5*mc.dir;
if (mc.scaleX < 0.05 || mc.scaleX > 1.0) {
mc.dir *= -1;
}
}
}

[/code]

You should notice a great improvement in tweening animation, even with the large number of objects.

In an effort to preserve memory, all the objects share a single Matrix instance. There is no need to create a unique matrix per object because, once assigned, the Matrix will not be modified.

In our example, the cacheAsBitmapMatrix property is applied to objects in a flat display list. Caching becomes a more meticulous exercise when dealing with the display list hierarchy. We will go over this in the next section.

Writing the Code

For this exercise, we will draw three clickable sprites: one to make a phone call, one to send a text message, and one to send an email. Figure 2-2 shows our application and three native applications launched upon interactivity.
our application with three buttons

An AIR application cannot call, send a text message, or send an email directly, but it can invoke the native applications dedicated to these tasks and pass arguments such as the phone number or email address.

The URI scheme is a way to include small data items inline. AIR passes the argument to the Android system according to the official tel, sms, and email URI schemes. If the argument contains invalid characters or spaces, the command will be ignored. Valid characters are digits, dots, and the plus sign (+). Android currently does not support multiple numbers or a body argument for a text message, but it does support multiple emails and a body argument for an email.

If more than one application has a custom URI, the choices are represented in a menu. This is the case with mailto, as demonstrated in Figure 2-2, with both native email and Gmail applications.

Note that a mouse event works on a multitouch device as though it were a single-touch device.

Using Flash Professional

If you are using Flash Professional, add the following code to the Document class named Main, created earlier:

package {
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.net.URLRequest;
import flash.net.navigateToURL;
public class Main extends Sprite {
public function Main() {
// create the three sprites and their listeners
var callButton:Sprite = createSprite(0xFF3300, 140, 150);
callButton.addEventListener(MouseEvent.CLICK, callMe);
addChild(callButton);
var textButton:Sprite = createSprite(0x0099FF, 140, 350);
textButton.addEventListener(MouseEvent.CLICK, textMe);
addChild(textButton);
var mailButton:Sprite = createSprite(0x00FF11, 140, 550);
mailButton.addEventListener(MouseEvent.CLICK, mailMe);
addChild(mailButton);
}
function createSprite(hue:int, xPos:int, yPos:int):Sprite {
var temp:Sprite = new Sprite();
temp.graphics.beginFill(hue, 1);
temp.graphics.drawRect(0, 0, 200, 100);
temp.graphics.endFill();
temp.x = xPos;
temp.y = yPos;
return temp;
}
function callMe(event:MouseEvent):void {
trace(“calling”);
navigateToURL(new URLRequest(‘tel:18005551212’));
}
function textMe(event:MouseEvent):void {
trace(“texting”);
navigateToURL(new URLRequest(‘sms:18005551212’));
}
function mailMe(event:MouseEvent):void {
trace(“emailing”);
navigateToURL(new URLRequest
(‘mailto:[email protected]?subject=Hello&body=World’));
}
}
}

Select Control→Test Movie→Test to compile the application. You should always run your code on the desktop before installing it on a device in case you have syntax errors.

Using Flash Builder

The code is the same.

Select the small black arrow to the right of the Run button, and then select Run Configurations. Under Mobile Application, select Main if it is not selected. The “Target platform” should be Google Android. Under “Launch method”, select “On desktop” and choose your device from the pull-down menu. Click Run to compile and create the mobile application. If you don’t see your device listed, you can add it manually.

For Windows users, if you see the name of your device in the list, it indicates that the necessary drivers are preinstalled. Otherwise, make sure to install them.