Monday, November 19, 2007

Adding the mx.controls.Text component to a custom TreeItemRenderer

Update: This post has a much improved example of how to do this properly. I highly recommend going there now. :)

I recently had to build a custom TreeItemRenderer which displayed an icon and a label. I found Mac Martine's example to be quite helpful in implementing it successfully. More recently I needed to extend this work to add a mx.controls.Text component to the renderer, to display a variable length description under the label. I initially believed that this would be a simple task, but it turns out to be quite a challenge.

My first strategy to accomplish this was to implement a new TreeItemRenderer from scratch without extending mx.controls.treeClasses.TreeItemRenderer. I wanted to do this because I believed I could very quickly lay it out in MXML. The problem is, that the default TreeItemRenderer implements the expand/collapse controls for the item and does so by accessing a property (isOpening) and method (dispatchTreeEvent) which are both scoped mx_internal. So without some extra gymnastics, I am better off extending the default TreeItemRenderer.

I reverted back to my modified implementation of Mac Martine's custom renderer. I had to modify the existing updateDisplayList() method override and implement the measure() method override. In updateDisplayList(), I basically just need to position the Text component under the existing Label component. In measure(), I need to make the renderer taller to accomodate the height of the additional Text component. The problem is that the Text component renderering code is complex and a seemingly inconsistent in how it behaves. If you don't set the width and height properties in just the right order, it won't render correctly. After several hours of trial and error, I was able to get it to behave properly. Here is basically what I did.

1) In measure(), I set the width property of the Text component to the width of the item renderer less the x position of the label. Then I read the measuredHeight property of the Text component which is calculated based on the length of the text/htmlText property of the component and the explicit width of the component, and I add that to the measuredHeight of my renderer.

2) In updateDisplayList, after positioning all of my components, I explicitly set the height of the Text component to the measuredHeight property of the Text component. This seems like an odd thing to have to do, but if I don't do it, the text doesn't display at all.

If you do these things, your Text will display and your renderer will be sized properly. There is one other important thing to do if your renderers are going to be different sizes, which is to set the variableRowHeight property on the Tree to true.

Additionally, if your Tree component will need to be resizable, there is something else that needs to be done to get the Text component to resize correctly. When the width and height of a Text component are both explicitly set, the component doesn't call measure() anymore, so it won't resize. To fix this, we need to set the explicitWidth & explicitHeight on the Text component to NaN. So we add a listener to the parent Tree to listen for ResizeEvents, and when they occur, set the explicitHeight and explicitWidth to NaN. This will then cause Text component to resize properly.

I have created an example application with source code (right click the example and select View Source). It consists of an MXML application and a custom renderer. Hopefully this will save you so some time.