How to animate the position of the items in a SliverAppBar to move them around the title when closed How to animate the position of the items in a SliverAppBar to move them around the title when closed flutter flutter

How to animate the position of the items in a SliverAppBar to move them around the title when closed


You can create your own SliverAppBar by extending SliverPersistentHeaderDelegate.

The translate, scaling, and opacity changes will be done in the build(...) method because this will be called during extent changes (via scrolling), minExtent <-> maxExtent.

Here's a sample code.

import 'dart:math';import 'package:flutter/material.dart';void main() {  runApp(MyApp());}class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return MaterialApp(      theme: ThemeData(        primaryColor: Colors.blue,      ),      home: HomePage(),    );  }}class HomePage extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Scaffold(      body: CustomScrollView(        slivers: <Widget>[          SliverPersistentHeader(            delegate: MySliverAppBar(              title: 'Sample',              minWidth: 50,              minHeight: 25,              leftMaxWidth: 200,              leftMaxHeight: 100,              rightMaxWidth: 100,              rightMaxHeight: 50,              shrinkedTopPos: 10,            ),            pinned: true,          ),          SliverList(            delegate: SliverChildBuilderDelegate(              (_, int i) => Container(                height: 50,                color: Color.fromARGB(                  255,                  Random().nextInt(255),                  Random().nextInt(255),                  Random().nextInt(255),                ),              ),              childCount: 50,            ),          ),        ],      ),    );  }}class MySliverAppBar extends SliverPersistentHeaderDelegate {  MySliverAppBar({    required this.title,    required this.minWidth,    required this.minHeight,    required this.leftMaxWidth,    required this.leftMaxHeight,    required this.rightMaxWidth,    required this.rightMaxHeight,    this.titleStyle = const TextStyle(fontSize: 26),    this.shrinkedTopPos = 0,  });  final String title;  final TextStyle titleStyle;  final double minWidth;  final double minHeight;  final double leftMaxWidth;  final double leftMaxHeight;  final double rightMaxWidth;  final double rightMaxHeight;  final double shrinkedTopPos;  final GlobalKey _titleKey = GlobalKey();  double? _topPadding;  double? _centerX;  Size? _titleSize;  double get _shrinkedTopPos => _topPadding! + shrinkedTopPos;  @override  Widget build(    BuildContext context,    double shrinkOffset,    bool overlapsContent,  ) {    if (_topPadding == null) {      _topPadding = MediaQuery.of(context).padding.top;    }    if (_centerX == null) {      _centerX = MediaQuery.of(context).size.width / 2;    }    if (_titleSize == null) {      _titleSize = _calculateTitleSize(title, titleStyle);    }    double percent = shrinkOffset / (maxExtent - minExtent);    percent = percent > 1 ? 1 : percent;    return Container(      color: Colors.red,      child: Stack(        children: <Widget>[          _buildTitle(shrinkOffset),          _buildLeftImage(percent),          _buildRightImage(percent),        ],      ),    );  }  Size _calculateTitleSize(String text, TextStyle style) {    final TextPainter textPainter = TextPainter(        text: TextSpan(text: text, style: style),        maxLines: 1,        textDirection: TextDirection.ltr)      ..layout(minWidth: 0, maxWidth: double.infinity);    return textPainter.size;  }  Widget _buildTitle(double shrinkOffset) => Align(        alignment: Alignment.topCenter,        child: Padding(          padding: EdgeInsets.only(top: _topPadding!),          child: Opacity(            opacity: shrinkOffset / maxExtent,            child: Text(title, key: _titleKey, style: titleStyle),          ),        ),      );  double getScaledWidth(double width, double percent) =>      width - ((width - minWidth) * percent);  double getScaledHeight(double height, double percent) =>      height - ((height - minHeight) * percent);  /// 20 is the padding between the image and the title  double get shrinkedHorizontalPos =>      (_centerX! - (_titleSize!.width / 2)) - minWidth - 20;  Widget _buildLeftImage(double percent) {    final double topMargin = minExtent;    final double rangeLeft =        (_centerX! - (leftMaxWidth / 2)) - shrinkedHorizontalPos;    final double rangeTop = topMargin - _shrinkedTopPos;    final double top = topMargin - (rangeTop * percent);    final double left =        (_centerX! - (leftMaxWidth / 2)) - (rangeLeft * percent);    return Positioned(      left: left,      top: top,      child: Container(        width: getScaledWidth(leftMaxWidth, percent),        height: getScaledHeight(leftMaxHeight, percent),        color: Colors.black,      ),    );  }  Widget _buildRightImage(double percent) {    final double topMargin = minExtent + (rightMaxHeight / 2);    final double rangeRight =        (_centerX! - (rightMaxWidth / 2)) - shrinkedHorizontalPos;    final double rangeTop = topMargin - _shrinkedTopPos;    final double top = topMargin - (rangeTop * percent);    final double right =        (_centerX! - (rightMaxWidth / 2)) - (rangeRight * percent);    return Positioned(      right: right,      top: top,      child: Container(        width: getScaledWidth(rightMaxWidth, percent),        height: getScaledHeight(rightMaxHeight, percent),        color: Colors.white,      ),    );  }  @override  double get maxExtent => 300;  @override  double get minExtent => _topPadding! + 50;  @override  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>      false;}


Its a bit messy in formulas, but here is how you can do all the calculations about animation:

UPD: added to code variable to make Y axis offset for images when extended.

Full code to reproduce:

import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return MaterialApp(      title: 'Material App',      home: Body(),    );  }}class Body extends StatefulWidget {  const Body({    Key key,  }) : super(key: key);  @override  _BodyState createState() => _BodyState();}class _BodyState extends State<Body> {  double _collapsedHeight = 60;  double _expandedHeight = 200;  double      extentRatio; // Value to control SliverAppBar widget sizes, based on BoxConstraints and  double minH1 = 40; // Minimum height of the first image.  double minW1 = 30; // Minimum width of the first image.  double minH2 = 20; // Minimum height of second image.  double minW2 = 25; // Minimum width of second image.  double maxH1 = 60; // Maximum height of the first image.  double maxW1 = 60; // Maximum width of the first image.  double maxH2 = 40; // Maximum height of second image.  double maxW2 = 50; // Maximum width of second image.  double textWidth = 70; // Width of a given title text.  double extYAxisOff = 10.0; // Offset on Y axis for both images when sliver is extended.  @override  Widget build(BuildContext context) {    return Scaffold(      body: SafeArea(        child: NestedScrollView(          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {            return <Widget>[              SliverAppBar(                  collapsedHeight: _collapsedHeight,                  expandedHeight: _expandedHeight,                  floating: true,                  pinned: true,                  flexibleSpace: LayoutBuilder(                    builder:                        (BuildContext context, BoxConstraints constraints) {                      extentRatio =                          (constraints.biggest.height - _collapsedHeight) /                              (_expandedHeight - _collapsedHeight);                      double xAxisOffset1 = (-(minW1 - minW2) -                              textWidth +                              (textWidth + maxW1) * extentRatio) /                          2;                      double xAxisOffset2 = (-(minW1 - minW2) +                              textWidth +                              (-textWidth - maxW2) * extentRatio) /                          2;                      double yAxisOffset2 = (-(minH1 - minH2) -                                  (maxH1 - maxH2 - (minH1 - minH2)) *                                      extentRatio) /                              2 -                          extYAxisOff * extentRatio;                      double yAxisOffset1 = -extYAxisOff * extentRatio;                      print(extYAxisOff);                      // debugPrint('constraints=' + constraints.toString());                      // debugPrint('Scale ratio is $extentRatio');                      return FlexibleSpaceBar(                        titlePadding: EdgeInsets.all(0),                        // centerTitle: true,                        title: Stack(                          children: [                            Align(                              alignment: Alignment.topCenter,                              child: AnimatedOpacity(                                duration: Duration(milliseconds: 300),                                opacity: extentRatio < 1 ? 1 : 0,                                child: Padding(                                  padding: const EdgeInsets.only(top: 30.0),                                  child: Container(                                    color: Colors.indigo,                                    width: textWidth,                                    alignment: Alignment.center,                                    height: 20,                                    child: Text(                                      "TITLE TEXT",                                      style: TextStyle(                                        color: Colors.white,                                        fontSize: 12.0,                                      ),                                    ),                                  ),                                ),                              ),                            ),                            Align(                              alignment: Alignment.bottomCenter,                              child: Row(                                crossAxisAlignment: CrossAxisAlignment.end,                                mainAxisAlignment: MainAxisAlignment.center,                                children: [                                  Container(                                    transform: Matrix4(                                        1,0,0,0,                                        0,1,0,0,                                        0,0,1,0,                                        xAxisOffset1,yAxisOffset1,0,1),                                    width:                                        minW1 + (maxW1 - minW1) * extentRatio,                                    height:                                        minH1 + (maxH1 - minH1) * extentRatio,                                    color: Colors.red,                                  ),                                  Container(                                    transform: Matrix4(                                        1,0,0,0,                                        0,1,0,0,                                        0,0,1,0,                                        xAxisOffset2,yAxisOffset2,0,1),                                                                      width:                                        minW2 + (maxW2 - minW2) * extentRatio,                                    height:                                        minH2 + (maxH2 - minH2) * extentRatio,                                    color: Colors.purple,                                  ),                                ],                              ),                            ),                          ],                        ),                      );                    },                  )),            ];          },          body: Center(            child: Text("Sample Text"),          ),        ),      ),    );  }}