b-log – betriebsraum weblog

Software Development, Human-Computer Interaction, Projects…

stage.invalidate() and Event.RENDER for UI elements

November 3, 2007

We’ve been doing lots of ui stuff lately using ActionScript 3 and the new methods available for redrawing. Unfortunately there’s a Flash Player Bug with Event.RENDER which you should keep in mind when using that approach. Before talking about the bug, here’s a quick introduction to general redrawing techniques (just in case you’re not already familiar with that topic. If you’re interested in the AS3 stuff and the Bug, just skip the next paragraphs…):
Let’s say you want to create a custom menu class for your website: You create the class, the constructor, some methods for adding and removing menu items and getter / setter methods for the menu properties and you create one central draw() method which draws the whole menu and items based on the that data. The problem is that the menu has to be redrawn each time something changes. Consider the following example:

var menu:Menu = new Menu();
menu.addItem(new MenuItem("item1", 1)); //something changed: store data, call draw() internally
menu.addItem(new MenuItem("item2", 2)); //something changed: store data, call draw() internally
menu.addItem(new MenuItem("item3", 3)); //something changed: store data, call draw() internally
menu.itemSpacing = 10; //something changed: store data, call draw() internally

In the above simplified example code the menu would be completely redrawn 4 times in a single frame. But how can you achieve just 1 redraw of the menu? The simplest solution is to call a public method redraw() at the end of the creation process:

var menu:Menu = new Menu();
menu.addItem(new MenuItem("item1", 1)); //don't call draw() internally, just store data
menu.addItem(new MenuItem("item2", 2)); //don't call draw() internally, just store data
menu.addItem(new MenuItem("item3", 3)); //don't call draw() internally, just store data
menu.itemSpacing = 10; //don't call draw() internally, just store data
menu.redraw() // now call draw() internally

Works. The menu will be redrawn only 1 single time. But now there’s another problem: You have to make sure to call redraw() AFTER you you have called the other methods, which is bad. An optimal solution would allow for calling your setup methods in an arbitrary order but still draw the menu only once internally, which brings me to the next solution:
In the old ActionScript 1 and ActionScript 2 days you could use the onEnterFrame event for that:

var menu:Menu = new Menu();
menu.addItem(new MenuItem("item1", 1)); //something changed: store data, call invalidate() internally
menu.addItem(new MenuItem("item2", 2)); //something changed: store data, call invalidate() internally
menu.addItem(new MenuItem("item3", 3)); //something changed: store data, call invalidate() internally
menu.itemSpacing = 10; //something changed: store data, call invalidate() internally
// call draw() on the next frame

The basic principle is:
In each method that changes something visually you would call an invalidate() method at the end. This means “something has changed” and is “invalid” now, so redraw on the next frame. invalidate() then adds an onEnterFrame() event (to a temporary movieclip) that calls the draw() method when it’s executed. If it is called multiple times in a single frame (like in the above addItem() calls), the onEnterFrame is overridden each time and just the last invalidate() call is actually executed and draws the next frame. This is basically how the V2 Flash components work. In ActionScript 3.0 redrawing can be handled in a more advanced way:

You can use the built-in method stage.invalidate() and Event.RENDER to redraw your custom ui classes. For the render event to be broadcasted, the object must be on the display list and stage.invalidate() must be called (tells the flayer player to redraw the display list at the next render opportunity). You can create a class which acts as the base for all your custom ui components using that technique.
Colin Moock demonstrates that in his Essential ActionScript 3.0 book with subclasses of the Shape class. For ui components with child elements and interactivity you could subclass the sprite class and create a BasicSprite class (the following code is based on Moock’s BasicShape class with some modifications):

package de.betriebsraum.display {
 
	import flash.display.Sprite;
	import flash.events.Event;
 
	import de.betriebsraum.IDisposable;
 
	public class BasicSprite extends Sprite implements IDisposable {
 
		protected var _changed:Boolean = false;
 
		public function BasicSprite() {
			addEventListener(Event.ADDED_TO_STAGE, addedToStageListener);
			addEventListener(Event.REMOVED_FROM_STAGE, removedFromStageListener);
		}
 
		public function redraw():void {
			draw();
			clearChanged();
		}
 
		protected function draw():void {
 
		}
 
		protected function setChanged():void {
			_changed = true;
			requestRedraw();
		}
 
		protected function clearChanged():void {
			_changed = false;
		}
 
		protected function hasChanged():Boolean {
			return _changed;
		}
 
		protected function requestRedraw():void {
			if (stage != null) {
				stage.invalidate();
			}
		}
 
		protected function addedToStageListener(ev:Event):void {
			onAddedToStage(ev);
			stage.addEventListener(Event.RENDER, onRenderStage);
			if (hasChanged()) {
				requestRedraw();
			}
		}
 
		protected function onAddedToStage(ev:Event):void {
 
		}
 
		protected function removedFromStageListener(ev:Event):void {
			onRemovedFromStage(ev);
			stage.removeEventListener(Event.RENDER, onRenderStage);
		}
 
		protected function onRemovedFromStage(ev:Event):void {
 
		}
 
		protected function onRenderStage(ev:Event):void {
			if (hasChanged()) {
				redraw();
			}
		}
 
		public function dispose():void {
			removeEventListener(Event.ADDED_TO_STAGE, addedToStageListener);
			removeEventListener(Event.REMOVED_FROM_STAGE, removedFromStageListener);
			if (stage != null) {
				stage.removeEventListener(Event.RENDER, onRenderStage);
			}
		}
 
	}
 
}

The most noticable modifications are:

When you look at the code (or the comments in BasicShape) you’ll notice that this is really an efficient technique because the object is redrawn at the next render opportunity only if it is on the display list and only if it has changed.

You can make it even more efficient if you use different types of invalidation (as the V3 components do): Instead of redrawing everything in the draw() method you could draw or set only certain elements of your components, e.g. when the component’s size changes. If you need that kind of invalidation, keyboard- / focus management and consistency with other V3 components etc., then you could subclass the V3 UIComponent class but if you want to keep it small, simple and lightweight and don’t have to build an extensive ui framework, base classes like BasicShape or BasicSprite might be the better choice.

Unfortunately there’s also a dark side of that approach:
After building some ui classes (based on BasicSprite) I noticed that sometimes they wouldn’t be redrawn – no redraw occured although setChanged() was called. Particulary it occured when I tried to setup child components in the draw() method of another component or in other event listener methods. Googling revealed that there seems to be a bug in Flash Player: Triggering stage.invalidate() during a “render” event listener fails. (184574)

In the V3 components source code there are some comments which basically say the same thing (List.as, line 437: “// Call drawNow on nested components to get around problems with nested render events”). The workaround is to call redraw() (or some method that immediately redraws the component) in those situations.

I hope this will be fixed in the next player version…If you have other solutions / workarounds for this bug, please send a mail or leave a comment.

Filed under: Flex/AS3

7 Responses to “stage.invalidate() and Event.RENDER for UI elements”

  1. Lanny says:

    An additional problem with the RENDER event is that it is not dispatched when the wmode of a swf is not “default”. Both “opaque” and “transparent” will cause this problem.

  2. felix says:

    the wmode problem was fixed in the latest player version
    but unfortunately “Triggering stage.invalidate() during a render event listener…(184574)” hasn’t been fixed yet…

  3. chris says:

    What’s driving me nuts, is I’m using a similar set up to Moock’s, with a base class called AbstractUI. If I remove an instance of AbstractUI from the display list, Event.RENDER no longer dispatches when I call stage.invalidate()…ever. It completely breaks!
    Is there something that could be stopping the Event in the capture phase? Please help.

  4. felix says:

    Hi Chris,

    as far as I know Event.RENDER is only dispatched when the object is on the display list (which is good because usually you dont’t want to redraw when the object is not visible). In Moock’s approach a redraw is requested when you add it back to the display list.

  5. […] code. The Flex 2 & 3 components do not have this problem, why Flash? It turned out to be a bug in the Flash Player. The old way of doing invalidation in the Flex 1.5/1 & Flash 8/MX 2004/MX components was to […]

  6. […] draw updates to be performed at the end of a frame. I say preferred – it would be if it wasn’t buggy. Even the CS3 components don’t rely on it to work as advertised. Jesse Warden has a fix for the CS3 […]

  7. […] trying to tween both enabled and mouseFocused at the same time would cause redrawState() (see this for an explanation on the solution). But still, I think the advantages far outweight the […]

Add a comment

You must be logged in to post a comment.