How to create a hero style animation on the same screen route with Flutter?
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:
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'), ); }), ) ], ),);