AS3 Scrolling List for Android and iOS devices

I created a very simple AS3 list that works with the both Android and iOS devices. The project files include a Flash Professional project created with Flash Builder 4. You will need Adobe AIR for Android or the packager for iPhone to create naive create native iOS (iPhone, iPad) or Android.

If you only want to build for Android, then I recommend you check out Adobe Flash Builder Burrito and the Flex Hero SDK (or SDK 4.5). There is already a list control in the Flex Hero SDK (or SDK 4.5) for Android devices. However for iOS devices, you will need a scrolling list that works with AS3 and CS5 for packaging.

The list I created is an AS3 list that works for multiple devices, touch scrolls, and uses custom item renderers that detect user interaction. Here is an example AS3 project with the list in action. Just use your mouse like you would your finger on a mobile device to scroll the list and select items.



AS Scrolling List (click to view)

The list is suited best for smaller sets of data because the list does not recycle list items. But for most mobile applications you don’t normally have that many items to scroll. Adobe also recommends that you not use the drawing API in Flash because of its memory consumption on mobile devices. It would be better to create a MovieClip or Sprite in Flash and use that as the background of your item renderer. However, int this project I used the drawing API to change the selection color of the list item throwing all caution to the wind.

TouchList

The TouchList class creates the list, adds items and handles touch events dispatched by the item renderers. You might notice that I didn’t use any actual TouchEvent listeners in the list. This is because a TouchEvent is essentially a MouseEvent and I couldn’t see any difference in using one over the other. The TouchList class has a built in delay to differentiate between a scrolling and touch action. Like the Android phone, you can’t select an item while scrolling and pressed items are deselected if you scroll while pressed.

TouchListItemRenderer

TouchListItemRenderer implements ITouchListItemRenderer and renders the display of the items in the list data. This renderer can be customized to show whatever type of data you want in the list. List items can also be variable height.

ITouchListItemRenderer

If you want to create an item renderer for the list, then it must implement the ITouchListItemRenderer interface. This interface gives the renderer basic functionality to interact with user selection and touch events used by the list.

ListItemEvent

The list item event is a custom custom ListItemEvent dispached when a list item renderer is pressed. The event contains the event payload and a instance of the item renderer selected.

Installation

Included in the GitHub repository is the working project files for that I created in Flash Builder 4 that handles adding the list to the stage, screen orientation on the device, stage resize, and other functions for an Android AIR application. To install, just checkout the project file and import it into Flash Builder. I have also included Android .apk file if you want to deploy it directly to your Android phone.

The AS3ScrollinList project is located on GitHub.

If you do use the list in a project, be sure to drop me a note or mention me in your will. This list is actually a combination of my efforts and those of others in the Flash community. So please share what you build as well. If you have improvements, just post them back to this post or feel free to fork the GitHub code.

Work Cited

-Mister

Simple AS3 Mouse Scrolling List

Here is a simple ActionScript 3 list that scrolls with selectable items done two ways. The list itself acts like a component added to the stage, it can be sized and placed anywhere on the stage. The first way scrolling is achieved is based on the movement of the mouse over a defined masked area.

The smooth movement of the list is accomplished with TweenLite:

ListScroll.swf

The second way to scroll the list was on an enter frame event. This method is not as smooth because TweenLite could not be produced to create the movement of the list:

ListScroll2.swf

That’s pretty much the extent of it. Below is the code for the list application and scrolling list.

List.as

package
{
    import flash.display.MovieClip;
    import flash.display.Shape;
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.text.TextField;

    [SWF( width = '320', height = '440', backgroundColor = '#000000', frameRate = '30')]
    public class ListScroll extends Sprite
    {
        private var list:MouseScrollList;

        public function ListScroll()
        {
            addEventListener(Event.ADDED_TO_STAGE, init);
        }

        private function init(e:Event):void
        {
            removeEventListener(Event.ADDED_TO_STAGE, init);
            list = new MouseScrollList(300, 400, false);
            list.y = 20;
            list.x = 5;
            this.addChild(list);
        }
    }
}

MouseScrollList.as

package
{
    import com.greensock.TweenLite;
    import com.greensock.easing.Quad;

    import flash.display.MovieClip;
    import flash.display.Shape;
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.geom.Point;
    import flash.text.TextField;

    public class MouseScrollList extends Sprite
    {
        private var background:Sprite;
        private var menuMask:Shape;
        private var listHitArea:Shape;
        private var list:Sprite;

        private var prevY:Number = 0;
        private var listHeight:Number;
        private var hitAreaHeight:Number;
        private var listY:Number;
        private var listX:Number;
        private var listInitY:Number;

        private var verticalPadding:Number = 5;
        private var itemHeight:Number = 35;
        private var children:Number = 50;

        private var componentWidth:Number;
        private var componentHeight:Number;

        private var scrollOnMouseMove:Boolean = false;  // uses mouse move instead of enter frame, but doesn't work as good as I thought
        private var scroll:Boolean = true; // on mouse down set to false to hold scroll position in list when item is clicked

        public function MouseScrollList(w:Number, h:Number, scrollOnMouseMove:Boolean = false)
        {
            this.componentWidth = w;
            this.componentHeight = h;
            this.scrollOnMouseMove = scrollOnMouseMove;

            addEventListener(Event.ADDED_TO_STAGE, init);
            addEventListener(Event.REMOVED, destroy);
        }

        private function init(e:Event):void
        {
            removeEventListener(Event.ADDED_TO_STAGE, init);

            if(scrollOnMouseMove){
                addEventListener(MouseEvent.MOUSE_MOVE, handleMouseMove);
            } else {
                addEventListener(Event.ENTER_FRAME, handleEnterFrame);
            }

            listHitArea = new Shape

            menuMask = new Shape();
            menuMask.graphics.clear();
            menuMask.graphics.beginFill(0xFFFFFF,.4);
            menuMask.graphics.drawRect(0, 0, componentWidth, componentHeight)
            menuMask.graphics.endFill();

            this.addChild(menuMask);

            list = new Sprite();
            list.mask = menuMask;

            this.addChild(list);

            for(var i:int = 0; i < children; i++) {
                var textField:TextField = new TextField();
                textField.text = String(i);
                textField.mouseEnabled = false;

                var item:Sprite = new Sprite();
                    item.graphics.clear();
                    item.graphics.beginFill(0xFF0000, 1);
                    item.graphics.drawRect(0, 0, componentWidth - verticalPadding*2, itemHeight)
                    item.graphics.endFill()
                    item.y = i*(verticalPadding + itemHeight);
                    item.x = verticalPadding;

                    item.addEventListener(MouseEvent.ROLL_OVER, handleRollOver);
                    item.addEventListener(MouseEvent.ROLL_OUT, handleRollOut);
                    item.addEventListener(MouseEvent.MOUSE_DOWN, handleMouseDown);
                    item.addEventListener(MouseEvent.MOUSE_UP, handleMouseUp);

                    item.addChild(textField)
                list.addChild(item);
            }

            listHitArea = new Shape();
            listHitArea.graphics.clear();
            listHitArea.graphics.beginFill(0xFFFFFF,0);
            listHitArea.graphics.drawRect(0, 0, componentWidth, componentHeight)
            listHitArea.graphics.endFill();

            this.addChild(listHitArea);

            var point:Point = new Point(listHitArea.x, listHitArea.y);
                point = this.globalToLocal(point);

            listX = point.x;
            listY = point.y;
            listInitY = list.y;
            hitAreaHeight = listHitArea.height;
            listHeight = children*(verticalPadding + itemHeight) - hitAreaHeight;
        }

        private function scrollMouseMove(y:Number):void
        {
            var percent:Number = y/hitAreaHeight;
            var newY:Number = -(Math.round(listHeight*percent));

            if(newY + itemHeight*3 > hitAreaHeight) {
                newY = hitAreaHeight;
            } else if (newY + itemHeight*3 > listInitY) {
                newY = listInitY;
            }

            TweenLite.to(list, 2, {y:newY, ease:Quad.easeOut});
        }

        private function scrollEnterFrame(y:Number):void
        {
            var distance:Number = Math.cos( ( -(y + listY)/hitAreaHeight)*Math.PI )*15;
            var currentY:Number = list.y;
            var newY:Number;

            if( (currentY + distance - verticalPadding) > listInitY + verticalPadding) return;

            newY = list.y + distance;

            var delta:Number = Math.abs(prevY - newY);

            if(delta < 1) {
                list.y = prevY;
                return;
            }

            if(newY >= listInitY) {
                newY = listInitY;
            } else if (Math.abs(newY) > listHeight){
                newY = -listHeight;
            }

            prevY = list.y;
            list.y = newY;
        }

        private function handleMouseMove(event:MouseEvent):void
        {
            var x:Number = this.mouseX + Math.abs(listX);
            var y:Number = this.mouseY + Math.abs(listY);

            if( menuMask.hitTestPoint( x, y )  && scroll) {
                scrollMouseMove(y - Math.abs(listY));
            }
        }

        private function handleEnterFrame(event:Event):void
        {
            var x:Number = this.mouseX + Math.abs(listX);
            var y:Number = this.mouseY + Math.abs(listY);

            if( listHitArea.hitTestPoint( x, y ) && scroll  ) {
                scrollEnterFrame(y);
            }
        }

        public function handleRollOver(e:MouseEvent):void
        {
            e.target.alpha = .5;
        }

        private function handleRollOut(e:MouseEvent):void
        {
            e.target.alpha = 1;
        }

        private function handleMouseDown(e:MouseEvent):void
        {
            e.target.alpha = .2;
            scroll = false;
        }

        private function handleMouseUp(e:MouseEvent):void
        {
            e.target.alpha = 1;
            scroll = true;
        }

        private function destroy(e:Event):void
        {
            removeEventListener(Event.REMOVED, destroy);

            var list:MovieClip = list as MovieClip;

            if(list.numChildren != 0) {
                var i:int = list.numChildren;
                while( i-- ){
                    var item:Sprite = list.getChildAt(i) as Sprite;
                    item.removeEventListener(MouseEvent.MOUSE_DOWN, handleMouseDown);
                    item.removeEventListener(MouseEvent.MOUSE_UP, handleMouseDown);
                    item.removeEventListener(MouseEvent.ROLL_OVER, handleRollOver);
                    item.removeEventListener(MouseEvent.ROLL_OUT, handleRollOut);
                    list.removeChildAt( i );
                }
            }

            removeEventListener(Event.ADDED_TO_STAGE, init);
            removeEventListener(Event.REMOVED, destroy);
            removeEventListener(Event.ENTER_FRAME, handleEnterFrame);
            removeEventListener(MouseEvent.MOUSE_MOVE, handleMouseMove);
        }
    }
}

You can download a Flash working example here.

- Mister

AS3 Button with Text and Basic Styling

It’s been a while since I had the opportunity to work on a pure AS3 project. I recently had a need for a simple AS3 button that displays text and decided to customize the SimpleButton control. As I quickly discovered, the SimpleButton control isn’t really setup to support text or a lot of other features, and hacking these features into it doesn’t make much sense. So I built a quick little class that emulates the behavior of SimpleButton but could display text and offer a few other features for styling the button dynamically.

SimpleButtonProject.swf

SimpleButtonProject.as

package
{
	import com.components.CustomButton;
	import flash.display.Sprite;
	import flash.events.MouseEvent;

	[SWF( width = '200', height = '200', backgroundColor = '#FFFFFF', frameRate = '20')]
	public class SimpleButtonProject extends Sprite
	{
		private var button:CustomButton;

		public function SimpleButtonProject()
		{
			button = new CustomButton("Welcome");
			button.x = 0;
			button.addEventListener(MouseEvent.CLICK, handleMouseClick);
			addChild(button);
		}

		private function handleMouseClick(event:MouseEvent):void
		{
			button.label = "Clicked"

			/*
			     // remove the button and mark it for garbage collection
			     button.removeEventListener(MouseEvent.CLICK, handleMouseClick);
			     this.removeChild(button);
			     button = null;
			*/
		}
	}
}

CustomButton.as

package com.components
{
	import flash.display.Shape;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.text.TextField;
	import flash.text.TextFormat;
	import flash.text.TextFormatAlign;

	public class CustomButton extends Sprite
	{
		// ---- Properties -----

		protected var __button:CustomSimpleButton;
		protected var __txtFormat:TextFormat;
		protected var __txtField:TextField;
		protected var __background:Shape;
		protected var __hitarea:Sprite;

		protected var _label:String = "";
		protected var _font:String = "Arial";
		protected var _fontSize:int = 12;
		protected var _fontColor:uint = 0x000000;
		protected var _upColor:uint = 0xFFCC00;
		protected var _overColor:uint = 0xCCFF00;
		protected var _downColor:uint = 0x00CCFF;
		protected var _buttonWidth:int;
		protected var _buttonHeight:int;
		protected var _buttonStrokeColor:int = 0x000000;

		public function get label():String
		{
			return _label;
		}
		public function set label(value:String):void
		{
			_label = value;
			updateDisplayList();
		}

		public function get buttonStrokeColor():uint
		{
			return _buttonStrokeColor;
		}
		public function set buttonStrokeColor(value:uint):void
		{
			_buttonStrokeColor = value;
			updateDisplayList();
		}

		public function get font():String
		{
			return _font;
		}
		public function set font(value:String):void
		{
			_font = value;
			updateDisplayList();
		}

		public function get fontSize():int
		{
			return _fontSize;
		}
		public function set fontSize(value:int):void
		{
			_fontSize= value;
			updateDisplayList();
		}

		public function get fontColor():uint
		{
			return _fontColor;
		}
		public function set fontColor(value:uint):void
		{
			_fontColor= value;
			updateDisplayList();
		}

		public function get upColor():uint
		{
			return _upColor;
		}
		public function set upColor(value:uint):void
		{
			_upColor = value;
			updateDisplayList();
		}

		public function get overColor():uint
		{
			return _overColor;
		}
		public function set overColor(value:uint):void
		{
			_overColor = value;
		}

		public function get downColor():uint
		{
			return _downColor;
		}
		public function set downColor(value:uint):void
		{
			_downColor = value;
		}

		// ---- Constructor -----

		public function CustomButton(label:String = "", w:int = 80, h:int = 22)
		{
			super();

			if(label != "") this.label = label;

			this.addEventListener(Event.REMOVED_FROM_STAGE, destroy);

			_buttonWidth = w;
			_buttonHeight = h;

			createChildren();
			updateDisplayList();
		}

		// ---- Public Methods -----

		public function destroy(event:Event = null):void
		{
			this.removeEventListener(Event.REMOVED_FROM_STAGE, destroy);

			if(__background) {
				__background.graphics.clear();
				__background = null;
			}

			if(__hitarea){
				__hitarea.removeEventListener(MouseEvent.MOUSE_OVER, onMouseOver);
				__hitarea.removeEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
				__hitarea.removeEventListener(MouseEvent.MOUSE_UP, onMouseOver);
				__hitarea.removeEventListener(MouseEvent.MOUSE_OUT, onMouseOut);
				__hitarea.graphics.clear();
				__hitarea = null;
			}

			if(__txtField) {
				__txtField.text = "";
				__txtField = null;
				__txtFormat = null;
			}
		}

		// ---- Protected Methods -----

		protected function createChildren():void
		{
			if(!__background) {
				__background = new Shape();
				addChild(__background);
			}

			if(!__txtField) {
				__txtField = new TextField();
				__txtField.selectable = false
				addChild(__txtField);
			}

			if(!__hitarea) {
				__hitarea = new Sprite();
				__hitarea.addEventListener(MouseEvent.MOUSE_OVER, onMouseOver);
				__hitarea.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
				__hitarea.addEventListener(MouseEvent.MOUSE_UP, onMouseOver);
				__hitarea.addEventListener(MouseEvent.MOUSE_OUT, onMouseOut);
				addChild(__hitarea);
			}
		}

		protected function drawButtonBackground(color:uint):void
		{
			if(__background){
				__background.graphics.beginFill(color);
				__background.graphics.lineStyle(1, buttonStrokeColor);
				__background.graphics.drawRect(0, 0, _buttonWidth, _buttonHeight);
				__background.graphics.endFill();
			}
		}

		protected function updateDisplayList():void
		{
			if(__txtField){
				__txtFormat = new TextFormat(font, fontSize, fontColor, false, null, null, null, null, TextFormatAlign.CENTER);
				__txtField.width = _buttonWidth;
				__txtField.defaultTextFormat = __txtFormat;
				__txtField.text = _label;
				__txtField.y = (_buttonHeight - __txtField.textHeight)/2;
			}

			if(__background) {
				drawButtonBackground(upColor);
			}

			if(__hitarea){
				__hitarea.graphics.beginFill(0x000000, 0);
				__hitarea.graphics.drawRect(0, 0, _buttonWidth, _buttonHeight);
				__hitarea.graphics.endFill();
			}
		}

		protected function onMouseOver(e:Event):void
		{
			e.stopPropagation();
			drawButtonBackground(overColor);
		}

		protected function onMouseDown(e:Event):void
		{
			e.stopPropagation();
			drawButtonBackground(downColor);
		}

		protected function onMouseUp(e:Event):void
		{
			e.stopPropagation();
			drawButtonBackground(overColor);
		}

		protected function onMouseOut(e:Event):void
		{
			e.stopPropagation();
			drawButtonBackground(upColor);
		}
	}
}

So there you have it, a very simple button that displays text and has a few other features. I realize its not so simple ( # lines of code), but I added a lot of public properties to change the style and label dynamically, as well as logic for garbage collection. If you don’t need those extra features, you can easily be customize it to fit any your needs.

-Mister

Adobe MAX 2009 – Los Angeles

This year Adobe Max 2009 is in my own backyard, well sort of 6 miles up the street, which means about an hour away in LA traffic. I am hoping to get in for free some how, either through Adobe Max Awards or by posting the Adobe Max Widget. See everyone there, maybe…

http://max.adobe.com/widget/MaxWidget.swf

Google crawls Flash, mostly… (now imporved!)

Today Google reported that they now have a new algorithm for indexing the textual content of Flash files. This has always been something businesses have been concerned with, how to do SEO (Search Engine Optimization) with Flash. Flash developers (and Flex) would have to place the content of the Flash into a page so that search engines could index it, now it looks like Google has stepped up and created a way to index the content of the Flash file itself, right down to the menu items. Take a look at the article on Google’s blog.

Google came out with another post today to answer questions around indexing Flash content. The question answer section of the post was great until you get to the very last question:

Q: What are the current technical limitations of Google’s ability to index Flash?
There are three main limitations at present, and we are already working on resolving them:

1. Googlebot does not execute some types of JavaScript. So if your web page loads a Flash file via JavaScript, Google may not be aware of that Flash file, in which case it will not be indexed.
2. We currently do not attach content from external resources that are loaded by your Flash files. If your Flash file loads an HTML file, an XML file, another SWF file, etc., Google will separately index that resource, but it will not yet be considered to be part of the content in your Flash file.
3. While we are able to index Flash in almost all of the languages found on the web, currently there are difficulties with Flash content written in bidirectional languages. Until this is fixed, we will be unable to index Hebrew language or Arabic language content from Flash files.

We’re already making progress on these issues, so stay tuned!

Hmm, if Google doesn’t index JavaScript and you use something like SWFObject to embed your Flash then you are pretty much back to where you were before Google made the improvements. For issue 1, this might not be a problem for smaller gadgets on 3rd party sites, but you don’t want your main application site to have that annoying click through problem caused by the patent law suit by EOLAS against Microsoft. Remember those times, before the issue was solved with JavaScript. For issue 2, your content will get indexed, but it won’t be considered part of your Flash content, this might effect some metrics or Omniture reporting (which means money).

Well, I guess its partial good news, at least they say they are working on those issues.

UPDATE:

Adobe announced at Max that it was working with Google to finally be able to track Flash applications. Google posted a new blog article on the subject here.

- Mr

Error #2025 – Clash between Flex 2 & Flash CS3

I got my new copy of Flash CS3 and proceeded to make a new preloader for my Flex application.  I made a document Class for my CS3 file and attached a CS3 ProgressBar component.  I had a public function in the Document Class for setting the progress of the Flex preloading.   I dropped the SWF file into my Flex document and set up all the preloading code.  If you set a preloader in Flex it will load that file in the first frame before it loads the rest of the Flex application.  You can do progressive preloading the same way you would in Flash.   The CS3 preloader worked like a charm because with CS3, I can now (though still not elegantly) talk to the functions inside the loaded SWF file.   Here is the CS3 Document Class code:

package com.preloader
{
	import flash.display.MovieClip;
	import fl.controls.ProgressBar;
	import fl.controls.ProgressBarMode;

	public class WelcomeScreenClass extends flash.display.MovieClip
       {
		private var progressBar:ProgressBar;

	        public function WelcomeScreenClass()
               {
			progressBar = new ProgressBar();
			progressBar.indeterminate = false;
			progressBar.mode = ProgressBarMode.MANUAL;
			progressBar.setSize(150, 22);
			progressBar.move(10, 50);
			progressBar.setStyle("barPadding", 3);
			progressBar.setProgress(50, 100);

			addChild(progressBar);
	        }

		public function setLoaderProgress(loaded:Number, total:Number):void
		{
			progressBar.setProgress(loaded, total);
			trace("loaded: " + loaded + " of total: " + total);
		}
	}
}

This all sounds good so far, a decent marriage between Flash and Flex, weeee!!   About a week later I noticed a bug in my Flex application.  Every time you hit the Tab key on any dialog created with the PopUpManager or even the Alert, I would get the following error code:

ArgumentError: Error #2025: The supplied DisplayObject must be a child of the caller.
 at flash.display::DisplayObjectContainer/getChildIndex()
 at mx.core::Container/getChildIndex()
 at mx.containers::Panel/getChildIndex()
 at fl.managers::FocusManager/::getChildIndex()
 at fl.managers::FocusManager/::sortByDepth()
 at fl.managers::FocusManager/::sortByTabIndex()
 at Array$/Array::_sort()
 at Array/http://adobe.com/AS3/2006/builtin::sort()
 at fl.managers::FocusManager/::sortFocusableObjectsTabIndex()
 at fl.managers::FocusManager/::sortFocusableObjects()
 at fl.managers::FocusManager/::keyDownHandler()

This particular bug is very vague and a search of the Internet yielded nothing that seem to fit my situation.  I am using Cairngorm with Modules and my suspicion was that I had created a shared code typology.   Modules are cool because they can reduce your initial application load, but they are a real pain in the ass to manage because you run into these strange bugs that always seem related to a race condition with loading one Class or Manager into one module.    I spent a few days tracking down this issue, testing each module, commenting out code.  I finally created a new project without any modules, determined that it was the shared code typology issue.  I still had the same issue.

Something about the error dawned on me, the “fl.managers”.   This is a CS3 code package that Flex does not have.   I then decided to remove my CS3 prelaoder, and guess what, the bug went away.  I had inadvertently created my own bug by using CS3 with the new ProgressBar component.  This component most likely extends UIComponent, which contains the FocusManager, and therefore, causes the issue with the FocusManager in Flex.  According to shared code typology 101, the first instance of the loaded manager wins.

The solution was to yank out the CS3 ProgressBar component and just create my own progress bar using the Flash drawing tools.   This was a tough error, and probably not the first one that will be encountered as Flash and Flex start mingling more in the near future.  

-mr