Using custom curve in Flutter Hero animations

2 minute read

Sometimes the default Hero behavior is not enough and you’d like to add some custom taste to it. One of the simple ways to do it is to customize curve of the transition animation.

In this post you’ll see a sample that allows to use any custom curve to animate bounds of the Hero. In my case it was a bit faster expansion in vertical axis.

If you can’t see the video (on Safari), here’s the GIF.

Hero properties

Hero class has several convenient properties that allow to customize its behavior. These are:

  • flightShuttleBuilder that allows to wrap or change entire Hero animation by e.g. adding rotation or fade transition
  • placeholderBuilder that gives us possibility to show Widget in place previously occupied by the animated widget
  • createRectTween which is a function that determines the bounds of the Hero during animation - that’s what we want to customize today

What is createRectTween

This property takes a CreateRectTween function which is simply a typedef for Tween<Rect> Function(Rect begin, Rect end).

It means that it interpolates two Rects between begin and end state. By default a RectTween(begin: begin, end: end) is used in Hero. It uses its lerp method which aside from few asserts only interpolates Rect coordinates in linear manner:

static Rect lerp(Rect a, Rect b, double t) {
  assert(t != null);
  if (a == null && b == null)
    return null;
  if (a == null)
    return Rect.fromLTRB(b.left * t, b.top * t, b.right * t, b.bottom * t);
  if (b == null) {
    final double k = 1.0 - t;
    return Rect.fromLTRB(a.left * k, a.top * k, a.right * k, a.bottom * k);
  }
  return Rect.fromLTRB(
    lerpDouble(a.left, b.left, t),
    lerpDouble(a.top, b.top, t),
    lerpDouble(a.right, b.right, t),
    lerpDouble(a.bottom, b.bottom, t),
  );
}

The method lerpDouble is where the interpolation takes place:

double lerpDouble(num a, num b, double t) {
    if (a == null && b == null) return null;
    a ??= 0.0;
    b ??= 0.0;
    return a + (b - a) * t;
}

How to change the default implementation

When defining your Hero you should return a custom createRectTween function like this:

Hero(
    tag: index,
    createRectTween: (begin, end) {
        return CustomRectTween(a: begin, b: end);
    },
    child: Poster(),
),

And finally CustomRectTween has to extend RectTween and to have overwritten lerp method.

The parameter t is animation clock value between 0.0 and 1.0. Each Curve can be used to interpolate any given value between 0.0 and 1.0 on the curve. I use my custom cubic curve in this example.

class CustomRectTween extends RectTween {
  CustomRectTween({this.a, this.b}) : super(begin: a, end: b);
  final Rect a;
  final Rect b;

  @override
  Rect lerp(double t) {
    Curves.elasticOut.transform(t);
    //any curve can be applied here e.g. Curve.elasticOut.transform(t);
    final verticalDist = Cubic(0.72, 0.15, 0.5, 1.23).transform(t);

    final top = lerpDouble(a.top, b.top, t) * (1 - verticalDist);
    return Rect.fromLTRB(
      lerpDouble(a.left, b.left, t),
      top,
      lerpDouble(a.right, b.right, t),
      lerpDouble(a.bottom, b.bottom, t),
    );
  }

  double lerpDouble(num a, num b, double t) {
    if (a == null && b == null) return null;
    a ??= 0.0;
    b ??= 0.0;
    return a + (b - a) * t;
  }
}

It’s important to note that I wanted to change only top coordinate of the Hero’s Rect, so when calculating the value I use (1 - verticalDist) (thus inverting the curve).

I hope you’ll find it useful and try to experiment with Hero widget on your own.

I highly recommend the post Mastering Hero Animations in Flutter by Chema Molins from Flutter Community.

Updated: