Friday, October 16, 2009

New & Improved TreeItemRenderer Example


I have written two posts (here and here) on creating variable size item renderers for List based components that size correctly. The example for the TreeItemRenderer mostly works, but still has some issues that have been plaguing me for a while. I decided to spend some time trying to figure out what the problem really is and made significant progress that I wanted to share.

TreeRenderer Example Version 2

I have made some changes to the original TreeRendererExample that significantly improve its behavior. The example code is hosted on the Flexnaut googlecode site under the TreeRendererExample2 project.

Enhancement 1

The original custom renderer was a lot more complicated than it needed to be. I was adding a resize event listener to the tree (for each renderer) that set the Text component's width/height to NaN. The measure() override looked like this:
override protected function measure():void
  {
    super.measure();
    
    description.width = explicitWidth - super.label.x;
    measuredHeight += description.measuredHeight;
  }

Then in updateDisplayList I would set the height of the Text component to it's measuredHeight. In the new version, I don't set any resize listener on the Tree component and I don't do set any properties on the Text component. I just use the Text component's size to calculate the size of the renderer:
override protected function measure():void
  {
    super.measure();
    
    measuredHeight = measuredMinHeight = measuredHeight description.getExplicitOrMeasuredHeight();
  }

Then, in updateDisplayList, I do the following to size the Text component:
//This forces the Text component to calculate it's height
  description.width = width - description.x;

  //This sets the Text component to the calculated width & height
  description.setActualSize( description.getExplicitOrMeasuredWidth(), description.getExplicitOrMeasuredHeight() );

With these changes, everything almost works correctly. The item renderers all size correctly, but the problem is that the renderers seem to overlap each other now. This condition can be detected in the renderer updateDisplayList by comparing the unscaledHeight parameter to the measuredHeight parameter. It looks like there is a bug in ListBase which causes it to not detect changes in the height of the renderers. Fortunately, the fix is simply to add this line to updateDisplayList:
if ( unscaledHeight != measuredHeight ) callLater( ListBase(owner).invalidateList );

This causes the protected property itemsSizeChanged on ListBase to be set to true, and invalidates the display list. The callLater is necessary, because updateDisplayList can't be called while the current updateDisplayList is running.

With these changes, we now have a much simpler and functional TreeItemRenderer with a Text component. The following demonstrates the improved behavior:



Unable to initialize flash content

Original Broken Version - Scroll and click around to see broken behavior


Unable to initialize flash content

New Version - Scroll and click works as expected

Check out the source!

4 comments:

Unknown said...

Thanks for the great posts. I used it to help me make a distinct item renderer for various node. I.e. in this case it is for a normal tree node in the tree and for a datagrid for each leaf node.

private function init(event:Event):void {
/* Manually position the controls. */
if(data.datalist==null || data.datalist==false){
var lbl:CounterpartiesDataRendererLabel = new CounterpartiesDataRendererLabel();
placeHolder = lbl;
lbl.data = data;

} else {
var dg:CounterpartiesDataRendererDataGridPaged = new CounterpartiesDataRendererDataGridPaged();
placeHolder = dg;
dg.data = data.list;
var par:Object = dg.parent;
}
moveIt( placeHolder );
this.addChild(placeHolder);
}

Aqeel Aslam said...

Thanks for sharing the knowledge. I am trying to add two buttons on every leaf node of a tree.
I want to perform a specific action against each of them.
I tried to use your source code and replaced your description component with a button throughout the code. But I am getting the following error: "ArgumentError: Error #2004: One of the parameters is invalid.
at flash.display::Graphics/drawRoundRect()
....
at
mx.core::UIComponent/callLaterDispatcher()[E:\dev\3.0.x\frameworks\projects\framework\src\mx\core\UIComponent.as:8403]"

Could you please help me how to add two buttons on the right side of every leaf node of a tree in Flex?

Thanks

Kelly Davis said...

Aqeel,

I think you would be better off looking at the ExtensibleTreeItemRenderer example (http://flexnaut.blogspot.com/2009/10/extensible-list-item-renderers.html). It is geared to be more generic. Use the ActorTreeItemRenderer as an example. I was very easily able to add buttons to it without any issues. If you have any issues with that, let me know. It might also be good to see your source code.

Benji said...

I almost can't believe this worked! I've spent more than a day (!) trying to figure out a problem - I have a DataGrid with item renderer that contains Text in it. All looks very nice ... initially. After some time if the browser is inactive (maybe even if it's active) the height of each renderer gets recalculated - and the new size doesn't contain the height of the Text control. Which causes rows in the table to overlap. And after moving the scroll a bit everything gets back to normal. So adding this to updateDisplayList fixed it:

if ( unscaledHeight != measuredHeight ) {
callLater( ((ListBase)(ListBaseContentHolder(parent).parent).invalidateList ));
}

Thank you so much for sharing this!