Sunday, September 09, 2007

AutoScrolling for Flex Tree

We've been mostly fixing bugs on the SDK as we prepare for the release for MAX, and one of the bugs that was assigned to me was SDK-9645, https://bugs.adobe.com/jira/browse/SDK-9645. The problem was that in a Tree (and other List-based components), when you set the horizontalScrollPolicy to auto, the scrollbars actually don't come out when they should. This seems like a bug at first, but we did this by design for performance reasons. In order to display the scrollbar properly, we need to measure the width of all the items (on-screen or not) and this would just take too much time to do by default. So instead, to get a scrollbar to show up, you need to set maxHorizontalScrollPosition, which is how much the user can scroll. However, this can be annoying, especially as you might not know how big your data is. So I've created a simple component that extends Tree that measures the width of all the renderers so the scrollbars are calculated correctly. In order to do this, in updateDisplayList, which is called whenever we need to redraw, I call measureWidthOfItems, which calculates the max width for all the item renderers. This means that redrawing takes some more time and is a little slower. Anyways, here's the code:
package{
    import flash.events.Event;
   
    import mx.controls.Tree;
    import mx.core.mx_internal;
    import mx.core.ScrollPolicy;
    import mx.events.TreeEvent;
   
    public class AutoSizeTree extends Tree
    {
         public function AutoSizeTree(){
              super();
              horizontalScrollPolicy = ScrollPolicy.AUTO;
         }
        
         // we need to override maxHorizontalScrollPosition because setting
         // Tree's maxHorizontalScrollPosition adds an indent value to it,
         // which we don't need as measureWidthOfItems seems to return exactly
         // what we need.  Not only that, but getIndent() seems to be broken
         // anyways (SDK-12578).
        
         // I hate using mx_internal stuff, but we can't do
         // super.super.maxHorizontalScrollPosition in AS 3, so we have to
         // emulate it.
         override public function get maxHorizontalScrollPosition():Number
         {
              if (isNaN(mx_internal::_maxHorizontalScrollPosition))
                  return 0;
              
              return mx_internal::_maxHorizontalScrollPosition;
         }
   
         override public function set maxHorizontalScrollPosition(value:Number):void
         {
              mx_internal::_maxHorizontalScrollPosition = value;
              dispatchEvent(new Event("maxHorizontalScrollPositionChanged"));
           
              scrollAreaChanged = true;
              invalidateDisplayList();
         }
        
         override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
         {
              // we call measureWidthOfItems to get the max width of the item renderers.
              // then we see how much space we need to scroll, setting maxHorizontalScrollPosition appropriately
              var diffWidth:Number = measureWidthOfItems(0,0) - (unscaledWidth - viewMetrics.left - viewMetrics.right);
             
              if (diffWidth <= 0)                     maxHorizontalScrollPosition = NaN;                else                     maxHorizontalScrollPosition = diffWidth;                                     super.updateDisplayList(unscaledWidth, unscaledHeight);           }      } }
[Code updated 3/20/09] Because we're doing this extra calculation, it means for a Tree with a lot of items, we may become slower. There are better ways of doing the same thing besides measuring every single renderer in updateDisplayList(), but this is definitely the easiest way. If performance is really a problem with this, let me know, and I'll try to come up with something a little smarter. Below is an example, with the top one being the new AutoSizeTree component and the bottom one being the old one. Notice how the top is a little slower when resizing with an effect (this shows it's a little slower when laying out in general) when lots of the items are open. [Post updated on 3/20/09 to fix an issue: SDK-18499]

47 comments:

lmurphy said...

I happily discovered your AutoSizeTree as I was struggling with horizontal scroll issues with the Tree component. Using your code creates the horizontal scroll behavior I was looking for.

However, even though the horizontal scroll bar appears correctly and I can scroll to the right, the text of the items is chopped off (clipped). The visibility of items corresponds with the original width (without scroll bar) of the Tree control - anything to the right of the original width does not display.

I then used a regular Tree component and set horizontalScrollPolicy="auto" and maxHorizontalScrollPosition="900" (a large number to force scrolling). The same behavior occurs.

I am currently using Flex Builder 3.

Any suggestions?

Ryan said...

I think your issue's related to SDK-11847 .

This is fixed in the version I'm using, but you can always take the fix and put it in your code (email me if you want to do this), or you can download a nightly build from us at Adobe Labs or wait for Beta 2 to be released (coming in the next week!).

Peter Kehl said...

Ryan,

I made it work by turning off scrolling policy on my AutoResizeTree and puting it into a scrollable HBox. Then in my AutoResizeTree I have:

override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
this.width= measureWidthOfItems();
this.height= measureHeightOfItems();

super.updateDisplayList(unscaledWidth, unscaledHeight);
}

That resizes the tree OK and the scrollable HBox scrolls fine. However, I don't see a way to get rid of the tree's scrollbars - although the sliders are not there. Any ideas, please?

Ryan said...

If you set horizontalScrollPolicy to off, you shouldn't get any hints about a scroll bar.

I would try setting the width in measure() rather than in updateDisplayList(). As long as you have an explicit width on the parent HBox, I think that would work.

Ryan said...

So I investigated it a little more, and in Flex 2.01, this works. Just make updateDisplayList(), the following:

var diffWidth:Number = measureWidthOfItems(0,0) - (unscaledWidth - viewMetrics.left - viewMetrics.right);

if (diffWidth <= 0)
{
maxHorizontalScrollPosition = NaN;
horizontalScrollPolicy = ScrollPolicy.OFF;
}
else
{
maxHorizontalScrollPosition = diffWidth;
horizontalScrollPolicy = ScrollPolicy.ON;
}

super.updateDisplayList(unscaledWidth, unscaledHeight);


Again, this is fixed in the newest beta of Flex 3, but for people with 2.01, this is probably the easiest way to fix the problem ppl keep describing.

Jürgen said...

This was really helpful and timely, Ryan. I was banging my head against the wall as the tree in my application was refusing to use scrollbars.

Having said this, I do have a major problem and a minor one:

The solution works great on Windows Platforms but no text is showing on Mac and Linux flash players. The tree is functional and the blue bars indicate indicate the currently selected item and highlight possible entries when hovered over them. But no text or graphic is showing.
The minor issue is that the the text width calculation does not seem to take the vertical scrollbar into consideration. I do have tree text showing through the vertical scroll bar.

I am using the Flex Builder 3 Eclipse plugin, but need to use for compatibility a Flex 2.0.1 SDK (HotFix 3 to be precise). I do use the second version (the one you posted 9/29/2007 1:02 AM )

You can see yourself on the production site at http://fotoflot.com and select the “Take a tour” button.

Thanks for the help,
Jürgen

Jürgen said...

Quick update: When I do not override “maxHorizontalScrollPosition” (e.g., leave out that entire function) it runs well on the Linux and Windows flash players. Won't be able to test Mac until some time tomorrow.

Running that version on fotoflot.com now.

Jürgen

Ryan said...

Hey Jurgen,

Sorry, but I really don't know what would cause the platform differences. I'll try it on a MAC at work.

digcode said...

thx )

Peter said...

hello all,

first of all a big respect for this blog.

Got also problems with the vertical scrolling….

Here is nice tutorial for a tree with amfPHP (http://www.sephiroth.it/tutorials/fl…fphp/index.php)

i’ve used it for my app, but running into some trouble now, with the scrollbars, what i really didn’t expect!!

here you can see the app and the source code:
http://www.rootop.de/testing/TestTree.html

it’s all german, so when you click on Enomis -> Einzelstoffe->Obst , then there appears a scrollbar for a short time and then disappears again, even if there are more branches… it really makes me mad.. i tried serveral function like..

//scroll always to bottom, otherwise, only the items are shown, that are in the visible area
tree.verticalScrollPosition=tree.maxVerticalScroll Position;

tree.invalidateList();

tree.validateNow();

but nothing really works. the general problem is, that i don’t know when flex display the scrollbar and when not. because in some cases it works.
so, if there is a solution or a workaround, i would be very happy. thanks in advance. kind regards, peter

Ryan said...

It's probably due to the lazy-loading part. If you're using Flex 3 codebase (a lot of stuff was fixed around this b/w Flex 2.01), then please file a bug for it at http://bugs.adobe.com/flex/. You could try something like verticalScrollPolicy="on" to see if that helps, but I'm not sure without fooling around with it some more.

metaine said...

Ryan, when i click "shrink new", scroll slider appears in a wrong place.
I'm using flash player v. 9,0,124,0

Anonymous said...

It seems that the horizontal scrollbar position is not correct when items labels are calculated through a labelFunction returning strings containing html tags. This is as if html tags were taken into account when calculating tree width, while this should not be the case (calculated width should be text width with html applied).

This is working fine while using the Tree class.

Greg said...

First, thanks for help with horizontal scrolling, much appreciated.

I am also having issues testing on Mac, no text appears in tree. Do you know how I can resolve this?

Thanks,

Greg

samuel said...

Hello,

Waht about vertical Scrollbar. I' ve got the same bug when adding leaf on the fly

Ryan said...

Hey samuel,

Vertical scrollbars should work automatically. If they're not showing up and they should, please file a bug at http://bugs.adobe.com/flex/.

You may ask why are vertical scrollbars different than horizontal ones? The reason is for vertical stuff, we need to figure out what to show on screen, and we do all the vertical (and horizontal) measuring of what's on screen. But we don't touch the offscreen rows for performance reasons. Knowing how many items are shown and how big a row is, we can figure out vertical scrollbars. Horizontal scrollbars are different because we've got no idea how long all rows are (one particularly long row could be off-screen, and we don't want to measure off-screen rows). So we could measure the width of items on-screen, but if you scrolled, you'd see the horizontal scrollbar change, which would be funky behavior. So this is why we decided to try to do something smart, but leave it up to the developers because we don't want everyone hit with any performance issues.

Aleksey said...

Ryan,
If a vertical scroll bar is automatically added and then removed from this component (verticalScrollPolicy is set to "auto") and the horizontal scroll bar is automatically added with the help of your code, the nodes are not clipped properly and overlap the component's right border...
Please help

Ryan said...

Hey Aleksey,

Not sure what you mean. It could be a bug. If you post some sample code/steps, I'll try to take a look at it.

-Ryan

Aleksey said...

Ryan,
Thanks for your response. I posted a sample here:

http://www.alekseyvays.com/tree/TreeTest.html

If you expand and then collapse all nodes, you'll see what I'm talking about. The source files are here, just in case:

www.alekseyvays.com/tree/TreeTest.zip

Again, Thank You,
-Aleksey

Sabarish Sasidharan said...

Thanks for the Flex 2.0.1 fix, it worked for me.

Aleksey said...

What's in the Flex 2.0.1 fix?
Another way to reproduce the bug in your example is:
1. Expand the root node
2. Collapse the root node
3. Press 'shrink new autosizer' button
The node text ends up being clipped to the right of the tree:(

Anonymous said...

This code fails in Flash Player 10, in modules that reside in the PopUpManager. It still works on trees in the main application, but fails in the popped up modules.

The tree is there with nodes, but you can no longer see them, yet if you have drag set on you can drag them and stuff, very odd.

Anonymous said...

I have a problem with the vertical scrollbar. I am having a problem when I update the tree dataprovider with new data on the run. When the scrollbar is visible and if I scroll to the bottom of the tree and scroll back to the top, the first node in the tree disappears.


After the dataprovider update I do the below
tree.invalidateList()
tree.invalidateDisplaylist()

Even after doing all the above sometimes I see that the tree area displays old nodes over the new nodes.

Any inputs suggestions will be greatly helpful

Thanks in advance,
Flex user.

JabbyPanda said...

There is a new bug submitted in Adobe JIRA related to the issue that mx:Tree component renders incorrectly in Flash player 10 when 2 conditions are met
1) this particular code proposed by Ryan is used together with a mx:Tree component.
2) mx:Tree is displayed inside PopUpWindow

Anonymous said...

The vertical scrollbar has the same problem. when you open the children item, even the list overflows, but the vertical scrollbar did not enabled, if you close/open the children item again. Then the scrollbar works. Any suggestions about how to fix the problem?
Thanks,

Ryan said...

The bug a lot of people seem to be running into are with clipContent. I haven't looked at the treeCode in a while, but take a look where clipContent is being set to true/false. Also take a look at where the scrollbars are being set to visible/invisible as that's what is triggering it.

Anonymous said...

Recently implemented this, and there is are 2 very small changes that need to be made for both fp9 and fp10 support.


FP9/10 Issue: Text will completly disapear in some cases.
Fix: change
maxHorizontalScrollPosition = NaN;
to
maxHorizontalScrollPosition = 0;

Issue: Text getting chopped of at end.
Fix: change
maxHorizontalScrollPosition = diffWidth;
to
maxHorizontalScrollPosition = diffWidth + 10;
or what ever correction factor you need.

website design nyc said...

nice post

Ryan said...

Thanks for filing the bug and leaving the comments. The post has since been updated and should be working properly.

Scott said...

Has anyone figured out how to clip the tree content correctly after the vertical scrollbar has been added and then removed?

I can't figure out how to keep the tree items from extending onto the parent after this.

Thanks!

Hans Nuttin said...

clipping problem is caused by the addClipMask() method, it does not take the visibility of the scrollbar in consideration, the following should fix this:

override mx_internal function addClipMask(layoutChanged:Boolean):void
{
var vm:EdgeMetrics = viewMetrics;

if (horizontalScrollBar && horizontalScrollBar.visible)
vm.bottom -= horizontalScrollBar.minHeight;

if (verticalScrollBar && verticalScrollBar.visible)
vm.right -= verticalScrollBar.minWidth;

listContent.scrollRect = new Rectangle(
0, 0,
unscaledWidth - vm.left - vm.right,
listContent.heightExcludingOffsets);
}

Thiago Colares said...

I'm using a menu Tree-component-based. Does anybody knows how to implement an autoscroll? I mean, when you open a folder that is positioned at the last visible line, its children apears hidden by the limits of the component. Then the user have to scroll down manually to see it. Is there a way to make it auto?? Thanks! And congratulations for the blog!

HJ Chen said...

When I tried to override the addClipMask to fix the clipping problem, I got a compilation error "1004: Namespace was not found or is not a compile-time constant." This compilation bug has been documented in ASC-3728 and the workaround is defining "use namespace" outside the class definition.

...
use namespace mx_internal;

public class myTree extends Tree
{
...
override mx_internal function addClipMask(layoutChanged:Boolean):void
{
}
}

Ryan said...

I haven't looked at the addClipMask issue, but the issue you're running in to is that you're probably not importing the mx_internal namespace. At the top, add an import for:

import mx.core.mx_internal;

HJ Chen said...

Hi Ryan,

Actually I did importing the mx_internal namespace but still got the compilation error.

Janak Jivani said...

Hi Ryan/Chen,
I got the same error for clipping while overriding "addClipMask". please help me out if you solved it.
Thank you.

Brandon said...

I've got an issue w/ this component. In the example @ the top do this to repro:

Click shrink new autosize tree
Expand 1st node
Expand 3rd node to add vertical scrollbar
Collapse 3rd node to get rid of vertical scrollbar

You should now see that the long names extend outside of the tree component when there's no vertical scrollbar.

Thoughts on how to fix this?

Brandon said...

Nevermind - I noticed that the clipping issue discussed above was the same issue I was having and the proposed solution of overriding addClipMask worked great.

Anonymous said...

Hello Ryan,

because we have a lot of items in our tree (>1000), the measureWidthOfItems is not fast enough to calculate the maximum width.
Have you got a better solution to fasten the width calculation?

Thanks a lot.

Wannes

Ryan said...

Wannes,

Having over 1000 items will always be slow. There's no good way to measure all of these items at once without being slow. That is why the default behavior in Flex doesn't do this extra work--by default we are optimizing for performance. What I'd suggest is to set the maxHorizontalScrollPosition to a specific value as just a guess of what you would need.

-Ryan

Anonymous said...

Thanks for the advice.

We solved the problem a bit by caching the width of each item.

So we override the measureWidthOfItems and add the measured items to a dictionary.

Something like:

if (cacheNodeWidth && itemWidthMap[data]) {
rw = itemWidthMap[data];
} else {
var factory:IFactory = getItemRendererFactory(data);
item = measuringObjects[factory];

if (!item) {
item = getMeasuringRenderer(data);
}

item.explicitWidth = NaN; // gets set in measureHeightOfItems
setupRendererFromData(item, data);

rw = item.measuredWidth;

if (cacheNodeWidth)
itemWidthMap[data] = rw;
}
w = Math.max(w, rw);
}


Note: If the labelitems or the structure of the tree nodes is dynamic, you need to update your dictionary.

Wannes

Anonymous said...

Thank you so much for the solution. It worked like a charm and I'd been beating my head against the wall about this for hours. Thanks again !!

Anonymous said...

Hi Ryan,

First of all, thanks for the great post.

I'm also struggling with the horizontal scroll issue and in my case your solution didn't work as expected.

My case is that I'm using a TreeRenderer which adds more objects to the node (checkBox, images, etc... ). The point is that the measureWidthOfItems ignores this extra elements, so the scroll bar only appears to make sure the labels are displayed, but not the other elements...

Any idea of how to make this for this case??

Thanks!

Ryan said...

It's been a while since I've looked at this code, but measureWidthOfItems() should create a normal renderer and figure out how big it is--it shouldn't just ask for the label's width. Are you sure that's the issue?

Anonymous said...

Hi again Ryan, it's me again.

Thanks for your quick answer.

I've been looking more about this issue, and I discovered that in fact, what actually measures is what we set as LabelField. It ignores whatever you put as children of the item with the renderer, it will act as if only a label with the labelField content was placed.

If you want more information about how looks my treeItemRenderer, I'm based on the checkTree like the one in this link http://bearly.in/Flex_3_Tree_Control_with_Radio_Button_and_Checkbox_Controls

Any idea about how to change this behaviour in this function?

Ryan said...

In CheckTreeRenderer, just as you override updateDisplayList(), you need to override measure(). In there you should take in to account all of the children in your item renderer. So something like:

measuredWidth = myCheckBox.x + myCheckBox.getExplicitOrMeasuredWidth() + 17;

There should be quite a few articles on measure() and updateDisplayList(). I'd take a look at them.

Hope that helps,
Ryan

Anonymous said...

Hi!

I had no idea about the existence of Measure method in TreeItemRenderer....

Now it works perfect. I just override the function, call super.measured and then increase the measured width with the extra elements added, because besides the checkbox, I have other images added or not depending on the data, so its not a constant value.

Thanks a lot Master!.