Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a hero style animation on the same screen route with Flutter?

Mockup

I'm trying to create a dynamic animation described in the mockup above. What I need is:

  • A round shape that will represent an avatar (user pic, for example);
  • A text centered below it.
  • Below those (that use half of the screen) is a scrollable PageView.

The animation should be as the following:

Begin: In a Stack both centered at the begin.
Animation: Scaling down and sliding the text (with variable lenght) that must be to the right of the avatar.
End:
As the second image in the mockup. Side-to-side while the content below keeps scrolling.

Thought in SliverPersistentHeader combined with CustomMultiChildLayout but the problem is that the text starts centered and ends align to the left and I can animate this dynamically. I was trying to remove the offset of the centered text in the end but it doesn't feel right.

Any help or a sample only with this animation would be appreciate. Thank you.

like image 793
Miguel Ruivo Avatar asked Sep 25 '18 09:09

Miguel Ruivo


1 Answers

You will need a Sliver to animate your layout based on the scroll offset. More specifically, SliverPersistentHeader in your situation.

CustomMultiChildLayout is not necessary though, you can achieve the same result using tweens and align/padding/stuff. But you can give it a go if your layout starts to become too complex.

The trick is to use the scroll offset given by SliverPersistentHeader to compute the current progression. Then use that progression to position element between their original and final position.

Here's a raw example:

enter image description here

class TransitionAppBar extends StatelessWidget {
  final Widget avatar;
  final Widget title;

  const TransitionAppBar({this.avatar, this.title, Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SliverPersistentHeader(
      pinned: true,
      delegate: _TransitionAppBarDelegate(
        avatar: avatar,
        title: title,
      ),
    );
  }
}

class _TransitionAppBarDelegate extends SliverPersistentHeaderDelegate {
  final _avatarTween =
      SizeTween(begin: Size(150.0, 150.0), end: Size(50.0, 50.0));
  final _avatarMarginTween =
      EdgeInsetsTween(begin: EdgeInsets.zero, end: EdgeInsets.only(left: 10.0));
  final _avatarAlignTween =
      AlignmentTween(begin: Alignment.topCenter, end: Alignment.centerLeft);

  final _titleMarginTween = EdgeInsetsTween(
      begin: EdgeInsets.only(top: 150.0 + 5.0),
      end: EdgeInsets.only(left: 10.0 + 50.0 + 5.0));
  final _titleAlignTween =
      AlignmentTween(begin: Alignment.center, end: Alignment.centerLeft);

  final Widget avatar;
  final Widget title;

  _TransitionAppBarDelegate({this.avatar, this.title})
      : assert(avatar != null),
        assert(title != null);

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    final progress = shrinkOffset / 200.0;

    final avatarSize = _avatarTween.lerp(progress);
    final avatarMargin = _avatarMarginTween.lerp(progress);
    final avatarAlign = _avatarAlignTween.lerp(progress);

    final titleMargin = _titleMarginTween.lerp(progress);
    final titleAlign = _titleAlignTween.lerp(progress);
    return Stack(
      fit: StackFit.expand,
      children: <Widget>[
        Padding(
          padding: avatarMargin,
          child: Align(
            alignment: avatarAlign,
            child: SizedBox.fromSize(size: avatarSize, child: avatar),
          ),
        ),
        Padding(
          padding: titleMargin,
          child: Align(
            alignment: titleAlign,
            child: DefaultTextStyle(
                style: Theme.of(context).textTheme.title, child: title),
          ),
        )
      ],
    );
  }

  @override
  double get maxExtent => 200.0;

  @override
  double get minExtent => 100.0;

  @override
  bool shouldRebuild(_TransitionAppBarDelegate oldDelegate) {
    return avatar != oldDelegate.avatar || title != oldDelegate.title;
  }
}

which you can use with a CustomScrollView:

Scaffold(
  body: CustomScrollView(
    slivers: <Widget>[
      TransitionAppBar(
        avatar: Material(
          color: Colors.blue,
          elevation: 3.0,
        ),
        title: Text("Hello World"),
      ),
      SliverList(
        delegate: SliverChildBuilderDelegate((context, index) {
          return ListTile(
            title: Text('$index'),
          );
        }),
      )
    ],
  ),
);
like image 115
Rémi Rousselet Avatar answered Oct 11 '22 09:10

Rémi Rousselet