Quick Links

Education Edu

Simulations Sims

Math Tools Math

Games Games

Generative ArtArt

Actinoscript Prog

Farmville Farm

 

 


Perfect Pop Up Panels Demo

Perfect Pop Up Panels in Actionscript 3

Click the above-left picture of the Perfect Pop Up Panel demo to start the demo in a new window.

 
 

Perfect Pop Up Panels in Actionscript 3

History

Some few times in ancient history, I managed to create pop-up tabbed panels that worked correctly. However, each time, I either forgot my previous work, or was unable to find it, and so I recreated it repeatedly. It was probably not designed in an especially re-usable manner anyway.

So, frustrated, that, yet again, I recently found myself re-inventing this wheel, I created a system designed to be re-usable and decided to publish it on my blog so that I would be able to readily re-find it. If someone else gets benefit from it, all the better.

This may not be the canonical method to create such, but it works solidly, it is object oriented and easily extended, and it seems to me to be relatively straightforward. If there is a move canonical means to accomplish such, I wouldn't mind a heads up.

Result

Content on this page requires a newer version of Adobe Flash Player.

Get Adobe Flash player

If that size is too small, you can see it the full size of your browser window by clicking the picture at top.

Design

Perhaps it's presumptuous of me to call my pop up panels perfect, but I was relatively pleased with the results. There were a couple of requirements that I had for them:

  1. The "popping" mechanism needed to be independent of the content's code.
  2. New panels needed to be add-able with a single additional code line.

That left a few considerations to the approach: what the panel is, where the panel exists, & who handles the popping.

The panel could be either a graphic or a movieclip. The panel could either contain the contents or the contents could internally have a panel-shapped graphic as an element in its display list. The popping mechanism could exist in the panel itself, the contents, or the parent calling class.

Because I wanted the panel to be able to contain components or simlar items that require mouse interaction, my first approach of poping the panel down when mousing out of the panel graphic/movieclip didn't seem to work because the mouse_out event gets triggered when moving over a component. This could be solved by doing a point hit test with the current mouse position against the panel, but this got to be burdensome. Also, there's always the case of someone quickly mousing away from the panel and the mouse_out event somehow not getting triggered. This could alternately be solved by creating an invisible shield behind each panel whenever it gets popped, that catches any mouse_over activity, but this prevents multiple panels from being opened concurrently. Such a shield would work, but not as part of the panel class.

Ideally, all the code for popping would be handled by the panel itself, afterall, it does know how far it needs to move up and down, but I found it simpler to have a parent of the panel handle that. In such a distributed code model, the parent could either be the contents or higher level that handles all panels. I'd already determined the hard way that I didn't want to add popping code to content code, so, that left the document class.

So, my eventual design made the panel to effectively be a class that just draws the panel graphic and addChild-s the passed in content and tab movieclips. The contents know nothing of the panel and communicate with parents by dispatching custom events. All of the brains for handling the popping occurrs in the calling class

I've created a class called PopPanel that takes 5 parameters:

  • a movieclip for the tab
  • a movieclip for the contents
  • a background color (Number, defaulted to 0xFFFFFF)
  • a boolean indicating whether or not to draw a hairline black edge around the panel (Defaulted to false)
  • a boolean indicating whether to force the tab to be on the right side of the panel rather than the left. (Defaulted to false)

Document Class

As the document class (or your own calling class) is instrumental in the proper popping & dropping of panels, I discuss this at length. If the call is understood, the panel itself is relatively straightforward.

I tend to force panel placement at the bottom of the screen, but you can choose to do otherwise. Consecutive panels are placed overlapping so that only their tabs show and with a spacing between them of panelOffset. In the document class, I create the panels in an array defined at compile time, but they can be stuffed in at runtime instead. This is how I accomplish it:

private var panels:Array= [ // tab MovieClip contents MovieClip bgColor, showEdge tabOnRight new PopPanel( new GreenFace20x20(), new RedCar200x100(), WHITE, true), new PopPanel( new Txt123(), new ComponentizedPanel(), WHITE, true), new PopPanel( new OrangeCat20x20(), new RocketShip(), BLACK, true, true), new PopPanel( new GreenFace20x20(), new RedCar200x100(), WHITE, false, true), new PopPanel( new OrangeCat20x20(), new ComponentizedPanel(), WHITE, true) ];

Of course, this presumes that I have classes with names of those given above. I deliberately made the tab movieclips to be only 20x20. By default, PopPanel.as uses a margin of 10 that applies to all edges of the tab and the contents. In my case, I have predefined constants for WHITE and BLACK. Specifying true for the tabOnRight parameter creates the tab in the appropriate position on the panel, but the tab itself always starts at x=0 on the symbol -- the panel portion itself is what gets shifted in x. Having the panels start at the tab start position makes it easier to place them properly from the document class. Importantly, though, because my document class doesn't currently concern itself with the width of the panel, using this method may place a portion of the panel off the left edge of the stage depending on its width and how many panels appear before it. This is the case with the 3rd panel (rocket ship). I deliberately left this as a reminder of this limitation, however, the document class code could be amended to account for this, but the tabs would be unevenly spaced. Lacking any idea as to how to properly space tabbed panels so that the tabs are evenly spaced but the tabs' contents don't overshoot the stage, I figured it's probably best to just keep all tabs on the left unless the panel's contents go off the right side of the stage.

The document class also has a zero alpha stage-sized rectangle called shield which must be added to the display list prior to the panels. This lets me know when the mouse has left a popped panel.

In the document class, in order to effect proper setup and popping of the panels, I have the following functions:

  • addPanels()
  • resetPanelListeners()
  • popPanelUp(e:Event)
  • makeThisPanelHighest(pnl:MovieClip)
  • popAllPanelsDown(e:Event)
  • addShield()
  • shieldOn()
  • shieldOff()

addPanels

The purpose here is self evident. The panels are taken from array panels and added to the display list, positioned, and given event listeners.

resetPanelListeners

This routine re-adds the panels' event listeners if they have been removed.

popPanelUp

Triggered by a click on the tab, this routine pops the panel up, removes the panel's listeners, and turns the shield on.

makeThisPanelHighest

This swaps the current panel's Z position with that of the topmost panel ensuring that the current panel won't be occluded by tabs or other panels.

popAllPanelsDown

Runs as an event handler to a mouse_over involving the shield, this routine returns all panels to their down states, resets their listeners, turns the shield off, and has the panels do any necessary cleanup.

shieldOn

Adds the event listener for the shield.

shieldOff

Removes the shield's event listener.

The panel with "123" as its tab's movieclip has a button and a color picker in its content's movieclip. This is to demonstrate that the panels don't interfere with mouse clicks for their contents. However, the content's movieclip must either appropriately deal with all events generated by its contents, or, if events should affect the document class, events should be bubbled up to it. I arbitrarily gave one of the panels an inner glow filter to provide it a rounded 3D look. I generally don't use the black hairline edge when doing so. I also gave it a drop shadow filter, another nice touch if you have no performance concerns. Because some panels will have animations, pop-able subpanels, or certain components, I found it necessary to have each panel process it's own content minimization procedure. Rather than require a stubb in every content, I check for the presence of popDown() with the hasOwnProperty method.

If, for example, in the case of the color picker, the user exits the panel while the color picker's color panel is popped out, the color panel will stay in place even though the panel has popped back down. So, the shield's event handler, which gets activated on mouse_over (effectively leaving the panel), tells each contents' movieclip to deal with issues itself (in the contents' popDown routine.

BTW, the popping is done via AS3's built in tweening engine. This can readily be swapped out for Tweener, Tweenlite, or your favorite more efficient engine. Elastic on up, Bounce on down. Also, it's pretty acceptable UX-wise to have the panel simply jump up and down rather than do any tweening -- just in case the tweens start to grate after a bit.

Code

Here's a peek at the code

Document class

Again, all the brains for the system reside in the calling class, in my case, the document class.

package { import flash.display.MovieClip; import flash.events.Event; import flash.events.MouseEvent; import flash.filters.GlowFilter; import flash.filters.DropShadowFilter; import fl.transitions.Tween; import fl.transitions.easing.*; import PopPanel; public class PopPanelTestor extends MovieClip { // Constants: public static var WHITE:Number=0xffffff; public static var BLACK:Number=0x000000; // Public Properties: // Private Properties: private var shield:MovieClip = new MovieClip(); private var panels:Array= [ // tab MovieClip contents MovieClip bgColor, showEdge tabOnRight new PopPanel( new GreenFace20x20(), new RedCar200x100(), WHITE, true), new PopPanel( new Txt123(), new ComponentizedPanel(), WHITE, true), new PopPanel( new OrangeCat20x20(), new RocketShip(), BLACK, true, true), new PopPanel( new GreenFace20x20(), new RedCar200x100(), WHITE, false, true), new PopPanel( new OrangeCat20x20(), new ComponentizedPanel(), WHITE, true) ]; private var panelOffset:Number=5; private var topPanel:MovieClip; private var tween:Tween; // Initialization: public function PopPanelTestor() { addShield(); addPanels(); var igf:GlowFilter = new GlowFilter(); igf.color=BLACK; igf.inner=true; igf.quality=3; igf.strength=0.5; var dsf:DropShadowFilter = new DropShadowFilter(); dsf.strength=0.5; dsf.blurX=dsf.blurY=10; panels[panels.length-2].filters=[igf,dsf]; } // Public Methods: // Protected Methods: private function addPanels():void { var totalTabW:Number=0; for(var iPanel:Number=0; iPanel<panels.length; iPanel++) { addChild(panels[iPanel]); panels[iPanel].x=totalTabW; totalTabW+=panels[iPanel].tabWidth+panelOffset; panels[iPanel].y=stage.stageHeight; panels[iPanel].name="panel"+iPanel; topPanel=panels[iPanel]; } resetPanelListeners(); } private function resetPanelListeners():void { for(var iPanel:Number=0; iPanel<panels.length; iPanel++) { if ( !panels[iPanel].hasEventListener(MouseEvent.CLICK) ) { panels[iPanel].addEventListener(MouseEvent.CLICK,popPanelUp); } } } private function popPanelUp(e:Event):void { e.currentTarget.removeEventListener(MouseEvent.CLICK,popPanelUp); tween= new Tween( e.currentTarget, "y", Elastic.easeOut, stage.stageHeight, stage.stageHeight-e.currentTarget.dPop, 0.5, true ); makeThisPanelHighest(MovieClip(e.currentTarget)); shieldOn(); } private function makeThisPanelHighest(pnl:MovieClip):void { swapChildren(pnl,topPanel); topPanel=pnl; } private function popAllPanelsDown(e:Event):void { if (!tween.isPlaying) { for(var iPanel:Number=0; iPanel<panels.length; iPanel++) { if (panels[iPanel].y != stage.stageHeight) { tween= new Tween( panels[iPanel], "y", Bounce.easeOut, panels[iPanel].y, stage.stageHeight, 0.25, true ); panels[iPanel].contents.popDown();
} } shieldOff(); resetPanelListeners(); } } private function shieldOn():void { shield.addEventListener(MouseEvent.MOUSE_MOVE,popAllPanelsDown); } private function shieldOff():void { if (shield.hasEventListener(MouseEvent.MOUSE_MOVE)) { shield.removeEventListener(MouseEvent.MOUSE_MOVE,popAllPanelsDown); } } private function addShield():void { shield.graphics.beginFill(0,0); shield.graphics.drawRect(0,0,stage.stageWidth,stage.stageHeight); shield.graphics.endFill(); addChild(shield); } } }

PopPanel class

As previously mentioned, this class'es guts are not as interesting as the results. Of the three functions, setupBG creates the panel graphic, setupTab places the tab, and setup contents places the contents. Relatively straightforward. The important thing is the set of functionality in the document class parent of the tabs.

package { import flash.display.MovieClip; import flash.display.Sprite; public class PopPanel extends MovieClip { // Constants: // Public Properties: public var dPop:Number=0; public var tabWidth:Number=0; // Private Properties: private var bg:Sprite = new Sprite(); private var margin:Number=10; private var cw:Number; private var ch:Number; private var tw:Number; private var th:Number; public var tabOnRight:Boolean; public var contents:*; // Initialization: public function PopPanel(tmc:MovieClip, cmc:MovieClip, bgColor:Number, showEdge:Boolean=false, rightTab:Boolean=false) { cw=cmc.width; ch=cmc.height; tw=tmc.width; th=tmc.height; tabOnRight=rightTab; tabWidth=tw+2*margin; dPop=ch + 2*margin; setupBG(bgColor, showEdge); setupTab(tmc); setupPanel(cmc); } // Public Methods: // Protected Methods: private function setupBG(bgColor:Number, showEdge:Boolean):void { addChild(bg); if (showEdge) bg.graphics.lineStyle(0.1, 0); else bg.graphics.lineStyle(0,bgColor); bg.graphics.beginFill(bgColor); if (!tabOnRight) { with (bg.graphics) { moveTo(0, 0); // a lineTo(0, ch+2*margin); // b lineTo(cw+2*margin, ch+2*margin); // c lineTo(cw+2*margin, margin); // d curveTo(cw+2*margin, 0, cw+margin, 0); // f lineTo(tw+2*margin, 0); // g lineTo(tw+2*margin, -th-margin); // h curveTo(tw+2*margin, -th-2*margin, tw+margin, -th-2*margin); // j lineTo(margin, -th-2*margin); // k curveTo(0, -th-2*margin, 0, -th-margin); // m lineTo(0, 0); // a } } else { var xOff:Number= cw - tw; with (bg.graphics) { moveTo (0-xOff, margin); // A lineTo (0-xOff, ch+2*margin); // B lineTo (cw+2*margin-xOff, ch+2*margin); // C lineTo (cw+2*margin-xOff, -margin-th); // D curveTo(cw+2*margin-xOff, -2*margin-th, cw+margin-xOff, -2*margin-th); // E lineTo (cw+margin-tw-xOff, -2*margin-th); // F curveTo(cw-tw-xOff, -2*margin-th, cw-tw-xOff, -margin-th); // G lineTo (cw-tw-xOff, 0); // H lineTo (margin-xOff, 0); // I curveTo(0-xOff, 0, 0-xOff, margin); // A } } bg.graphics.endFill(); } private function setupTab(tmc:MovieClip):void { addChild(tmc); tmc.x=margin; tmc.y=-(tmc.height+margin); } private function setupPanel(cmc:MovieClip):void { contents=cmc; addChild(cmc); cmc.x=margin + (tabOnRight?tw - cw:0); cmc.y=margin; } } }