Source Code and Comments

Suppose you want to zoom and pan a MovieClip with static but otherwise complex vector content. From the point of view of performance - memory and frame rate - is it better to leave it in the vector form, draw a scaled up version into a Bitmap or use cacheAsBitmapMatrix method available in mobile AIR?

In this post we provide Compare.fla file which can be used to easily compare the three approaches on your devices by simply commenting in and out portions of code. The apps themselves are similar to our Zoom an Pan in AIR app available on the Android Market and described in our tutorial: Zoom In on a Point in an Image and Pan with Gestures - AIR Mobile Tutorial.

An important difference between the previous tutorial and the apps included here is that we show how to accomplish one-finger panning rather than two-finger panning.

Download

  • Download all source files corresponding to this app (Flash CS5 and CS5.5 formats): compare.zip

Working with the Source Files

The zip file contains Compare.fla and OnePointPan.fla - Flash CS5.5 files and CompareCS5.fla OnePointZoom.fla - Flash CS5 files. In Flash CS5.5, you simply open each of the fla files. Under AIR for Android Publish settings, in General select Portrait mode, under Deployment enter the location or create your p12 certificate and choose deployment settings that you want, under Permissions don't check anything. In addition, the package contains our useful InfoMobile AS3 class whose instance added as a child of the MainTimeline provides basic information about the frame rate and memory consumption of any app at runtime.

You can use CS5 versions of the files if you have AIR for Android Extension for Flash CS5 installed.

Note: Since the apps do not require any permissions when you publish apk files to your device, you will get a warning that no permissions were selected. Simply ignore the warning.

Code and Comments

Below is complete Timeline source code in Comapre.fla with comments within the code.

import flash.display.Bitmap;

import flash.display.BitmapData;

import flash.display.Sprite;

import flash.display.StageAlign;

import flash.display.StageScaleMode;

import flash.events.MouseEvent;

import flash.events.TransformGestureEvent;

import flash.ui.Multitouch;

import flash.ui.MultitouchInputMode;

import fl.motion.MatrixTransformer;

import flash.geom.Matrix;

import flash.geom.Point;

import flash.display.MovieClip;

import flash.geom.Rectangle;

 

stage.align = StageAlign.TOP_LEFT;

stage.scaleMode = StageScaleMode.NO_SCALE;

/*
We set Multitouch input mode to MultitouchInputMode.GESTURE, so gesture events are recognized. You can check if and what gesture events are supported on a given device using Multitouch.supportsGestureEvents Boolean property, and get the list of supported gestures from Multitouch.supportedGestures Vector.<String> property. We skip this check here.
*/

Multitouch.inputMode = MultitouchInputMode.GESTURE;

/*
We created on the stage a MovieClip, stored it in the Library and linked to AS3 under the name 'mcZoom'. Our clip, or its Bitmap snapshot, will be placed in a Sprite called 'container'.
*/

var container:Sprite=new Sprite();

this.addChild(container);

container.y=150;

/*
We create an instance of our custom performance monitor 'InfoMobile' to display the frame rate and memmory consumption, more precisely System.privateMemory, of our app at runtime.
*/

var info:InfoMobile=new InfoMobile();

this.addChild(info);

/*
We place the MovieClips created on the stage in front of 'container'.
*/

setChildIndex(mcPanel,numChildren-1);

setChildIndex(mcHelp,numChildren-1);

mcHelp.visible=false;

/*
Maximum zoom factor. The memory consumption increases with larger factors and the scaled up object becomes too large to use cacheAsBitmapMatrix method. The method works for objects not exceeding 1024 by 1024 pixels.
*/

var factor:int=2;

/*
mcZoom has dimensions 480 by 500 pixels. We store the dimensions of a scaled up object below.
*/

var bdWidth:Number=480*factor;

var bdHeight:Number=500*factor;

/*
A rectangle that will define the bounds for dragging.
*/

var rect:Rectangle;

/*
Within the function 'init' we add liteners to buttons.
*/

init();

 

function init():void {

mcPanel.btnExit.addEventListener(MouseEvent.CLICK, exitApp);

mcPanel.btnStart.addEventListener(MouseEvent.CLICK,startRestart);

mcPanel.btnHelp.addEventListener(MouseEvent.CLICK, showHelp);

}

/*
When the function 'startRestart' runs for the first time, we create our 'clip' - an instance of mcZoom - or a Bitmap image of it and add it to the Display List. To use BitmapData.draw to draw the scaled up version of 'clip' into a Bitmap, leave the code as is. To use cacheAsBitmapMatrix technique, uncomment the code between the two lines 'caching cacheAsBitmapMatrix part - beginning' and 'caching cacheAsBitmapMatrix part - end', and comment out the code between 'drawing into bitmap part - beginning' and 'drawing into bitmap part - end.' To add 'clip' as a MovieClip, comment out both portions mentioned above and uncomment the part 'keeping as a MovieClip part - beginning' and 'keeping as a MovieClip part - end'. (Note which portions are commented out initially.)
*/

function startRestart(e:MouseEvent):void {

if(container.numChildren==0){

var clip:MovieClip=new mcZoom();

var scaleMat:Matrix=new Matrix();

scaleMat.scale(factor,factor);

 

//////////////caching cacheAsBitmapMatrix part - beginning/////////////

/*

clip.cacheAsBitmapMatrix = scaleMat;

clip.cacheAsBitmap=true;

container.addChild(clip);

*/

//////////////caching cacheAsBitmapMatrix part - end/////////////

 

//////////////drawing into bitmap part - beginning/////////////

var bd:BitmapData=new BitmapData(bdWidth,bdHeight,false,0x000000);

bd.draw(clip,scaleMat);

var bitmap:Bitmap;

bitmap=new Bitmap(bd);

container.addChild(bitmap);

var ratio:Number;

ratio=Math.min(stage.stageHeight/bdHeight,stage.stageWidth/bdWidth);

ratio=Math.min(ratio,1);

bitmap.width = bdWidth * ratio;

bitmap.height = bdHeight * ratio;

//////////////drawing into bitmap part - end/////////////

 

//////////////keeping as a MovieClip part - beginning/////////////

/*

container.addChild(clip);

*/

//////////////keeping as a MovieClip part - end/////////////

 

rect=new Rectangle(-container.width+stage.stageWidth/2,

      -container.height+stage.stageHeight/2,container.width,container.height);

}

if(!container.hasEventListener("gestureZoom")){

container.addEventListener(TransformGestureEvent.GESTURE_ZOOM, onZoom);

}

if(!container.hasEventListener("mouseDown")){

container.addEventListener(MouseEvent.MOUSE_DOWN, onDown);

}

container.x=0;

container.y=150;

container.scaleX=1;

container.scaleY=1;

}

/*
Within the GESTURE_ZOOM handler, we make sure that 'container is not scaled more than by 'factor' or less than 0.8.
 
We use the MatrixTranformer class from the package fl.motion and its static method MatrixTransformer.matchInternalPointWithExternal to keep the point on which we are zooming in its original place in relation to the stage. That creates an impression of zooming about a point. We use TransformGestureEvent's properties localX, localY, stageX, stageY to remember the position of our zoom center relative to 'container' and to the stage so we can realign them after zooming using MatrixTransformer.
 
GESTURE_ZOOM event has phases. During GESTURE_ZOOM BEGIN phase, we remove the listener to MOUSE_DOWN to prevent zooming and panning at the sam time. We restore the listener in the END phase.
*/

function onZoom(event:TransformGestureEvent):void {

if(event.phase==GesturePhase.BEGIN){

container.removeEventListener(MouseEvent.MOUSE_DOWN, onDown);

container.stopDrag();

}

var locX:Number=event.localX;

var locY:Number=event.localY;

var stX:Number=event.stageX;

var stY:Number=event.stageY;

var prevScaleX:Number=container.scaleX;

var prevScaleY:Number=container.scaleY;

var mat:Matrix;

var externalPoint=new Point(stX,stY);

var internalPoint=new Point(locX,locY);

container.scaleX *= event.scaleX;

container.scaleY *= event.scaleY;

if(event.scaleX > 1 && container.scaleX > factor){

container.scaleX=prevScaleX;

container.scaleY=prevScaleY;

}

if(event.scaleY > 1 && container.scaleY > factor){

container.scaleX=prevScaleX;

container.scaleY=prevScaleY;

}

if(event.scaleX < 1 && container.scaleX < 0.8){

container.scaleX=prevScaleX;

container.scaleY=prevScaleY;

}

if(event.scaleY < 1 && container.scaleY < 0.8){

container.scaleX=prevScaleX;

container.scaleY=prevScaleY;

}

mat=container.transform.matrix.clone();

MatrixTransformer.matchInternalPointWithExternal(mat,internalPoint,externalPoint);

container.transform.matrix=mat;

if(event.phase==GesturePhase.END){

container.addEventListener(MouseEvent.MOUSE_DOWN, onDown);

}

}

/*
On MOUSE_DOWN event we define the rectangle that gives bounds for dragging so we don't drag the clip off screen. The dimensions of the rectangle depend on the current scale of the container. 'false' in startDrag stands for lockCenter=false.
*/

function onDown(e:MouseEvent):void {

rect=new Rectangle(-container.width+stage.stageWidth/2,

      -container.height+stage.stageHeight/2,container.width,container.height);

stage.addEventListener(MouseEvent.MOUSE_UP,onUp);

container.addEventListener(MouseEvent.ROLL_OUT,onOut);

container.startDrag(false,rect);

}

/*
We stop dragging on MOUSE_UP and ROLL_OUT.
*/

function onUp(e:MouseEvent):void {

stage.removeEventListener(MouseEvent.MOUSE_UP,onUp);

container.stopDrag();

}

 

function onOut(e:MouseEvent):void {

container.removeEventListener(MouseEvent.ROLL_OUT,onOut);

container.stopDrag();

}

/*
When the user taps on Exit button, the app quits.
*/

function exitApp(event:MouseEvent):void {

NativeApplication.nativeApplication.exit(0);

}

/*
btnHelp shows the MovieClip with the description of the app.
*/

function showHelp(e:MouseEvent):void {

mcHelp.visible=true;

container.visible=false;

info.visible=false;

mcHelp.addEventListener(MouseEvent.CLICK,hideHelp);

}

 

function hideHelp(e:MouseEvent):void {

mcHelp.visible=false;

container.visible=true;

info.visible=true;

mcHelp.removeEventListener(MouseEvent.CLICK,hideHelp);

}

Our Findings Regarding Performance

We tested the different versions of Compare.fla and OnePointPan.fla on two Android phones, Droid 2 and Droid Bionic, using our InfoMobile class. This is what we found.

  • The technique of drawing our scaled up 'clip' into a BitmapData object and turning it into a Bitmap worked very well. The frame rate held at 24 FPS with repeated zooming and panning. The memory cycled properly: lower for a scaled down image, slightly higher for for a zoomed image, overall, though, staying within constant ranges. The only complaint is that the quality of scaled down text was not perfect. We applied the same technique to a larger MovieClip and larger zoom factor in OnePointPan.fla with equally good results.
  • The technique of cacheAsBitmapMatrix kept memory within the same ranges as drawing into a Bitmap but FPS was erratic, dropping to as low as 14 FPS during zooming or panning. That was with the CPU rendering mode. With GPU rendering mode, the FPS help at 24 FPS. The memory went up but kept within fixed ranges. Text quality was good.
  • Keeping 'clip' as a vector object gave the worst results except for high quality of text at every scale. The FPS was dropping during zooming and panning. Worst yet, with zooming repeated many times, say 16 times (more than a user would probably do), the memory usage began steadily rising, higher and higher without cycling, or apparent bound. That effect was most disconcerning.

Please drop a note to barbara@flashandmath.com if you get different results on your device.

Note:   Occasionally, during zooming if your finger slides outside the screen too quickly, panning doesn't work well as the device apparently thinks your finger is still there outside. It is enough to place your two fingers on the object and zoom slightly to get back to normal behavior.

Note:   Our experiments indicate that on Android phones for TransformGestureEvent.GESTURE_ZOOM, at least the phones on which we checked, scaling in the X and Y directions are always the same:

event.scaleX=event.scaleY

Thus, the code within the onZoom function could be simplified.

You can find more detailed explanations of the

MatrixTransformer.matchInternalPointWithExternal(mat,internalPoint,externalPoint);

method in our tutorial: How To Zoom In on a Point in an Image and Pan in AS3 Flash.

This tutorial was written by Barbara Kaskosz of flashandmath.

Back to AIR for Android              Back to Flash and Math Home

We welcome your comments, suggestions, and contributions. Click the Contact Us link below and email one of us.

Adobe®, Flash®, ActionScript®, Flex® are registered trademarks of Adobe Systems Incorporated.