September 10th, 2008 by Kyle
Tags: bubble, datatip, Flex, speech bubble, tooltip
Posted in: ActionScript, Flex
I recently had a request to help show an approach for creating a custom “speech bubble” tooltip.
Here is a sample that should help demonstrate the approach.
There are docs on creating custom tooltips and that is essentially what my sample is based upon:
http://livedocs.adobe.com/flex/3/html/help.html?content=tooltips_1.html
The next challenge was to customize the border. I mimicked what was described in the docs:
http://livedocs.adobe.com/flex/3/html/help.html?content=tooltips_1.html
The docs show you how to customize the tooltip border, but since my custom tooltip extends Panel instead, I need to use the skin for the Panel border.
(thus I am using mx.skins.PanelSkin) It is also useful to look at what the parent of that class is doing (mx.skins.halo.HaloBorder).
I basically stole the “tail drawing” bit from the tooltip skin though and just patched it on to the end of the drawBorder method in the PanelSkin.
You may want to make this more robust, but this is just the general approach. (Add logic to determine what side to draw the tail on, what color the tail should be, etc.)
Download a zipfile containing the source to this sample.
Browse the source of this example.
Or continue into the blog entry to see the source:
Here is the app code:
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script><![CDATA[
import mx.events.ToolTipEvent;
private function createCustomTip(title:String, body:String, event:ToolTipEvent):void {
var ptt:PanelToolTip = new PanelToolTip();
ptt.bodyText = body;
ptt.title=title;
event.toolTip = ptt;
}
private function positionTip(event:ToolTipEvent):void{
event.toolTip.x=event.currentTarget.x + event.currentTarget.width + 10;
event.toolTip.y=event.currentTarget.y;
}
]]></mx:Script>
<mx:Style>
PanelToolTip {
borderSkin: ClassReference("MyPanelSkin");
}
</mx:Style>
<mx:Button id="b1"
label="Delete"
toolTip=" "
toolTipCreate="createCustomTip(‘DELETE’,'Click this button to delete the report.’, event)"
toolTipShow="positionTip(event)"
/>
<mx:Button id="b2"
label="Generate"
toolTip=" "
toolTipCreate="createCustomTip(‘GENERATE’,'Click this button to generate the report.’, event)"
toolTipShow="positionTip(event)"
/>
<mx:Button id="b3"
label="Stop"
toolTip="Click this button to stop the creation of the report. This button uses a standard ToolTip style."
/>
</mx:Application>
Here is the custom tooltip:
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml"
implements="mx.core.IToolTip"
width="200"
alpha=".8"
borderThickness="2"
backgroundColor="0xCCCCCC"
dropShadowEnabled="true"
borderColor="black"
borderStyle="solid"
roundedBottomCorners="true"
cornerRadius="10"
horizontalAlign="center"
>
<mx:Script><![CDATA[
[Bindable]
public var bodyText:String = "";
// Implement required methods of the IToolTip interface; these
// methods are not used in this example, though.
public var _text:String;
[Bindable]
public function get text():String {
return _text;
}
public function set text(value:String):void {
}
]]></mx:Script>
<mx:Text text="{bodyText}" percentWidth="100"/>
<mx:Image source="@Embed(‘monkey_w_bananna.jpg’)" scaleContent="true" scaleX=".5" scaleY=".5"/>
</mx:Panel>
Here is the custom borderskin not my changes near the comment starting with “KyleQ”):
//
// ADOBE SYSTEMS INCORPORATED
// Copyright 2007 Adobe Systems Incorporated
// All Rights Reserved.
//
// NOTICE: Adobe permits you to use, modify, and distribute this file
// in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////
package
{
import flash.display.GradientType;
import flash.display.Graphics;
import flash.utils.getQualifiedClassName;
import flash.utils.describeType;
import mx.core.IContainer;
import mx.core.EdgeMetrics;
import mx.core.FlexVersion;
import mx.core.IUIComponent;
import mx.skins.halo.HaloBorder;
import mx.core.mx_internal;
use namespace mx_internal;
/**
* The PanelSkin class defines the skin for the Panel, TitleWindow, and Alert components.
*/
public class MyPanelSkin extends HaloBorder
{
// include "../../core/Version.as";
/**
* Constructor
*/
public function MyPanelSkin()
{
super();
}
/**
* @private
*/
private var oldHeaderHeight:Number;
/**
* @private
*/
private var oldControlBarHeight:Number;
/**
* @private
* Internal object that contains the thickness of each edge
* of the border
*/
protected var _panelBorderMetrics:EdgeMetrics;
/**
* @private
* Return the thickness of the border edges.
*
* @return Object top, bottom, left, right thickness in pixels
*/
override public function get borderMetrics():EdgeMetrics
{
if (FlexVersion.compatibilityVersion < FlexVersion.VERSION_3_0)
return super.borderMetrics;
var hasPanelParent:Boolean = isPanel(parent);
var controlBar:IUIComponent = hasPanelParent ? Object(parent).mx_internal::_controlBar : null;
var hHeight:Number = hasPanelParent ? Object(parent).mx_internal::getHeaderHeightProxy() : NaN;
var newControlBarHeight:Number;
if (controlBar && controlBar.includeInLayout)
newControlBarHeight = controlBar.getExplicitOrMeasuredHeight();
if (newControlBarHeight != oldControlBarHeight &&
!(isNaN(oldControlBarHeight) && isNaN(newControlBarHeight)))
_panelBorderMetrics = null;
if ((hHeight != oldHeaderHeight) &&
!(isNaN(hHeight) && isNaN(oldHeaderHeight)))
_panelBorderMetrics = null;
if (_panelBorderMetrics)
return _panelBorderMetrics;
var o:EdgeMetrics = super.borderMetrics;
var vm:EdgeMetrics = new EdgeMetrics(0, 0, 0, 0);
var bt:Number = getStyle("borderThickness");
var btl:Number = getStyle("borderThicknessLeft");
var btt:Number = getStyle("borderThicknessTop");
var btr:Number = getStyle("borderThicknessRight");
var btb:Number = getStyle("borderThicknessBottom");
// Add extra space to edges (was margins).
vm.left = o.left + (isNaN(btl) ? bt : btl);
vm.top = o.top + (isNaN(btt) ? bt : btt);
vm.right = o.bottom + (isNaN(btr) ? bt : btr);
// Bottom is a special case. If borderThicknessBottom is NaN,
// use btl if we don’t have a control bar or btt if we do.
vm.bottom = o.bottom + (isNaN(btb) ?
(controlBar && !isNaN(btt) ? btt : isNaN(btl) ? bt : btl) :
btb);
// Since the header covers the solid portion of the border,
// we need to use the larger of borderThickness or headerHeight
oldHeaderHeight = hHeight;
if (!isNaN(hHeight))
vm.top += hHeight;
oldControlBarHeight = newControlBarHeight
if (!isNaN(newControlBarHeight))
vm.bottom += newControlBarHeight;
_panelBorderMetrics = vm;
return _panelBorderMetrics;
}
/**
* @private
* If borderStyle may have changed, clear the cached border metrics.
*/
override public function styleChanged(styleProp:String):void
{
super.styleChanged(styleProp);
if (styleProp == null ||
styleProp == "styleName" ||
styleProp == "borderStyle" ||
styleProp == "borderThickness" ||
styleProp == "borderThicknessTop" ||
styleProp == "borderThicknessBottom" ||
styleProp == "borderThicknessLeft" ||
styleProp == "borderThicknessRight" ||
styleProp == "borderSides" )
{
_panelBorderMetrics = null;
}
invalidateDisplayList();
}
/**
* @private
*/
override mx_internal function drawBorder(w:Number, h:Number):void
{
super.drawBorder(w,h);
if (FlexVersion.compatibilityVersion < FlexVersion.VERSION_3_0)
return;
var borderStyle:String = getStyle("borderStyle");
if (borderStyle == "default")
{
// For Panel/Alert, "borderAlpha" is the alpha for the
// title/control/gutter area and "backgroundAlpha"
// is the alpha for the content area.
// We flip-flop the variables here so the "borderAlpha"
// is applied by the background drawing code at the bottom.
var contentAlpha:Number = getStyle("backgroundAlpha");
var backgroundAlpha:Number = getStyle("borderAlpha");
backgroundAlphaName = "borderAlpha";
radiusObj = null;
radius = getStyle("cornerRadius");
bRoundedCorners =
getStyle("roundedBottomCorners").toString().toLowerCase() == "true";
var br:Number = bRoundedCorners ? radius : 0;
var g:Graphics = graphics;
drawDropShadow(0, 0, w, h, radius, radius, br, br);
// If we don’t have rounded corners we need to initialize
// the complex radius object so the background fill code
// below works correctly.
if (!bRoundedCorners)
radiusObj = {};
var parentContainer:IContainer = parent as IContainer;
if (parentContainer)
{
var vm:EdgeMetrics = parentContainer.viewMetrics;
// The backgroundHole is the content area
backgroundHole = {x:vm.left, y:vm.top,
w: Math.max(0, w – vm.left – vm.right),
h: Math.max(0, h – vm.top – vm.bottom),
r:0};
if (backgroundHole.w > 0 && backgroundHole.h > 0)
{
// Draw a shadow around the content
// if the content and panel alpha are different.
// This could be a style property if needed
if (contentAlpha != backgroundAlpha)
{
drawDropShadow(backgroundHole.x, backgroundHole.y,
backgroundHole.w, backgroundHole.h,
0, 0, 0, 0);
}
// Fill in the content area
g.beginFill(Number(backgroundColor), contentAlpha);
g.drawRect(backgroundHole.x, backgroundHole.y,
backgroundHole.w, backgroundHole.h);
g.endFill();
}
}
// When the content and panel alpha are different, the border
// of the panel is drawn using borderColor. We’ve already
// drawn the content background so we set backgroundColor to
// borderColor here so the drawing code below is done with the
// border color.
}
// KyleQ: draw the tail at the top left side of the tooltip.
var gr:Graphics = graphics;
gr.beginFill(0×000000, 1);
gr.moveTo(x, y + 7);
gr.lineTo(x-11, y + 13);
gr.lineTo(x, y + 19);
gr.moveTo(x, y + 7);
gr.endFill();
}
/**
* @private
*/
override mx_internal function drawBackground(w:Number, h:Number):void
{
super.drawBackground(w,h);
if (getStyle("headerColors") == null && getStyle("borderStyle") == "default")
{
var highlightAlphas:Array = getStyle("highlightAlphas");
var highlightAlpha:Number = highlightAlphas ? highlightAlphas[0] : 0.3;
// edge
drawRoundRect(
0, 0, w, h,
{ tl: radius, tr: radius, bl: 0, br: 0 },
0xFFFFFF, highlightAlpha, null,
GradientType.LINEAR, null,
{ x: 0, y: 1, w: w, h: h – 1,
r: { tl: radius, tr: radius, bl: 0, br: 0 } });
}
}
/**
* @private
*/
override mx_internal function getBackgroundColorMetrics():EdgeMetrics
{
if (getStyle("borderStyle") == "default")
return EdgeMetrics.EMPTY;
else
{
return super.borderMetrics;
}
}
/**
* We don’t use ‘is’ to prevent dependency issues
*/
static private var panels:Object = {};
static private function isPanel(parent:Object):Boolean
{
var s:String = getQualifiedClassName(parent);
if (panels[s] == 1)
return true;
if (panels[s] == 0)
return false;
if (s == "mx.containers::Panel")
{
panels[s] == 1;
return true;
}
var x:XML = describeType(parent);
var xmllist:XMLList = x.extendsClass.(@type == "mx.containers::Panel");
if (xmllist.length() == 0)
{
panels[s] = 0;
return false;
}
panels[s] = 1;
return true;
}
}
}
Tweet
13 Comments »

October 21st, 2008 at 5:24 am
Thanks for the tut! Was wondering if you knew a way to override the auto datatip for say 1 lineseries of a Cartesain chart? Basicly so you could add an extra line to auto created datatip.
November 7th, 2008 at 12:53 pm
Ken you can use the dataTipFunction on the chart to customise the content for the datatips.
November 21st, 2008 at 11:57 am
Just came up with an optimization for your MyPanelSkin:
public class MyPanelSkin extends HaloBorder
{
override mx_internal function drawBorder(w:Number, h:Number):void
{
super.drawBorder(w,h);
var gr:Graphics = graphics;
gr.beginFill(0x000000, 1);
gr.moveTo(x, y + 7);
gr.lineTo(x-11, y + 13);
gr.lineTo(x, y + 19);
gr.moveTo(x, y + 7);
gr.endFill();
}
}
December 2nd, 2008 at 10:52 am
Hi
great article!
i have a question too:
how can you change the tooltip’s visibility from another class?
Eg i want a custom logic when to show tootips, like show if the button’s x coordinate is >100
January 1st, 2009 at 1:05 pm
I can’t tell you how useful these examples are for we noobs. Thanks you!
Though the comment above it says otherwise, your skin code assumes the border color is black though:
gr.beginFill(0×000000, 1);
I’m retrieving it dynamically:
var borderColor:uint = getStyle(“borderColor”);
…
gr.beginFill(borderColor, 1);
February 4th, 2009 at 11:46 pm
I’m searching for something fairly specific and your example is the closest I’ve found yet. I’m most interested in the ability to place an image inside a ToolTip. Very cool stuff. I have all of the code examples in place but my difficulty is that my ToolTips are tied to images in a four-column DataGrid instead of a button.
My DataGrid uses the itemRenderer and passes text to it. I evaluate the text by this._listData.label and set this.toolTip=”whatever”; based on what text was passed to the itemRenderer to begin with. It doesn’t let me state this.toolTipCreate or this.toolTipShow as a button or image normally would.
Any idea how to get it to work? Essentially, I’m building an IM front-end that when rolling over the name, displays information about the user in a tooltip. Or at least that’s my hope.
February 5th, 2009 at 1:14 am
While not the perfect solution, I did find a way to cheat.
Create a canvas box to meet the size requirements and populate it with the desired fields. Then when I mouseOver the appropriate dataGrid column, it dynamically fills out the canvas with the data associated with the highlighted person. When I mouseOut, it hides the canvas. It’s about as much like a toolTip as it can be without actually being one.
February 25th, 2009 at 11:02 pm
Good work! Here’s an alternative if anyone’s looking for something different:
http://aaronhardy.com/flex/advanced-callout-component/
February 26th, 2009 at 12:07 pm
Great stuff Aaron!
July 28th, 2009 at 3:01 am
Wow Grate Grate Grate way to understand
Really very very usefully this article
very very thanks for this
really grate
Thanks
atul singh Parihar
September 10th, 2009 at 7:02 am
Hi i have a problem this if you do horizontalAlign=”right” verticalAlign=”bottom”
then cut the tooltip so plz give me a good with out bug example
i hope u reply as soon as possible
thanks
March 10th, 2010 at 10:12 am
I have been struggling with this for some time and I endud writting my own:
http://www.jollant.net/SpeechBubble/BubbleSpeechTest.html
May 10th, 2010 at 9:33 am
[...] http://blog.flexmonkeypatches.com/2008/09/10/flex-custom-tooltip-speech-bubble/comment-page-1/ [...]