The Flex3 Tree control is easy to use for a simple file listing. However, when you want to use more complex node displays, the tree gets a little more complicated. By default, the Tree control supports folderOpenIcon, folderClosedIcon and defaultLeafIcon. However, the tree can display much more complex node listings with a little work.
In our sample application, purposely simplified for brevity, we display a set of folders that contain image files or additional subfolders. The folder display is much like the default folder listing, but the image files will show an image thumbnail and a file name. Keep in mind, this node display could contain much more elaborate HTML. In the example, the tree nodes are bound to the tree through an XMLList.
The application itself is very simple; it is a tree control. Some of the attributes (shown in bold) of the tree control are important for our purposes. itemRenderer indicates a custom ItemRenderer used to prepare each node for display. Without this attribute, the default ItemRenderer is used. iconFunction indicates a custom function for specifying icons for each node. This is useful in cases where you want to show different icons for different file or folder types. In our example, we will use this to determine what, if any, icon will be used. These two custom functions are discusseed below. A third attribute, variableRowHeight, indicates whether each node in the tree has the same height or not. We have set this to true, because in our case the height will varying between folders and image files. If the height will not vary, set this attribute to false (default), because there is some overhead associated with variable row heights. The rest of the code is mostly the XML data used to populate the tree. Note that the image fiiles are ordered from 0-9 and then a-f. This will be important later.
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
backgroundColor="white"
xmlns:cls="classes.*" >
<mx:Tree id="tree1"
labelField="@label"
dragEnabled="true" dropEnabled="true"
width="200" height="200" itemRenderer="classes.boxItemRenderer"
verticalScrollPolicy="auto" iconFunction="treeIconFunction"
borderColor="black" borderThickness="1" variableRowHeight="true" >
<mx:XMLListCollection id="tree1Data" >
<mx:XMLList>
<treenode label="node t1.1" >
<treeleaf label="leaf t1.1-A" thumbsource="images/0.gif" />
<treeleaf label="leaf t1.1-B" thumbsource="images/1.gif" />
</treenode>
<treenode label="node t1.2">
<treenode label="node t1.2.1" >
<treeleaf label="leaf t1.2.1-A" thumbsource="images/3.gif" />
</treenode>
</treenode>
<treenode label="node t1.3">
<treenode label="node t1.3.1" >
<treeleaf label="leaf t1.3.1-A" thumbsource="images/4.gif" />
<treeleaf label="leaf t1.3.1-B" thumbsource="images/5.gif" />
<treeleaf label="leaf t1.3.1-C" thumbsource="images/6.gif" />
</treenode>
<treenode label="node t1.3.2" >
<treeleaf label="leaf t1.3.2-A" thumbsource="images/7.gif" />
<treeleaf label="leaf t1.3.2-B" thumbsource="images/8.gif" />
<treeleaf label="leaf t1.3.2-C" thumbsource="images/9.gif" />
</treenode>
<treenode label="node t1.3.3" >
<treeleaf label="leaf t1.3.3-A" thumbsource="images/a.gif" />
<treeleaf label="leaf t1.3.3-B" thumbsource="images/b.gif" />
<treeleaf label="leaf t1.3.3-C" thumbsource="images/c.gif" />
</treenode>
<treenode label="node t1.3.4" >
<treeleaf label="leaf t1.3.4-A" thumbsource="images/d.gif" />
<treeleaf label="leaf t1.3.4-B" thumbsource="images/e.gif" />
<treeleaf label="leaf t1.3.4-C" thumbsource="images/f.gif" />
</treenode>
</treenode>
</mx:XMLList>
</mx:XMLListCollection>
</cls:boxTree>
<mx:Script>
<![CDATA[
private function treeIconFunction(item:Object):Class
{
var iconClass:Class;
// Embed icons.
[Embed(source="images/folder.png")]
var folderIcon:Class;
switch (XML(item).localName())
{
case "treenode":
iconClass = folderIcon;
break;
case "treeleaf":
break;
}
return iconClass;
}
]]>
</mx:Script>
</mx:Application>
In the script section of our application is the treeIconFunction that coincides with the iconFunction attribute in our Tree declaration. This iconFunction is simple. If the node is a treenode (folder), the folderIcon will be used. If the node is a treeleaf (image file), no icon will be used. For treeleafs, other elements will be added to the node display in our boxItemRenderer.
Custom ItemRenderers are somewhat complex compared to most elements in Flex. ItemRenderers, even if by a different name, is a fairly common concept for tree and list displays in other languages. And like Flex, the renderer tends to be fairly complex yet flexible. Flex's ItemRenderer has a few quirks of its own. In the boxItemRenderer code below, the measure() and updateDisplayList(unscaledWidth, unscaledHeight) functions are important for setting the height, width, and position of elements in each node display. These functions are discussed in many places across the web, but often not in a clear and concise way. These functions, although important, are not the focus of today's discussion. Perhaps I can detail these functions in a future post. Our interest is in the data function.
In the data function, we will build our custom node display if the node is a treeleaf node. The image thumbnail will be placed in an element called "iconImage". If the node associated with this data (and this instance of the boxItemRenderer) is a treeleaf (isLeaf()), and hte iconImage does not exist, create one. Once the iconImage is created, we tell it where to find the image file (from the bound XML data), and we add the iconImage to the node's control tree. If the node is not a treeleaf, then we remove the iconImage from this instance. This may seem strange. Why do we remove the iconImage if we did not add it in the top of the if block? This is due to one of the quirks of the Flex TreeItemRenderer. Flex reuses ItemRenderers, but the number of ItemRenderer instances used depends the size of the tree display. Flex will create enough ItemRenderers for each displayed node plus one or two instances just above and below the scrollable view area. Therefore, we must assume the ItemRenderer has been used previously. So, if an iconImage exists in the control tree for a treenode (not treeleaf), remove it.
package classes
{
import mx.controls.Image;
import mx.controls.Text;
import mx.controls.Tree;
import mx.controls.treeClasses.*;
public class boxItemRenderer extends TreeItemRenderer
{
protected var _tree:Tree;
public function boxItemRenderer()
{
super();
}
override public function set data(value:Object):void
{
super.data = value;
var iconImage:Image = super.getChildByName("iconImage") as Image;
if (isLeaf())
{
if(!iconImage)
{
iconImage = new Image();
iconImage.name = "iconImage";
iconImage.source = data.@thumbsource;
addChild(iconImage);
}
}
else {
if (iconImage) {
super.removeChild(iconImage);
iconImage = null;
}
}
invalidateDisplayList();
}
override protected function measure():void
{
super.measure();
if (isLeaf()) {
var iconImage:Image = super.getChildByName("iconImage") as Image;
if (iconImage) {
iconImage.width = 32;
iconImage.height = 32;
measuredHeight = iconImage.height;
}
}
}
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
if( super.data )
{
data as XML;
if (isLeaf())
{
var iconImage:Image = super.getChildByName("iconImage") as Image;
if (iconImage) {
iconImage.height = iconImage.getExplicitOrMeasuredHeight();
if (unscaledHeight == 0) unscaledHeight = iconImage.height;
this.label.height = iconImage.height;
iconImage.x = Math.max(this.label.x - iconImage.width - 2, 2);
}
}
}
}
// Determines if node type is leaf
private function isLeaf():Boolean
{
return ('treeleaf' == XML(super.data).name().localName.toLowerCase());
}
}
}
Our sample application is shown below. This is a very simple tree listing. Expand each node and see either image file nodes with thumbnails of additional subfolders. Expand enough folders to cause the vertical scroll bar to appear, and then expand some more. Then move the scroll bar up and down. If you recall from above, our images were ordered in the list of nodes from 0-9 and a-f. However, after expanding the nodes and scrolling up and down, what has happened to our image displays? The images are in hte wrong order, or more accurately, the wrong image is with the correct node.
These image refernce errors are a direct result of ItemRenderer reuse by Flex. No only does Flex reuse ItemRenderers, it reuses dirty (with data) ItemRenderers. Therefore, we need to provide some more clean up of the ItemRenderer before we can use it. To do this, we make a small modification to the data(value) function of our boxItemRenderer class. If the node is a folder, we are removing any existing iconImage, so no worries. If the node is an image file, and an iconImage already exists, we must assume it is for the wrong image. In this case we reset the image source value (bold code below). This will display the correct image with the correct node when ItemRenderers are reused.
override public function set data(value:Object):void
{
super.data = value;
var iconImage:Image = super.getChildByName("iconImage") as Image;
if (isLeaf())
{
if(!iconImage)
{
iconImage = new Image();
iconImage.name = "iconImage";
iconImage.source = data.@thumbsource;
addChild(iconImage);
}
else {
iconImage.source = data.@thumbsource;
}
}
else {
if (iconImage) {
super.removeChild(iconImage);
iconImage = null;
}
}
invalidateDisplayList();
}
The application below is the same as the first example, except we added the clean-up code for image files on reused ItemRenderers. Expand the nodes and confirm that the images remain correct after scrolling.
cd116725-58e1-4a9a-ac89-c13e972c5c15|2|5.0