Wednesday, May 21, 2008

localToGlobal vs. contentToGlobal in Flex

If you hadn't heard, there are a set of functions in Flex called contentToGlobal and globalToContent that Sean Christmann and I discovered this afternoon doing some hacking.  If you're ever using globalToLocal or localToGlobal, you probably should be using the analagous "content" versions instead.  Here's why.

Let's say your application consists of two red boxes.  One of them is on your root or Application level stage, and the other is nested in a component who's x position is set to 5.  The Application level box is placed at x=10.  Let's say we want to make it so that both red boxes are at the same X position.  Since the box that contains the child is already 5 pixels from the left, we set our second red box's x to 5, combining for a total of 10.

Rather than doing these calculations by hand, Flex has a set of helpful methods that make it easy.  Traditionally I would do this using the "globalToLocal" and "localToGlobal" methods.  To calculate the X value using these methods we'd traditionally do something like this inside of our child container:

//get the application level red box's x coordinate
var box1_x : int = Application.application.box1.x;

//convert this x value into a global coordinate
var pt : Point = new Point(box1_x, 0);
var box1_global_x : int = Application.application.localToGlobal(pt).x;
//note: since box 1 is on the "global" stage, localToGlobal shouldn't do anything.
//I've just included it here for completeness.

//convert the global coordinate into our container's coordinate system
var pt2: Point = new Point(box1_global_x, 0);
var final_value : int = this.globalToLocal(pt2).x;

These five steps convert the box's x value into a global point and then back from global into our container's local coordinate system.  Setting box 2's x value to our "final_value" gives the desired result, as shown above.

The problems start showing up when your child's container uses a border.  Let's say we give our container a border thickness of 2.  If we run the code above to set our box's x position, we wind up with something like this:

Our second box renders displaced by a number of pixels equal to the border thickness from where we want it.

This is because Flex containers place their children inside a nested container called a "contentPane."  Content panes are separate containers that have their own coordinate systems - think of it as a box within a box.  In the outer box the component renders borders.  All children and their content are renderered in the inner box - the content box.  globalToLocal and localToGlobal calculations use the container's root coordinate system (the "outer box").  Since children are rendered inside of the content pane, globalToLocal type calculations really aren't "local" to these children.

The answer is to use contentToGlobal and globalToContent instead.  Replacing localToGlobal and globalToLocal in our previous example, the red box renders correctly, like this:

If you don't have any borders or padding then the two boxes line up and localToGlobal / globalToLocal works just fine, but if you do have them, these methods will fail you because they're based on the "outer box" coordinate system and not the content pane.


John Cook said...

Gotta love Flex nuances. Thanks for the great tip RJ!

Ryan Bell said...

Another thing worth noting in using localToGlobal (and the reverse), one that gave me fits until I figured it out: It's tempting to call localToGlobal on the object whose global position you're trying to determine, when in reality you need to call it on that object's parent container, assuming you're passing it the object's X and Y values.

This is probably a total newbie mistake, and your initial example shows it done correctly, but I thought it worth pointing out for those of us who are still getting stuck on simpler stuff ;-)

Anonymous said...

further to the previous post you can call localToGlobal on the object itself passing 0, 0 to find out where that object is, this is exactly the same as calling the parent with the x and y of the object, but it makes figuring out where corners other than upper left are slightly easier, for example if you want to know where the dead center of the object is pass width/2, height/2, this saves you having to add width/2 to x etc.

RJ said...

Great points guys.

For me the important thing with localToGlobal is to think of it in terms of coordinate spaces instead of points. Calling object.localToGlobal(point); translates the point from within object's coordinate space to the global space. For this reason, child.localToGlobal(zero_point); gives the child's global position (it's origin translated to global space) and childContainer.localToGloba(childPt); does the same thing.

Balaji said...

A Small help from you, I am getting the values from xml in percentages, as x="10%". I need to calcualte this and place the value which is there in the layout. Plase let me know how to do this,,

Anonymous said...

Thanks guys, that was exactly what i was looking for and couldn´t find anything with gugling for "get absolute position flex" ;)

swav said...

This is great info, much obliged!

Mike Graf said...

Great post. Thanks a lot.

Cyril Ronseaux said...

------ Beware of Scale :

When you want to know where the "top left" of a component is on the global stage, that's ok to go with : component.localToGlobal(zeroPoint).

But as soon as you want say the center or the bottom right corner : component.localToGlobal(new Point(component.width, component.height)) does work only if component content is not scaled.

Component.width is using its parent space. So I prefer the approach : component.parent.localToGlobal( component.topLeft )
component.parent.localToGlobal( bottomRight )

------ Question about Scrolls

I have two containers (A and B), on top of each other (think view stack), both inside a third container (C) which is scrollable.

A contains various nested containers, until a container D where user is adding Shapes. Now I want to take coordinate of one of the shapes and convert it to draw a copy of the shape on top of B.

I'm using :
sourceSystem is D (shape's parent)
targetSystem is B

var pointInSourceSystem:Point = new Point(shape.x, shape.y);
var pointInGlobalSystem:Point = d.contentToGlobal( pointInSourceSystem );
var pointInTargetSystem:Point = b.globalToContent( pointInGlobalSystem );

If I'm doing this, when the whole piece (A) is scrolled, my shape copy is not on top of the original shape :(. If I manually add scrolling positions to copy "pointInTargetSystem" is works... Why the hell ?