Introducing the FlexibleContainer component (aka Advanced ActionScript Refactoring – Step 3)
We’re now ready for our final step. We started with something that looked like this, and we are going to end up with something that looks like this. As you can see, the layout of the container is assignable at runtime.
After step 2, there was very little refactoring to do. I removed the DragTile class once and for all, and I added styles back in. The way that the Flex styling mechanism works, you can implement styles for non UIComponents, and that’s what I did. Each Layout object can have its own styles.
I created another concrete layout called CircleLayout to make sure that the layout was actually assignable at runtime. This exposed a few bugs, which I fixed. I factored out the common code between TileLayout and CircleLayout into a common concrete superclass, called Layout. In the end, the inheritance hierarchy for the layout managers looks like this:

I also “fixed” some nitpicky things along the way. I’ve always thought that the drag/drop feedback was a bit confusing, so I changed the behavior in a subtle way (see if you can spot it!).
In the end, the true test of whether this refactoring worked is to see how easy it is to add a new layout. Whereas the original DragTile code was 600+ lines of code, the CircleLayout code is only about 100 lines of code, and all of it pertains to geometry. There is virtually no code in there to manage renderers or animators or anything like that.
public class CircleLayout extends Layout { // ILayout interface override public function getMeasuredSize():Point { return getMaxSize(); } override public function findItemAt(px:Number, py:Number, seamAligned:Boolean):Number { // Can't execute this if we aren't attached to a container. if (!container || container.renderers.length == 0) return NaN; // Get the radius and center of the circle. var radius : Number = Math.min(unscaledContainerWidth, unscaledContainerHeight) / 2; var hCenter : Number = unscaledContainerWidth / 2; var vCenter : Number = unscaledContainerHeight / 2; var angle : Number = Math.atan2(py-vCenter, px-hCenter); if (angle < 0) angle += 2 * Math.PI; // figure out the closest "item" by working backwards from the angle to the index, using floating point math. var result : Number = container.renderers.length * angle / (2 * Math.PI); // depending on whether this is seam aligned, do a ceil or round. result = (seamAligned) ? Math.ceil(result) : Math.round(result); // do a modulo op to make sure that this is within [0, length-1]. Modulo is the correct // operator in this case because this is a circle. result %= container.renderers.length; return result; } override public function generateLayout():void { // Get the radius and center of the circle. var radius : Number = Math.min(unscaledContainerWidth, unscaledContainerHeight) / 2; var hCenter : Number = unscaledContainerWidth / 2; var vCenter : Number = unscaledContainerHeight / 2; // Find the max item size. var maxSize : Point = getMaxSize(); var max : Number = Math.max(maxSize.x, maxSize.y); // Inset the radius by the max size. radius -= max; // Loop through the items and position them. var length : int = container.renderers.length; for (var idx:int = 0; idx < length; idx++) { var renderer:IUIComponent = container.renderers[idx]; var target:LayoutTarget = animator.targetFor(renderer);//targets[idx]; // evenly space each item over 2*pi radians. var angle : Number = (2 * Math.PI) * idx / length; // position items on a circle. target.scaleX = target.scaleY = 1; target.item = renderer; target.unscaledWidth = renderer.getExplicitOrMeasuredWidth(); target.unscaledHeight = renderer.getExplicitOrMeasuredHeight(); target.x = hCenter + radius * Math.cos(angle) - target.unscaledWidth/2; target.y = vCenter + radius * Math.sin(angle) - target.unscaledHeight/2; target.animate = true; } // If there is more than one item, and if there is a drag target, nudge the items next to the drag target if (length > 1 && container.dragTargetIndex >= 0 && container.dragTargetIndex < length) { // Find the items to the left and right of the target. var leftIndex : int = (container.dragTargetIndex + length - 1) % length; var rightIndex : int = (leftIndex + 1) % length; var leftTarget : LayoutTarget = animator.targetFor(container.renderers[leftIndex]); var rightTarget : LayoutTarget = animator.targetFor(container.renderers[rightIndex]); // exaggerate the difference between the two targets by a factor of maxSize/2. var dx : Number = rightTarget.x - leftTarget.x; var dy : Number = rightTarget.y - leftTarget.y; var distance : Number = Math.sqrt( dx*dx + dy*dy ); leftTarget.x -= dx / distance * max/2; leftTarget.y -= dy / distance * max/2; rightTarget.x += dx / distance * max/2; rightTarget.y += dy / distance * max/2; } } protected function getMaxSize() : Point { // Can't execute this if we aren't attached to a container. if (!container) return new Point(0, 0); // Find the max item size. var maxWidth : Number = 0; var maxHeight : Number = 0; if(container.renderers.length > 0) { for(var i:int=0;i<container.renderers.length;i++) { var itemRenderer:IUIComponent = container.renderers[i]; maxWidth = Math.ceil(Math.max(maxWidth,itemRenderer.getExplicitOrMeasuredWidth())); maxHeight = Math.ceil(Math.max(maxHeight,itemRenderer.getExplicitOrMeasuredHeight())); } } return new Point(maxWidth, maxHeight); }
Could this be improved? Sure. There are still linkages between the layout and the container that we should probably get rid of. But for now, I think I’m going to stop. Maybe I’ll refactor the rest of it away later. :-)
Code for the final version can be found here.
Thank you for posting such an awesome component. That code is pretty rich and deep, and to be hoenst, I am not sure that I understand it all! Especially this part, I know that this is what adds all the items (images) to the display list, but I am not understanding the syntax and reasoning behind it… here is the code.
//_renderCash is an instance of Adobe’s AssociativeInstanceCache.as
//what exactly does this do, and why do we need to open and close the associations?
_renderCache.beginAssociation();
//loop through the array and create a bunch of items in our container
for(i = 0; i
it killed half my post… here is the second half
//loop through the array and create a bunch of items in our container
for(i = 0; i
ok, so it doesnt like what I am submitting! I am just going to leave out that line
Thank you for posting such an awesome component. That code is pretty rich and deep, and to be hoenst, I am not sure that I understand it all! Especially this part, I know that this is what adds all the items (images) to the display list, but I am not understanding the syntax and reasoning behind it… here is the code.
//_renderCash is an instance of Adobe’s AssociativeInstanceCache.as
//what exactly does this do, and why do we need to open and close the associations?
_renderCache.beginAssociation();
///what is this doing?
var renderer:IUIComponent = _renderCache.associate(_items);
//this is the really confusing part to me, especially the syntax of IDataRederer(rederer) …
//after looking at the IDataRederer interface it doesnt look like this should be possible… what am I missing?
IDataRenderer(renderer).data = _items;
//adds it to a list of renderers for later reference
_renderers = renderer;
//add this to the display list, any special reason for having to put it in a DisplayObject() call?
addChild(DisplayObject(renderer));
}
//again, why do we need to end the association?
_renderCache.endAssociation();
any further explanation?
ok, so it doesnt like what I am submitting! I am just going to leave out that line
///what is this doing?
var renderer:IUIComponent = _renderCache.associate(_items);
//this is the really confusing part to me, especially the syntax of IDataRederer(rederer) …
//after looking at the IDataRederer interface it doesnt look like this should be possible… what am I missing?
IDataRenderer(renderer).data = _items;
//adds it to a list of renderers for later reference
_renderers = renderer;
//add this to the display list, any special reason for having to put it in a DisplayObject() call?
addChild(DisplayObject(renderer));
}
//again, why do we need to end the association?
_renderCache.endAssociation();
any further explanation?
I would like to further customize this to reorder custom components instead of images (actually individual search field widgets) to construct a robust advanced search feature where queries can be built edited and stored. I have made the change to support components however I am seeing odd behavior on the drop (since I have only on panel, I am only worried about dragging and dropping to reorder – I have remove the DragEnter behaviors):
RangeError: Error #2006: The supplied index is out of bounds.
at flash.display::DisplayObjectContainer/getChildAt()
at mx.core::Container/getChildAt()[C:\dev\flex_201_gmc\sdk\frameworks\mx\core\Container.as:2369]
at mx.core::Container/http://www.adobe.com/2006/flex/mx/internal::getScrollableRect()[C:\dev\flex_201_gmc\sdk\frameworks\mx\core\Container.as:4234]
at mx.core::Container/mx.core:Container::createContentPaneAndScrollbarsIfNeeded()[C:\dev\flex_201_gmc\sdk\frameworks\mx\core\Container.as:4194]
etc.
I also see:
1.the dragProxy is not lined up with the mouse (usually when dragging with multiple components)
2. No “shuffle” effect when reordering components on a potential drop and
I am a relative Flex newbie (oldskool Flash 4-8 developer) and would appreciate any insight. I’ll be glad to include code samples of what you would think helpful.
Thansk again!
Randall Gremillion
[…] Introducing the FlexibleContainer component (aka Advanced ActionScript Refactoring – Step 3) […]
huffle†effect when reordering components on a potential drop and
I am a relative Flex newbie (oldskool Flash 4-8 developer) and would appreciate any insight. I’ll be glad to include code samples of what you would think helpful.
Thansk again!
I tried to contact you but contact page returns 404 (and so the about us) :(
[…] kuwamoto.org » Blog Archive » Introducing the FlexibleContainer component (aka Advanced ActionScript Refactoring – Step 3) […]
thanks for the walk through – the result looks quite useful. a bug that i just found:
in your example here http://examples.kuwamoto.org/DragTile/DragTile_step3/DragDrop.html
try this:
– change to canvas
– drag an image down into the second flexible container
– drag the image up into the first flexible container again
– click on the image
->
ArgumentError: Error #2015: Invalid BitmapData.
at flash.display::BitmapData()
at qs.controls::UIBitmap()
at qs.controls::FlexibleContainer/dragStart()