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.