Spark TextFlow LinkElement Rollover
This is a little example of how to create a skinned rollover popup on a LinkElement object in Flash Builder 4 (Flex 4) within a TextFlow object. This is something I have been waiting for since 2001. The old way to create a rollover action on an HTML link was to create an invisible MovieClip that floats over your hyperlink. Flex Builder 4 greatly simplifies this by adding rollover, mouse, and click events to all hyperlinks within the TextFlow object. This is a HUGE plus
I borrowed the click event action from an example by Peter deHaan posted on his blog Flex Examples. You will need the latest build of the Flex Builder 4 SDK and some helpful instructions on how to install the latest Flex Builder 4 SDK.
Example AIR Application Output

Main.mxml
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication title="Spark RichEditableText LinkElement Rollover"
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/halo">
<fx:Script>
<![CDATA[
import flashx.textLayout.formats.TextDecoration;
import mx.managers.PopUpManager;
import flashx.textLayout.elements.LinkElement;
import flashx.textLayout.events.FlowElementMouseEvent;
import mx.controls.Alert;
private var customToolTip:CustomToolTip;
protected function clickHandler(evt:FlowElementMouseEvent):void {
var linkEl:LinkElement = evt.flowElement as LinkElement;
Alert.show("The '" + linkEl.getFirstLeaf().text + "' link would have gone to " + linkEl.href + " in a " + linkEl.target + " window, but it was cancelled.", "Fun with hyperlinks");
evt.stopImmediatePropagation();
evt.preventDefault();
}
protected function rollOverHandler(evt:FlowElementMouseEvent):void
{
if(!customToolTip){
customToolTip = new CustomToolTip();
customToolTip.x = this.mouseX - customToolTip.width/2;
customToolTip.y = this.mouseY - 40;
PopUpManager.addPopUp(customToolTip, this, false);
}
customToolTip.text = LinkElement(evt.flowElement).href;
}
protected function rollOutHandler(evt:FlowElementMouseEvent):void
{
PopUpManager.removePopUp(customToolTip);
customToolTip = null;
}
]]>
</fx:Script>
<s:RichEditableText id="richEdTxt" editable="false" selectable="false" focusEnabled="false" horizontalCenter="0" verticalCenter="0">
<s:textFlow>
<s:TextFlow>
<!--
<s:linkNormalFormat textDecoration="{TextDecoration.NONE}"/>
<s:linkHoverFormat textDecoration="{TextDecoration.UNDERLINE}" />
<s:linkActiveFormat textDecoration="{TextDecoration.UNDERLINE}" />
-->
<s:p>Text that includes a link to <s:a href="http://adobe.com/" target="_blank"
rollOver="rollOverHandler(event);" rollOut="rollOutHandler(event);"
click="clickHandler(event);">Adobe</s:a>.</s:p>
</s:TextFlow>
</s:textFlow>
</s:RichEditableText>
</s:WindowedApplication>
CustomToolTip
<?xml version="1.0" encoding="utf-8"?>
<s:SkinnableContainer xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/halo">
<fx:Script>
<![CDATA[
import spark.skins.spark.BorderSkin;
[Bindable] private var _text:String;
public function set text(value:String):void
{
_text = value;
}
]]>
</fx:Script>
<s:Skin minHeight="25">
<s:Rect id="upFill"
top="1"
right="1"
left="1"
bottom="1"
radiusX="10"
radiusY="10">
<s:fill>
<s:LinearGradient rotation="90">
<s:GradientEntry color="#222222" ratio="0" alpha="0.45"/>
<s:GradientEntry color="#222222" ratio="0.45"/>
<s:GradientEntry color="#222222" ratio="0.65"/>
<s:GradientEntry color="#222222" ratio=".8"/>
</s:LinearGradient>
</s:fill>
<s:stroke>
<s:SolidColorStroke color="#222222" weight="0.5"/>
</s:stroke>
</s:Rect>
<s:Rect id="highlightFill"
top="2"
right="2"
left="2"
bottom="2"
radiusX="10"
radiusY="10">
<s:stroke>
<s:LinearGradientStroke rotation="90" weight="1">
<s:GradientEntry color="#FDFDFD" ratio="0" alpha="0.6"/>
<s:GradientEntry color="#FDFDFD" ratio="0.2" alpha="0"/>
</s:LinearGradientStroke>
</s:stroke>
</s:Rect>
<s:SimpleText id="labelElement" text="{_text}"
color="#FFFFFF"
right="20"
left="20"
verticalAlign="middle"
horizontalCenter="0"
verticalCenter="1"/>
<s:filters>
<s:DropShadowFilter color="#999999"
blurX="5"
blurY="5"
angle="90"
distance="2"
alpha="0.8"/>
</s:filters>
</s:Skin>
</s:SkinnableContainer>
The only think I couldn’t get working for this example (the commented out section) was changing the format of the hyperlinks within the TextFlow object.
Additional Resources
http://labs.adobe.com/technologies/flashbuilder4/
http://livedocs.adobe.com/flex/gumbo/langref/spark/primitives/RichEditableText.html
http://blog.flexexamples.com/2009/08/27/creating-a-linkelement-in-a-spark-richeditabletext-control-in-flex-4/
http://blog.flexexamples.com/2009/10/21/customizing-the-appearance-or-a-hyperlink-in-a-textflow-object-in-flex-4/
http://blog.flexexamples.com/2009/07/13/downloading-and-installing-flex-4-sdk-builds-from-opensource-adobe-com-flash-builder-4-beta-edition/
-Mister
Flex Garbage collection and styles
I read a really great post in my endless search for better garbage collection in AIR and Flex. Bernd Bindreiter of firstrow RIA posted a 5-part series on garbage collection and one of them involves removing skins from Button controls in order to have them garbage collected properly. In his post he removes each individual skin from the button using something like:
myButton.setStyle("overSkin", null);
myButton.setStyle("upSkin", null);
myButton.setStyle("downSkin", null);
What I wondered was if I could do the same thing by just setting the styleName to null:
myButton.sytleName = null;
This does work for skins, but as Bernd’s comments point out, other styles such as “color, fontSize, paddingTop” don’t create memory leaks, so this method only works with skins. However, setting the styleName to null is also an effective way for marking the object ready to be garbage collected. I recommend reading the rest of his posts about garbage collection techniques. I am always searching for the holy grail of garbage collection and his posts were very insightful.
-Mister
Adobe MAX 2009 – Los Angeles
This year Adobe Max 2009 is in my own backyard, well sort of 6 miles up the street, which means about an hour away in LA traffic. I am hoping to get in for free some how, either through Adobe Max Awards or by posting the Adobe Max Widget. See everyone there, maybe…
Adobe AIR TextField bug when resetting HTML text.
I ran into a really strange issue that only occurs within Adobe AIR applications (AIR 1.5.1/1.5.2). When I create a TextField control, set it’s style, and then assign it HTML text it appears fine. However, if I wish to reset that TextField content and the original content has links, the entire replaced content becomes one HTML link. I think the issue can be better explained with some images and some code samples.
Read more…
Truncating HTML Text
With Flex truncating text within a Label control is easily done by setting the property “truncateToFit” to true. This parameter doesn’t do much when using the Text or TextArea controls in Flex. To truncate text in these controls you would have to build your own function to count characters and add the ellipses. There is one good example of creating your own custom Text control that truncates the text in the same manner as the Label control based on size of the control.
Read more…
Using AIR 1.5.1 InovkeReason for friendlier applications
I recently discovered a new feature of AIR 1.5.1 that makes AIR applications more consumer friendly in certain situations. When you have your application set to start on user login using “NativeApplication.nativeApplication.startAtLogin = true”, you may want the application to start as a background process rather than show the entire application at login. In AIR 1.5.1 there is a new property of the InvokeEvent called “reason” that will report if the application was started at login or was started when the users invokes the application by launching the application.
Read more…
Adding Mac OS X Close Behavior to AIR Application
Just a quick post because I have seen this done on other sites but it no longer is valid in AIR 1.5. Most Mac OS X applications minimize to system tray on closing the application using the “x” close button on the app. To achieve this in AIR 1.5 you will need to do the following on creationComplete event of the main application:
// on application creation complete setup default Mac OS X behavior
private function init():void
{
if (NativeApplication.supportsMenu) { // we are on a Mac
//this.nativeWindow.addEventListener(InvokeEvent.INVOKE, handleAppInvoke);
NativeApplication.nativeApplication.addEventListener(InvokeEvent.INVOKE, handleAppInvoke);
this.nativeWindow.addEventListener(Event.CLOSING, handleAppHide);
NativeApplication.nativeApplication.addEventListener(Event.EXITING, handleAppExit);
NativeApplication.nativeApplication.autoExit = false;
} else { // we are on a Windows machine
this.nativeWindow.addEventListener(Event.CLOSING, handleAppClosing);
NativeApplication.nativeApplication.autoExit = true;
}
}
// active that application
private function handleAppInvoke(event:InvokeEvent):void
{
this.nativeWindow.activate();
}
// close all open windows
private function handleAppClosing(event:Event):void
{
event.preventDefault();
for (var i:int = NativeApplication.nativeApplication.openedWindows.length - 1; i >= 0; --i) {
NativeWindow(NativeApplication.nativeApplication.openedWindows[i]).close();
}
}
// hide the application instead of closing it
private function handleAppHide(event:Event):void
{
if( this.nativeWindow.visible ) { // if the window is visible the event behaviour is canceld and the window is hidden
event.preventDefault();
this.nativeWindow.visible = false;
}
}
// close all open windows, remove event listener for hiding application and close applicatin
private function handleAppExit(event:Event):void
{
this.nativeWindow.removeEventListener(Event.CLOSING, handleAppHide);
for (var i:int = NativeApplication.nativeApplication.openedWindows.length - 1; i >= 0; --i) {
NativeWindow(NativeApplication.nativeApplication.openedWindows[i]).close();
}
this.close();
}
- Mister
Garbage Collection and Flex Event Listeners
On a recent AIR project I have become aware and subsequently obsessed with memory. My concerns have centered around garbage collection and event listeners, specifically how you clean up event listeners to allow Garbage Collection (GC) to clean up objects. Most DisplayObjects implement the EventDispatcher class that allows it to broadcast events. Trolling around the googleverse I found some interesting ways to insure that you break strong references to allow clean up or manually remove event listeners on your own to make your application more memory savvy.
When you use addEventListener() method and register a event listener to an object you create reference. By default any object that has a reference to a listener will keep that reference until it is removed using the removeEventListener() method. Strong references will not be cleaned up by Flash player GC and remain in memory until the application is closed or the world ends, whichever comes first. This makes for a horribly leaky AIR applications, especially those applications that may run for days on the user’s system without being restarted.
Use Additional Parameters of Listener
The addEventListener() method does have some additional (and seldom used) parameters to create a weak reference when adding the listener to an object. On of those parameters is “useWeakReference”, the 5th parameter of the method. To utilize this parameter you need to set it from false to true:
addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false);
.....
var button:Button = new Button()
button.addEventListener(MouseEvent.click, handleMouseClick, false, 0, true);
private function handleMouseClick(event:MouseEvent):void
{
// handle the event
}
This will allow the event to be cleaned up at some point by Flash garbage collection. If you are like me and and find it annoying that the useWeakReference parameter of the addEventListener() method is false by default, you can can extend the object and make useWeakReference true by default, then use that object in your project:
package com.custom.controls
{
import mx.controls.Button;
public class CustomButton extends Button
{
public function CustomButton()
{
super();
}
override public function addEventListener(type:String, listener:Function, useCapture:Boolean=false, priority:int=0, useWeakReference:Boolean=true):void
{
super.addEventListener(type, listener, useCapture, priority, useWeakReference);
}
}
}
Explicitly Removing Listeners
Even though setting useWeakReference=true will most likely mark an object for clean up, to be even more certain you should explicitly remove the event listener with the removeEventListener() method:
private var button:Button;
private function init()
{
var button:Button = new Button();
button.addEventListener(MouseEvent.click, handleMouseClick, false, 0, <strong>true</strong>);
}
private function handleMouseClick(event:MouseEvent):void
{
button.removeEventListener(handleMouseClick);
button = new Button();
// handle the event
}
Now you are certain that you have cleaned up and removed the listener manually and the object will be removed at some point during GC since there is no longer an explicit reference to the object.
Anonymous Functions
Another technique that comes in handy is to removing event listeners when using anonymous functions as event handlers. The trick to this techniques is the arguments.callee:
var button:Button = new Button()
button.addEventListner(MouseEvent.CLICK, function(event:MouseEvent):void {
event.currentTarget.removeEventListener(event.type, arguments.callee);
});
Bummer
Now for the disappointment. If you create an event listener using MXML they are always going to have strong reference by default with no way to change useWeakReference and they make it difficult to use removeEventListener() method:
private function handleClickEvent(event:Event):void
{
// handle event
}
<mx:Button click="handleClickEvent(event)"/>
So these objects will not be cleaned up when created in this way unless you have some clean up method that explicitly removes the event listeners or you add the event listeners to MXML object using ActionScript instead of binding.
So basically you need to add objects or listeners in ActionScript rather than use MXML when using the addEventListener() method, which sort of makes MXML less valuable in some respects because binding to objects and listening to events is what drives the framework.
Additional Links
For a great discussion on the topic, see Ted Patricks post and be sure to read all the comments as many of the examples were taken from discussion.
Garbage Collection Articles:
http://spreadingfunkyness.com/garbage-collection-with-flex-and-adobe-air/
http://blogs.adobe.com/aharui/2007/03/garbage_collection_and_memory.html
http://gskinner.com/blog/archives/2006/08/as3_resource_ma_2.html
http://www.adobe.com/devnet/flashplayer/articles/resource_management.html