Advanced Callout Component

12.17.2008

Most flex developers have become acquainted with the built-in tooltop the flex framework provides. It’s very convenient when all you need is a plain text hint to display briefly while the cursor is over a particular element.

But what’s a flex developer to do when more is called for? The flex tooltip sometimes just won’t cut it. To appease my appetite, I cooked myself up a callout component. Please, feast with me.

Here’s some of the features of the callout component:

  • Location-aware – It knows how far it is from the edge of the stage. If it’s too close to any edge of the stage, it knows to put the callout bubble in a direction that won’t make it display off-screen.
  • IUIComponent support – Anything you could stick in any other flex container you can stick inside the callout.
  • Skinnable – You can provide a custom skin for the tail and/or bubble.
  • Stylable – Several styles are supported such as background color, background alpha, padding, tail size, tail offset, preferred tail position, etc.
  • Flexible anchoring – You can anchor the callout (where the tail points to) to either a component or a specific point on the stage.

See it for yourself here! Right-click for the source. If you find bugs or make improvements, please let me know and I’ll post the changes.

Here are some known limitations:

  • Currently the tail can only be positioned on the corners or the center of a side. It can’t be offset from the center of a side. If you don’t know what I mean, you probably won’t notice.
  • The analysis of where the tail will be placed given stage limitations is done in a clockwise fashion. In other words, if the callout can’t fit on the stage with the tail on the bottom-left, it will try placing the tail on the left, then the top-left, then the top, etc. However, you can declare which position you would prefer (using the preferredTailPosition style) and it will perform the analysis in a clockwise fashion from that position.
  • Fade effects applied to the callout won’t work quite as expected due to overlapping skin parts.
  • Using a Text component for the content property doesn’t work currently. This should be fully supported; I just haven’t got around to fixing it.

Tags: , , , ,


Comments

01.05.2009 / Adam said:

Excellent work! I’m putting this to use :)

My application I’m using this in (annotating medical images) may require a user to choose which direction the tail extends. Would I be right in assuming that if I modify Callout.as/commitProperties() to not evaluate the tail position – essentially comment out

for each (var proposedTailPosition:String in tailPositionOrder) {…}

and modify

actualTailPosition = null;

to be

actualTailPosition = preferredTailPosition;

that the tail position would *always* be the preferred one?

Thanks,

Adam

01.05.2009 / Aaron Hardy said:

Sounds like a cool project! Your solution sounds like it would probably work except you might run into problems with the callout getting the correct anchor point. No worries though–I can definitely see others wanting to do exactly what you’re trying to do so I’ve updated the code. You might need to clear out your browser cache before pulling up the source again to ensure you actually get the new code.

Essentially I added a forcePreferredTailPosition style. When set to true, it forces the callout to use the preferredTailPosition regardless of whether the callout will end up off-screen.

Good luck! Post a comment if you get stuck or have more feedback. I’d love to see the project when you’re done if it’s open to the public.

02.25.2009 / Erich Cervantez said:

I like it! Great job!

03.12.2009 / Tristan said:

Hi Aaron, this is a great little script. I noticed it doesn’t have an explicit license, and I was interested in using it in my project but just wanted to make sure it was okay. It would be great if it were MIT or BSD-style licensed. Thanks!

03.12.2009 / Aaron Hardy said:

Hey Tristan,

Thanks for taking the time to even ask! Consider it released under the MIT license. I’ll put the license in the code itself when I get a chance, but go ahead and start using it.

If your project is public, I’d love to check it out after you implement the callout. I’m just curious to see how people are using it. If you’d like, feel free to post it here so others can see as well. Thanks!

Aaron

03.13.2009 / Koen said:

Hi,

Maybe a stupid question but how can you easily show and hide the tooltip?

For example onmouseover => show
onmouseout => hide

I dont get directly how to hide the callout you just created.
How can I access the one which is just created?

Because if I use callout.hide() then the callout variable should be a variable visible for
the whole application?

I m just starting with flex so maybe there is a really easy solution?

thx for the component!

03.13.2009 / Aaron Hardy said:

@Koen

callout.hide() would be the correct way, but I’m not sure what you mean when you say the variable should be visible for the whole application. If you’d like to explain a little more, maybe I can help you more effectively. On the other hand, here’s a simple example that might answer your question:

<mx:Box xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="300">
	<mx:Script>
		<![CDATA[
			import com.aaronhardy.callout.Callout;
 
			protected var callout:Callout
 
			protected function mouseOverHandler(event:MouseEvent):void
			{
				callout = new Callout()
				var contentLabel:Label = new Label();
				contentLabel.text = "Plain text content.";
				callout.content = contentLabel;
				callout.show(DisplayObjectContainer(event.currentTarget));
			}
 
			protected function mouseOutHandler(event:MouseEvent):void
			{
				callout.hide();
			}
		]]>
	</mx:Script>
	<mx:Label text="MouseOver Test" mouseOver="mouseOverHandler(event);"
			mouseOut="mouseOutHandler(event);"/>
</mx:Box>

03.13.2009 / Aaron Hardy said:

@Tristan

I’ve now added the MIT license to the source code. Good luck on your project!

03.15.2009 / Tristan said:

Thanks Aaron! I’m currently using it as a callout for cells of a DataGrid. I had to modify it to be able to use a separate regular DisplayObject (instead of a DisplayObjectContainer) as an ‘owner’ in order to reliably calculate the correct dimensions from an ItemRenderer, but it’s working quite well. The project isn’t ready and is sort of niche, but when it’s public I will let you know. Thanks again.

03.16.2009 / Koen said:

Thx Aaron,

Ok I got it. That was the first thing I tried and it works …

but I was trying to achieve the following:

create a showCallout(event:Event , text:String) method to create callouts so I just have to give the
text I want to show within the callout.

onMouseOver=”showCallout(event,’This is my help text in the callout’)”

And then later on

on click=”hideCallout(//name of the callout//);”

But then when I use multiple callouts, I cant use the hide method anymore
because I dont know the name of the callout to hide?

Or is it not a problem to use a different variable for every callout I want
to make on a screen? It sounds a bit overkill no? Because mostly the things
that change are the text and the component where the callout is shown?

So imagine that I want to create hundred callouts on one screen?
What would be the best approach to create them and to hide them seperatly?

thx

03.18.2009 / Aaron Hardy said:

@Koen

Your description still isn’t completely clear but if I understand correctly, it sounds like you have many different components in your application, they can create their own callouts, but only one callout will ever exist per component. Now you’re trying to track which callout was created by which component so you can close the associated callout when the user clicks on a given component. Does that sound right?

If that’s what you mean, it sounds like you need a dictionary to map each component to its associated callout. Here’s the language reference: Dictionary. Basically, when a callout is created for a component, you will store the component that “owns” the callout as the dictionary entry’s key and the callout as the entry’s value. Something like dict[button3] = myNewCallout;

Then when the user clicks on the button, you can hide the callout like so: Callout(dict[button3]).hide(); In your case you’ll probably want to substitute event.currentTarget in for button3 since your functions are event listeners for a variety of components and not just button3.

Hope that helps! Good luck and post back if you’re still having problems.

03.18.2009 / Aaron Hardy said:

@Tristan

If you don’t mind and think your code changes are generic enough to help others out, send over your changes and I’ll update the code. Thanks for the response!

03.30.2009 / Chad Rainey said:

Hi Aaron, great plugin! I am having an issue where the callout is placed off-stage a little the first time it is displayed, then the next time it is placed appropriately. I am weeding through the code, but I am new to Flex / AS3 so it is a little Greek to me.

My callout is about 320px x 200px most of the time at it’s largest size.

Is there a place I can tweak or fix that? We are developing a cool mapping app, I will post up a link once it goes live.

Thanks!
Chad

03.31.2009 / Chad Rainey said:

OK, I think I figured it out. Basically I am using htmlText on my lables and text fields and the getExplicitOrMeasuredHeight() is not able to accurately calculate the height of the callout if there are line breaks () in the html…

So I did a little workaround and added some height to the callout if it was under a certain height and it seems to work well now!

Thanks again! I will post up a link once the project goes live!

04.03.2009 / Dan said:

Firstly, and importantly, thanks Aaron for the great code.

I have just started with Flex/AS3 and am creating a little AIR app. I am using your code to pop-out tips within a custom TextArea class but I am getting a compile error on “DisplayObjectContainer” to remove this I had to “import flash.display.DisplayObjectContainer” … I dont quite understand why I had to do this but your examples didnt which is confusing.

But also, now when I run the app, it crashes on the “callout.show( DisplayObjectContainer( urlCurrentlyOver.target ) );” method call.

The “urlCurrentlyOver” above is my own utility object for storing the values of a mouse event and the propert “.target = mousevent.target” so this should work evaluate to the same thing as your examples.

The error I get is “Type Coercion failed: cannot convert mx.core::UITextField@810a0b1 to flash.display.DisplayObjectContainer.”

I understand you may not have enough code, but perhaps you may know what I have done wrong.

Thanks in advance.

04.04.2009 / Dan said:

Also, is there any way it can be styled like a Windows InfoTip which has a slight gradient background, black border and drop shadow (e.g. http://nick.typepad.com/blog/2008/12/coming-in-feedd.html)

04.06.2009 / Shilpa said:

Hi Aaron,

First of all i must appreciate your work.Great job done.This has definately lessend my work but i am stuck in between.

I am creating a real time location system where in alarms(which are small images as icons)come on images(say floorplan) and i want to show callouts on alarms at runtime.
They work perfectly uptill this point.But now when i scroll down the image,the alarm moves but the callout remain at the initial position(i.e it does not move when i scroll up and down).

Also why is it that i can only show callouts at POPUp.Cant i add it as a child on any container so that it comes in my container instead of stage.

Is there any way to get this going.I am really stuck.

04.15.2009 / Aaron Hardy said:

@Dan

The reason you’re getting your type coercion error is due to event bubbling. Technically, while you think you’re clicking on the TextArea, you’re actually clicking on the UITextField within the TextArea. The UITextField is the one dispatching the event and is therefore what you’re seeing as event.target. What you want to be using is event.currentTarget. That will be the TextArea to which you added the event listener. TextArea extends DisplayObjectContainer while UITextField does not, hence the type coercion error.

As far as styling a tip like in Windows, you might have a bit of trouble due to the bubble and the tail being separate skin parts, but you could probably get something close.

Good luck!

04.15.2009 / Aaron Hardy said:

@Shilpa

The reason the callout stays where it was initially created is because it’s taking advantage of Flex’s PopUpManager which doesn’t pay much attention to scrolling or window resizing. In fact, it’s technically not even part of the application element at all, but instead the system manager. I’m sure the component could be made more aware of scrolling, window resizing, or any movement on the owner, but I’m pretty sure it would take a good amount of work.

The callout can’t simply be the child of any container because container positioning and sizing just wasn’t built to accommodate it. Try something like this:

<mx:Box width="600" height="600">
	<mx:Button x="250" y="50"/>
</mx:Box>

Notice the button doesn’t actually get positioned at x=250, y=50. Layering and clipping of containers is another issue, but just as imperative. By using the PopUpManager, you get a lot of flexibility I don’t think you could get by just making it a child of a container.

Thanks for bringing those points up though. Maybe someone out there will act on them and blow our minds.

09.21.2009 / stacey reiman said:

awesome work! one problem I’m having: when I go to another state, the callouts are still there. How to autoclose on leaving a state or viewstack?

gracias!

09.21.2009 / Aaron Hardy said:

@Stacey

No hay de que. The callout acts like any other popup you might use with the PopUpManager. The callout is actually a child of SystemManager, which hangs around no matter what state you’re in. The solution to your problem is to remove them manually when you switch states and you can add them back when you come back to the state if you choose. If you’d rather the callouts be removed automatically, you could tweak the Callout.show() method. You’d add some code to watch for when “owner” is removed from the display list and trigger the callout to close itself when such an event occurs. I won’t have any time soon to implement that functionality but I’d be glad to answer questions. ¡Suerte!

10.07.2009 / Ron Rebennack said:

Great code… This is what I’ve been needing for a while now.

Small problem that I’m not sure how to fix:
If you use the Callout on an object (let’s say a button) that is inside of a TitleWindow or Panel, the Callout’s position is off by the height of the Panel’s header. So the Callout is 20+ pixels above the button. Any ideas?

Thanks!

10.08.2009 / Aaron Hardy said:

@Ron
Thanks for trying it out! I’ve updated the source with a fix for your issue. I ran into a similar issue on a current project of mine and patched it, but hadn’t got around to updating the source in the example app. Hopefully that works for you. It should handle positioning better for scaled and/or rotated components as well. Be sure to clear your cache before checking out the source again. Good luck and let me know if you run into any other snags.

10.09.2009 / Ron Rebennack said:

Awesome! I love it when someone maintains their code & pays attention to their blogs.

Thanks for the fix.

01.03.2010 / Aaron Fay said:

Simple. Awesome.

Aaron

02.12.2010 / Sean Wesenberg said:

Hey this is great stuff.

If anyone wants to add a drop shadow, here is what I did…

in Callout.as updateDisplayList method add the following…

            //shadow
            var filter:BitmapFilter = getBitmapFilter();
            var myFilters:Array = new Array();
            myFilters.push(filter);
            filters = myFilters;

now, also in Callout.as add this method to the class…

        private function getBitmapFilter():BitmapFilter {
          var color:Number = 0x000000;
          var angle:Number = 45;
          var alpha:Number = 0.6;
          var blurX:Number = 8;
          var blurY:Number = 8;
          var distance:Number = 3;
          var strength:Number = 0.65;
          var inner:Boolean = false;
          var knockout:Boolean = false;
          var quality:Number = BitmapFilterQuality.HIGH;
          return new DropShadowFilter(distance, angle, color, alpha, blurX, blurY, strength, quality, inner, knockout);
        }

Obviously, you can change the values and tailor towards your needs. This creates a seamless drop-shadow around the tail (tailSkin) and the content area (bubbleSkin). There are some performance considerations that you might want to read up on the DropShadowFilter docs. This does not create a rectangular dropshadow, so it is more ‘expensive’ to draw out. But again, seems to work for me. Caution to all internet posted code. :P

02.20.2010 / Brian said:

I had some problems when using htmlText and getting the tool tip to resize properly – The solution is found on this blog http://idletogether.com/automatically-resize-texttextarea-based-on-content-autosize-in-flex/ – just use Text instead of TextArea (unless you just want a text area) and may have to set a fixed width to make sure it wraps properly

02.20.2010 / Aaron Hardy said:

Thanks for contributing!

03.09.2010 / TomM said:

Aaron,

Have you ported this to Flash 4?

Tom

03.10.2010 / Aaron Hardy said:

@Tom, I think you meant Flex 4–unless you’re old school like that. No, it hasn’t been ported yet. Which essentially means I’d love to but it’s low on my todo list at this point. If someone else wants to attempt a port, I’d be glad to post it and give full props.

05.20.2010 / Ian said:

Hi,

I’m new to Flex and have been playing around with the callout sample, using it for tooltips. I have one question about it though. How do you set the callout item to appear at your current mouse coordinates?

05.20.2010 / Aaron Hardy said:

@Ian
To start off, check out the function showCoordinateCallout() in the demo. Notice that it takes creates a callout and sets the anchorPoint property to an arbitrary point. This point is in the stage coordinate space. So all you need to do is make that point reflect the position of your mouse within the stage. For example, if you wanted to show the callout on a MOUSE_CLICK event, you could add an event listener on the stage (or any other interactive display object) for the MOUSE_CLICK event and when the event handler is called, you could create a point using:

new Point(event.stageX, event.stageY);

and set that as the anchorPoint property for the callout. Let me know if you run into trouble.

05.21.2010 / Ian said:

Thanks for the reply.
I have tried setting the anchorPoint property to my current mouse pointer x and y.
For some reason I keep getting some random results. Sometimes it displays the tip exactly on the mouse coordinates, but other times its far to the right of my pointer.
Any ideas?

05.21.2010 / Aaron Hardy said:

Go ahead and post some code. Make sure the point is relative to the top-left of the stage. If you’re using a mouse event, you’ll need to use stageX and stageY from the event and not mouseX and mouseY. mouseX and mouseY are relative to the display object you clicked on, not the stage.

05.24.2010 / Ian said:

I draw sprites on the screen and add a EventListener to each.
Then I have the callout with 2 labels on it.
Here is the code I use.

sprObject.addEventListener(MouseEvent.ROLL_OVER, onOver);
sprObject.addEventListener(MouseEvent.ROLL_OUT, onOut);
 
private function onOver(event:MouseEvent):void {
	var list:XMLList = XMLData.children();
 
	for each (var itemList:XML in list)
	{	
		if (event.target.name.toString() == itemList.name.toString()) {
			break;
		}
	}
 
	var strName:String = itemList.name.toString();
	var strActive:String = itemList.active.toString();
 
	showToolTip(strName ,strActive, event);
 
}
 
private function showToolTip(strText:String,strText2:String,event:MouseEvent):void
{
	var selectPoint:Point = localToGlobal(new Point(event.stageX, event.stageY));
 
	callout = new Callout();
	var contentBox:VBox = new VBox();
	contentBox.setStyle("horizontalAlign", "center");
	contentBox.setStyle("backgroundColor", 0xD3D3D3);
	contentBox.setStyle('borderStyle', 'solid');
	contentBox.setStyle("bordercolor", 0x000000);
	contentBox.setStyle("borderThickness", 2);
 
	var contentLabel:Label = new Label();
	contentLabel.text = "Block - " + strText;
	contentLabel.setStyle("color", 0xff0000);
 
	var contentLabel2:Label = new Label();
	contentLabel2.text = strActive;
	contentLabel2.setStyle("color",  0x00FF00);
 
	contentBox.addChild(contentLabel);
	contentBox.addChild(contentLabel2);
	callout.content = contentBox;
	callout.anchorPoint = new Point(selectPoint.x, selectPoint.y);
	callout.show(DisplayObjectContainer(event.target));				
 
}

05.24.2010 / Aaron Hardy said:

You don’t want to use localToGlobal in your showToolTip function. event.stageX and event.stageY are already global. So if you take a global point and infer that it’s actually a local point (which it’s not) by converting it from the local coordinate space to your global coordinate space, you’ll potentially end up with a point that’s right-and-down from your actual mouse point.

05.26.2010 / Ian said:

I have remove the localToGlobal part and is using the stageX and Y fom the mouse. But unfortunately I’m still getting the same result. For some of my points the mouse pointer is correct – but for others it shows to the top right.

Just more information about what I’m doing.
I draw sprites on a , when I move my mouse over a specific sprite I need some information displayed in the tooltip.

05.26.2010 / Aaron Hardy said:

Feel free to send me your project (or a simplified version) and I’ll look it over.

05.28.2010 / Aaron Hardy said:

Folks, this component was built for Flex 3, not Flex 4. If you want to use it in Flex 4, you’ll need to replace this function:

private static function classConstruct():Boolean {
	var styleDeclaration:CSSStyleDeclaration = StyleManager.getStyleDeclaration('Callout');
 
	if (!styleDeclaration) {
		styleDeclaration = new CSSStyleDeclaration();
	}
 
	styleDeclaration.defaultFactory = function():void {
		this.tailOffset = 0;
		this.tailSize = 30;
		this.paddingTop = 10;
		this.paddingRight = 10;
		this.paddingBottom = 10;
		this.paddingLeft = 10;
		this.preferredTailPosition = TAIL_POSITION_BOTTOM_LEFT;
		this.forcePreferredTailPosition = false;
		this.backgroundColor = 0xFFFFFF;
		this.backgroundAlpha = 1;
	}
 
	StyleManager.setStyleDeclaration('Callout', styleDeclaration, false);
 
	return true;
}

with this:

private static function classConstruct():Boolean {
	var styleDeclaration:CSSStyleDeclaration = 
			FlexGlobals.topLevelApplication.styleManager.getStyleDeclaration(
					'com.aaronhardy.callout.Callout');
 
	if (!styleDeclaration) {
		styleDeclaration = new CSSStyleDeclaration();
	}
 
	styleDeclaration.defaultFactory = function():void {
		this.tailOffset = 0;
		this.tailSize = 30;
		this.paddingTop = 10;
		this.paddingRight = 10;
		this.paddingBottom = 10;
		this.paddingLeft = 10;
		this.preferredTailPosition = TAIL_POSITION_BOTTOM_LEFT;
		this.forcePreferredTailPosition = false;
		this.backgroundColor = 0xFFFFFF;
		this.backgroundAlpha = 1;
	}
 
	FlexGlobals.topLevelApplication.styleManager.setStyleDeclaration(
			"com.aaronhardy.callout.Callout", styleDeclaration, false);
 
	return true;
}

Even then it’s not truly Flex 4-ized, but it will work.

05.28.2010 / Ian said:

Awesome, Thank you for all your help. That change worked 100%

08.09.2010 / Paul Michael said:

This is just what I need! Thank you. :)


Leave a Comment

Your email address is required but will not be published.




Comment