KIRN TECH JUL 28 · 3 MIN READ

HeroAnimation. How to get a widget position?

Hero animation is done between two different layout positions of the HeroAnimation widget, it encapsulates the animation process which is done by moving the HeroFly widget between the old and new positions of the HeroStill widgets.
HeroAnimation example
Please check the Article about how HeroAnimation works.

To get the HeroStill layout position we'll try to use localToGlobal() of its RenderBox.

The [size] of each RenderBox is expressed as a width and a height. Each box has its own coordinate system in which its upper left corner is placed at (0,0). The lower right corner of the box is therefore at (width, height). The box contains all the points including the upper left corner and extends to, but not including, the lower right corner.

For the first test case, we use an offset from the root node:

final rect = localToGlobal(Offset.zero);
And it works as expected, check the example ^^.

But when we try to enhance the test scenario a little bit by putting it in a Scaffold we run into an issue when the HeroFly widget lands in the wrong position.
HeroFly was made visible after the animation ends for the bug demo.
Actually, Scafold is with an AppBar, which gives that extra dy:56 offset. We can check that from the visual representation of the render tree in DevTools.
From the source code, we may notice that localToGlobal includes that extra dy:56 in a result because it traverses the whole render object tree and sums up the child-parent offset of each node.

To understand better how it works and how to fix it let's implement our own render object tree traversal and sum up offsets for each node:

node.localToGlobal(node.parent).

Offset globalPosition() {

    var accumulatedOffset = Offset.zero;
    AbstractNode? node = this;

    while (node != null) {
      if (node is RenderBox) {
        AbstractNode? ancestor = node.parent;
        while (ancestor != null) {
          if (ancestor is RenderObject) {
            break;
          }
          ancestor = ancestor.parent;
        }

        if (ancestor != null) {
          final offset = node.localToGlobal(Offset.zero, ancestor: ancestor as RenderObject);

          accumulatedOffset += offset;
        }
      }
      node = node.parent as RenderObject?;
    }

    return accumulatedOffset;
}
node visit:

HeroStillRenderObject:           Offset(0.0, 0.0),     summed: Offset(0.0, 0.0)
RenderConstrainedBox:            Offset(0.0, 171.5),   summed: Offset(0.0, 171.5)
RenderFlex:                      Offset(4.0, 4.0),     summed: Offset(4.0, 175.5)
RenderPadding:                   Offset(623.5, 407.0), summed: Offset(627.5, 582.5)
RenderStack:                     Offset(0.0, 0.0),     summed: Offset(627.5, 582.5)
HeroSceneMarkerRenderObject:     Offset(0.0, 0.0),     summed: Offset(627.5, 582.5)
RenderConstrainedBox:            Offset(0.0, 56.0),    summed: Offset(627.5, 638.5)
RenderCustomMultiChildLayoutBox: Offset(0.0, 0.0),     summed: Offset(627.5, 638.5)
in a printed node visit we may see that our result includes redundant constraints applied by Appbar, which caused the HeroFly incorrect position:

RenderConstrainedBox: Offset(0.0, 56.0), accumulatedOffset: Offset(627.5, 638.5)

and it happens already after HeroSceneMarkerRenderObject, which is an HeroScene render tree entry.

So the fix would be to stop tree traversal after HeroSceneMarker, as hero animation takes place just within a HeroAnimationScene.


Offset globalPosition() {
    AbstractNode? node = this;

    while (node != null) {
      if (node is HeroSceneMarkerRenderObject) {
        return localToGlobal(Offset.zero, ancestor: node);
      }
      node = node.parent as RenderObject?;
    }
    return Offset.zero;
  }
Now it works as expected:
For HeroAnimation to track its layout position default localToGlobal(Offset.zero) doesn't cover each case, as it keeps traversing even after HeroAnimationScene was passed, to fix that we implemented our own tree traversal, which stops exactly where is needed — after the scene, and to detect HeroAnimationScene in the render object tree we used an embedded scene marker — HeroSceneMarkerRenderObject.

Please check the Source code.
Thanks for reading!

Don't miss the latest news. Subscribe to our newsletter!
© 2023 All Right Reserved. KiRN Tech.
sales@kirn.tech
Quick and Smooth Mobile App Launch
For Your Startup.
Peremohy Ave 24, Kyiv, Ukraine, 04116