DisplayObject Quirks and Tips
08.28.2010After having worked a great deal on the Rain SVG library, I’ve come to learn and re-learn some of the quirks and workarounds of DisplayObject, the fundamental UI class in ActionScript. For your benefit and mine, here they are. It’s only a short list of the many so if you’d like to add on, feel free to post a comment.
CacheAsBitmap with a mask breaks mouse events
If you have a sprite that has a mask and cacheAsBitmap set the true, the display object will no longer dispatch mouse events.
In this example, I’m using a PNG that’s partially transparent as a mask and it’s masking a black square. This type of mask only works when cacheAsBitmap is set to true for both the mask and the maskee. The result is a working mask, but the maskee will no longer dispatch mouse events.
[Embed(source="books.png")] protected var Books:Class; public function CacheAsBitmapAndMask() { var books:Bitmap = new Books(); var square:Sprite = new Sprite(); square.graphics.beginFill(0x000000); square.graphics.drawRect(0, 0, books.width, books.height); square.graphics.endFill(); square.addChild(books); square.mask = books; square.cacheAsBitmap = true; books.cacheAsBitmap = true; addChild(square); square.addEventListener(MouseEvent.CLICK, clickHandler); } protected function clickHandler(event:MouseEvent):void { trace('click'); }
Filter and a mask breaks mouse events
At its core, this quirk seems to be the same as the previous one. In this example I have a shape masking a sprite and the sprite has a drop shadow. It’s important to remember that adding a filter to a display object will automatically set cacheAsBitmap to true. The display object again will not dispatch mouse events.
public class FilterAndMask extends Sprite { public function FilterAndMask() { var mask:Shape = new Shape(); mask.graphics.beginFill(0xff0000); mask.graphics.drawRect(25, 25, 50, 50); mask.graphics.endFill(); var maskee:Sprite = new Sprite(); maskee.graphics.beginFill(0x000000); maskee.graphics.drawRect(0, 0, 100, 100); maskee.graphics.endFill(); maskee.filters = [new DropShadowFilter()]; maskee.addChild(mask); maskee.mask = mask; addChild(maskee); maskee.addEventListener(MouseEvent.CLICK, clickHandler); } protected function clickHandler(event:MouseEvent):void { trace('click'); } }
See https://bugs.adobe.com/jira/browse/FP-61 and https://bugs.adobe.com/jira/browse/FP-3818 for the bug reports.
You can work around this issue by wrapping the maskee in another sprite. Rather than setting the filter on the maskee, set it on the wrapper. Here’s an example:
public function FilterAndMaskWorkaround() { var mask:Shape = new Shape(); mask.graphics.beginFill(0xff0000); mask.graphics.drawRect(0, 0, 100, 100); mask.graphics.endFill(); var maskee:Sprite = new Sprite(); maskee.graphics.beginFill(0x000000); maskee.graphics.drawRect(0, 0, 100, 100); maskee.graphics.endFill(); maskee.addChild(mask); maskee.mask = mask; var maskeeWrapper:Sprite = new Sprite(); maskeeWrapper.addChild(maskee); maskeeWrapper.filters = [new DropShadowFilter()]; addChild(maskeeWrapper); maskee.addEventListener(MouseEvent.CLICK, clickHandler); } protected function clickHandler(event:MouseEvent):void { trace('click'); }
CacheAsBitmap breaks concatenatedMatrix and hitTestPoint()
When an ancestor of a given display object has cacheAsBitmap set to true, the display object’s concatenated matrix is incorrect. This also affects the validity of hitTestPoint() and probably some other functions as well.
In this example the ancestor has a filter which in turn sets cacheAsBitmap to true. When clicking on the child, the concatenated matrix reports tx=1 and ty=1. In this case, tx and ty are incorrect.
public function CacheAsBitmapConcatMatrix() { var ancestor:Sprite = new Sprite(); ancestor.x = 50; ancestor.y = 50; ancestor.filters = [new DropShadowFilter()]; addChild(ancestor); var child:Sprite = new Sprite(); child.x = 25; child.y = 25; var g:Graphics = child.graphics; g.beginFill(0x00dd00); g.drawRect(0,0,200,200); g.endFill(); child.cacheAsBitmap = true; child.addEventListener(MouseEvent.CLICK, child_clickHandler); ancestor.addChild(child); } protected function child_clickHandler(event:MouseEvent):void { var child:DisplayObject = DisplayObject(event.target); trace(child.transform.concatenatedMatrix); }
The workaround is fairly simple:
protected function getConcatenatedMatrix(source:DisplayObject):Matrix { var concatenated:Matrix = source.transform.concatenatedMatrix.clone(); var p:Point = source.localToGlobal(new Point(0, 0)); concatenated.tx = p.x; concatenated.ty = p.y; return concatenated; }
See https://bugs.adobe.com/jira/browse/FP-121 for the bug report. This post is also helpful: http://www.sephiroth.it/weblog/archives/2008/03/cacheasbitmap_hell.php.
Mouse events dispatched for transparent portions of bitmaps
Add a bitmap with some transparent pixels to a sprite. If you then click the tranparent portions of the bitmap, the sprite will dispatch a click event just as it would if you clicked opaque portions. This is often not desired behavior as you may want the click to “fall through” to whatever display object is behind it. There’s no easy way to toggle this functionality either. Most workarounds are limited by the other quirks mentioned so far or are not dynamic enough for general use. The best workaround I’ve used so far is InteractivePNG created by Moses Gunesch.
Possibly unexpected dimensions after rotation and scaling
Take a 100×100 square and rotated it 45 degrees. What’s the width of the shape? In Flash-world, it’s 141.4, or in other words, the “bounds” of the rotated rectangle is 141.4 pixels across. What if I want to access the unrotated width again? For one, you can set the rotation back to 0 and then request the width. That’s generally not the greatest option. Another option is to get the bounds of the square within its own coordinate space. Here’s an example:
public function RotatedDimensions() { var square:Shape = new Shape(); square.graphics.beginFill(0xff0000); square.graphics.drawRect(0, 0, 100, 100); square.graphics.endFill(); square.rotation = 45; addChild(square); trace(square.width); // Traces 141.4 var internalBounds:Rectangle = square.getBounds(square); trace(internalBounds.width); // Traces 100 }
The best way I can describe what getBounds() is doing here is that it’s seeing the square without regard to its scale or rotation within its parent. It’s important to understand what this does when scale is introduced. Let’s take this example:
public function RotatedScaledDimensions() { var square:Shape = new Shape(); square.graphics.beginFill(0xff0000); square.graphics.drawRect(0, 0, 100, 100); square.graphics.endFill(); square.scaleX = square.scaleY = .5; square.rotation = 45; addChild(square); trace(square.width); // Traces 70.7 var internalBounds:Rectangle = square.getBounds(square); trace(internalBounds.width); // Traces 100 trace(internalBounds.width * square.scaleX); // Traces 50 }
In this case the square’s been scaled down to half its original size and is rotated 45 degrees. If you ask for the width, flash reports 70.7. If you get the dimensions of the square within its own coordinate space, it will report 100 for the width. In other words, these are the unscaled, unrotated dimensions of the square. The third trace is an example of how to get the scaled but unrotated width of the square.
Invisible children contribute to parent’s dimensions
Create a child shape, set it to be invisible, and add it to a sprite. Even though the child is not visible, its dimensions still contribute to the parent’s dimensions. This may or may not be what you’re expecting, but there’s no way to easily toggle the inclusion of invisible children when calculating a display object’s dimensions.
Take this example:
public function InvisibleBounds() { var container:Sprite = new Sprite(); var left:Shape = new Shape(); left.graphics.beginFill(0xff0000); left.graphics.drawRect(0, 0, 50, 50); left.graphics.endFill(); container.addChild(left); var right:Shape = new Shape(); right.graphics.beginFill(0xff0000); right.graphics.drawRect(0, 0, 50, 50); right.graphics.endFill(); right.x = 50; right.visible = false; container.addChild(right); trace(container.width); // Traces 100 }
Even though the child on the right is invisible, the container still includes it in its dimensions.
See https://bugs.adobe.com/jira/browse/FP-741 for the bug report and some workarounds that work well but can be slow.
I hope that helps anyone running into the same issues. Please post a comment if you have quirks or tips of your own.
Tags: bug, cacheAsBitmap, DisplayObject, events, filter, mask, mouse, rotation, scale, transparent


[...] DisplayObject Quirks and Tips (from AaronHardy.com) [...]