Hardy
Hardy

Reputation: 2606

Sliver Appbar [Collapsing Toolbar] animate title from left to center in Flutter

Here is my Build method for collapsing toolbar:-

     @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: CustomScrollView(
        controller: controller,
        slivers: <Widget>[
          SliverAppBar(
            pinned: true,
            expandedHeight: appBarHeight,
            leading: IconButton(
              icon: Icon(
                Icons.arrow_back_ios,
                color: Colors.black,
              ),
              onPressed: () => null,
            ),
            floating: true,
            flexibleSpace: FlexibleSpaceBar(

          titlePadding: EdgeInsets.only(left:leftV , bottom:bottomV ),
          title: Text(
            "Title ",
            style: TextStyle(
              color: Colors.black,
              fontSize: 16.0,
            ),
          ),
        ),
      ),
      SliverList(delegate:
          SliverChildBuilderDelegate((BuildContext context, int index) {
        return ListTile(title: Text("Flutter / $index"));
      }))
    ],
  ),
);
}

As per the doc I got solution to remove padding :-

/// By default the value of this property is /// EdgeInsetsDirectional.only(start: 72, bottom: 16) if the title is /// not centered, EdgeInsetsDirectional.only(start 0, bottom: 16) otherwise. final EdgeInsetsGeometry titlePadding;

But I got the output as :-

enter image description here

I want to center the title when the app bar is totally collapsed.

Issue has been filed in github also check here.

Upvotes: 11

Views: 12310

Answers (5)

Enisco
Enisco

Reputation: 11

I had the same issue, I resolved it using LayoutBuilder as the child for the flexibleSpace widget of the SliverAppBar. The purpose of the LayoutBuilder is to enable me to know the current position (height) of the appBar.

I'm using MediaQuery.of(context).size to automatically get the size of the screen.

var top = 0.0;
var appbarThreshold = 140.0;

class _MySliverAppBarState extends State<MySliverAppBar> {
  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;

    return SliverAppBar(
      centerTitle: true,
      pinned: true,
      leading: TextButton(
        child: CircleAvatar(
          radius: size.width / 4,
          backgroundColor: Colors.blue.withOpacity(0.3),
        ),
        onPressed: () {
          print("Hello");
        },
      ),
      leadingWidth: size.width / 4,
      collapsedHeight: size.height / 11.5,
      expandedHeight: size.height / 5,
      backgroundColor: Colors.white,
      foregroundColor: Colors.black,

      flexibleSpace: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          top = constraints.biggest.height;

          return FlexibleSpaceBar(
            title: AnimatedOpacity(
              duration: const Duration(milliseconds: 300),
              opacity: 1.0,
              child: Text(
                top < appbarThreshold ? "Bloom" : "Welcome, Iremide",
                style: TextStyle(
                    fontSize: top < appbarThreshold
                        ? size.height / 30
                        : size.height / 40,
                    color: Colors.black87,
                    fontFamily: 'SourceSansSerif',
                    fontWeight: FontWeight.w700),
              ),
            ),
            titlePadding: top < appbarThreshold
                ? EdgeInsets.fromLTRB(
                    size.width / 4.9, 0.0, 0.0, size.height / 18)
                : EdgeInsets.fromLTRB(
                    size.width / 14, 0.0, 0.0, size.height / 30),
          );
        },
      ),
    );
  }
}

You can adjust the Position of the Title when appBar is collapsed by editing the left padding size here:

//
                titlePadding: top < appbarThreshold
                    ? EdgeInsets.fromLTRB(
                        size.width / 4.9, 0.0, 0.0, size.height / 18)
                    : EdgeInsets.fromLTRB(
                        size.width / 14, 0.0, 0.0, size.height / 30),
              

Regards.

Upvotes: 1

Jignesh Pandav
Jignesh Pandav

Reputation: 111

          late ScrollController _scrollController;
          static const kExpandedHeight = 300.0;
        
          @override
          void initState() {
            super.initState();
            _scrollController = ScrollController()..addListener(() => setState(() {}));
          }
        
          double get _horizontalTitlePadding {
            const kBasePadding = 15.0;
            const kMultiplier = 0.5;
        
            if (_scrollController.hasClients) {
              if (_scrollController.offset < (kExpandedHeight / 2)) {
                // In case 50%-100% of the expanded height is viewed
                return kBasePadding;
              }
        
              if (_scrollController.offset > (kExpandedHeight - kToolbarHeight)) {
                // In case 0% of the expanded height is viewed
                return (kExpandedHeight / 2 - kToolbarHeight) * kMultiplier +
                    kBasePadding;
              }
        
              // In case 0%-50% of the expanded height is viewed
              return (_scrollController.offset - (kExpandedHeight / 2)) * kMultiplier +
                  kBasePadding;
            }
        
            return kBasePadding;
          }
    
    
    CustomScrollView(
            controller: _scrollController,
            slivers: [
              SliverAppBar(
                expandedHeight: kExpandedHeight,
                pinned: true,
                flexibleSpace: FlexibleSpaceBar(
                  title: Text(product.title),
                  titlePadding: EdgeInsets.symmetric(
                      vertical: 16.0, horizontal: _horizontalTitlePadding),
                  background: Hero(
                    tag: product.id,
                    child: Image.network(
                      product.imageUrl,
                      fit: BoxFit.cover,
                    ),
                  ),
                ),
              ),
              SliverList(
                  delegate: SliverChildListDelegate([
//add your widgets here
])
        ]
) //end of CustomScrollView

Upvotes: 1

Peter Keefe
Peter Keefe

Reputation: 1135

Edit:

I ended up creating a better solution that utilizes the transformations already happening within the FlexibleSpaceBar. After copying the file from the gist into your project, replace FlexibleSpaceBar with MyFlexibleSpaceBar and provide a titlePaddingTween such as

titlePaddingTween: EdgeInsetsTween(begin: EdgeInsets.only(left: 16.0, bottom: 16), end: EdgeInsets.only(left: 72.0, bottom: 16))

instead of titlePadding. The tween will animate from the "begin" EdgeInsets when the appbar is fully expanded to the "end" EdgeInsets when the appbar is collapsed.

I also added a foreground parameter that displays above the title and background, but doesn't transform as they do.

Original Answer:

The other answers are good, but they rebuild more widgets than necessary. My solution builds on the other answers but will only rebuild what is within ValueListenableBuilder:

class SamplePage extends StatelessWidget {
  static const _kBasePadding = 16.0;
  static const kExpandedHeight = 250.0;

  final ValueNotifier<double> _titlePaddingNotifier = ValueNotifier(_kBasePadding);

  final _scrollController = ScrollController();

  double get _horizontalTitlePadding {
    const kCollapsedPadding = 60.0;

    if (_scrollController.hasClients) {
      return min(_kBasePadding + kCollapsedPadding,
          _kBasePadding + (kCollapsedPadding * _scrollController.offset)/(kExpandedHeight - kToolbarHeight));
    }

    return _kBasePadding;
  }

  @override
  Widget build(BuildContext context) {
    _scrollController.addListener(() {
      _titlePaddingNotifier.value = _horizontalTitlePadding;
    });

    return Scaffold(

      body: NestedScrollView(
          controller: _scrollController,
          headerSliverBuilder: (context, innerBoxIsScrolled) {
            return <Widget>[
              SliverAppBar(
                  expandedHeight: kExpandedHeight,
                  floating: false,
                  pinned: true,
                  flexibleSpace: FlexibleSpaceBar(
                      collapseMode: CollapseMode.pin,
                      centerTitle: false,
                      titlePadding: EdgeInsets.symmetric(vertical: 16, horizontal: 0),
                      title: ValueListenableBuilder(
                        valueListenable: _titlePaddingNotifier,
                        builder: (context, value, child) {
                          return Padding(
                            padding: EdgeInsets.symmetric(horizontal: value),
                            child: Text(
                              "Title"),
                          );
                        },
                      ),
                      background: Container(color: Colors.green)
                  )
              ),
            ];
          },
          body: Text("Body text")
        ),
    );
  }
}

Upvotes: 9

Jon
Jon

Reputation: 385

I managed to get a solution with a ScrollController.

Example Result Gif

I used this next function:

double get _horizontalTitlePadding {
    const kBasePadding = 15.0;
    const kMultiplier = 0.5;

    if (_scrollController.hasClients) {
      if (_scrollController.offset < (kExpandedHeight / 2)) {
        // In case 50%-100% of the expanded height is viewed
        return kBasePadding;
      }

      if (_scrollController.offset > (kExpandedHeight - kToolbarHeight)) {
        // In case 0% of the expanded height is viewed
        return (kExpandedHeight / 2 - kToolbarHeight) * kMultiplier +
            kBasePadding;
      }

      // In case 0%-50% of the expanded height is viewed
      return (_scrollController.offset - (kExpandedHeight / 2)) * kMultiplier +
          kBasePadding;
    }

    return kBasePadding;
}

And I used it inside of my SilverAppBar titlePadding:

  child: Scaffold(
      body: CustomScrollView(
    controller: _scrollController,
    slivers: <Widget>[
      SliverAppBar(
        pinned: true,
        expandedHeight: kExpandedHeight,
        flexibleSpace: FlexibleSpaceBar(
          titlePadding: EdgeInsets.symmetric(
              vertical: 16.0, horizontal: _horizontalTitlePadding),

Just make sure to initialize the controller in initState():

_scrollController = ScrollController()..addListener(() => setState(() {}));

Upvotes: 2

Hardy
Hardy

Reputation: 2606

Found the solution on my own!!!

Add below code to your Sliver App Bar .........

 flexibleSpace: LayoutBuilder(
                builder:
                    (BuildContext context, BoxConstraints constraints) {
                  double percent =
                      ((constraints.maxHeight - kToolbarHeight) *
                          100 /
                          (appBarHeight - kToolbarHeight));
                  double dx = 0;

                  dx = 100 - percent;
                  if (constraints.maxHeight == 100) {
                    dx = 0;
                  }

                  return Stack(
                    children: <Widget>[
                      Padding(
                        padding: const EdgeInsets.only(
                            top: kToolbarHeight / 4, left: 0.0),
                        child: Transform.translate(
                          child: Text(
                            title,
                            style: MyTextStyle.getAppBarTextStyle(
                                screenUtil, appColors),
                          ),
                          offset: Offset(
                              dx, constraints.maxHeight - kToolbarHeight),
                        ),
                      ),
                    ],
                  );
                },
              ),

Percentage is calculated based on the scroll and it animation has been placed accordingly.

enter image description here

Upvotes: 3

Related Questions