How to fade in/out a widget from SliverAppBar while scrolling? How to fade in/out a widget from SliverAppBar while scrolling? flutter flutter

How to fade in/out a widget from SliverAppBar while scrolling?


This is actually quite simple using ScrollController and the Opacity Widget. Here's a basic example:

https://gist.github.com/smkhalsa/ec33ec61993f29865a52a40fff4b81a2


This solution uses the bloc pattern with a StreamBuilder, in addition to a LayoutBuilder to get a measure of the height available for the first time flutter builds the widget. The solution is probably not perfect as a locking semaphore was needed to prevent flutter constantly rebuild the widget in the StreamBuilder. The solution does not rely on animations, so you can stop the swipe midway and have a partially visible AppBar and CircleAvatar & Text.

Initially, I attempted to create this effect with setState, that did not work since the state became dirty because the build was not finished when setState was called before LayoutBuilder's return statement.

Image shows the effect of the code below

I have separated the solution into three files. The first main.dart resembles mostly what nesscx posted, with changes making the widget stateful and using a custom widget which is shown in the second file.

import 'package:flutter/material.dart';import 'flexible_header.dart'; // The code in the next listingvoid main() => runApp(MyApp());class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return MaterialApp(        title: 'Fading out CircleAvatar',        theme: ThemeData(          primarySwatch: Colors.purple,        ),        home: App());  }}class App extends StatefulWidget {  @override  _AppState createState() => _AppState();}class _AppState extends State<App> {  // A locking semaphore, it prevents unnecessary continuous updates of the  // bloc state when the user is not engaging with the app.  bool allowBlocStateUpdates = false;  allowBlocUpdates(bool allow) => setState(() => allowBlocStateUpdates = allow);  @override  Widget build(BuildContext context) {    return Scaffold(      body: Listener(        // Only to prevent unnecessary state updates to the FlexibleHeader's bloc.        onPointerMove: (details) => allowBlocUpdates(true),        onPointerUp: (details) => allowBlocUpdates(false),        child: DefaultTabController(          length: 2,          child: NestedScrollView(            headerSliverBuilder:                (BuildContext context, bool innerBoxIsScrolled) {              return <Widget>[                // Custom widget responsible for the effect                FlexibleHeader(                  allowBlocStateUpdates: allowBlocStateUpdates,                  innerBoxIsScrolled: innerBoxIsScrolled,                ),                SliverPersistentHeader(                  pinned: true,                  delegate: _SliverAppBarDelegate(                    new TabBar(                      indicatorColor: Colors.white,                      indicatorWeight: 3.0,                      tabs: <Tab>[                        Tab(text: 'TAB 1'),                        Tab(text: 'TAB 2'),                      ],                    ),                  ),                ),              ];            },            body: TabBarView(              children: <Widget>[                SingleChildScrollView(                  child: Container(                    height: 300.0,                    child: Text('Test 1',                        style: TextStyle(color: Colors.black, fontSize: 80.0)),                  ),                ),                SingleChildScrollView(                  child: Container(                    height: 300.0,                    child: Text('Test 2',                        style: TextStyle(color: Colors.red, fontSize: 80.0)),                  ),                ),              ],            ),          ),        ),      ),    );  }}// Not modifiedclass _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {  _SliverAppBarDelegate(this._tabBar);  final TabBar _tabBar;  @override  double get minExtent => _tabBar.preferredSize.height;  @override  double get maxExtent => _tabBar.preferredSize.height;  @override  Widget build(      BuildContext context, double shrinkOffset, bool overlapsContent) {    return new Container(      color: Colors.deepPurple,      child: _tabBar,    );  }  @override  bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {    return false;  }}

The second file flexible_header.dart contains the StreamBuilder and the LayoutBuilder, which closely interact with the bloc to update the UI with new opacity values. New height values are passed to the bloc which in turn updates the opacity.

import 'package:flutter/material.dart';import 'bloc.dart'; // The code in the next listing/// Creates a SliverAppBar that gradually toggles (with opacity) between/// showing the widget in the flexible space, and the SliverAppBar's title and leading.class FlexibleHeader extends StatefulWidget {  final bool allowBlocStateUpdates;  final bool innerBoxIsScrolled;  const FlexibleHeader(      {Key key, this.allowBlocStateUpdates, this.innerBoxIsScrolled})      : super(key: key);  @override  _FlexibleHeaderState createState() => _FlexibleHeaderState();}class _FlexibleHeaderState extends State<FlexibleHeader> {  FlexibleHeaderBloc bloc;  @override  void initState() {    super.initState();    bloc = FlexibleHeaderBloc();  }  @override  void dispose() {    super.dispose();    bloc.dispose();  }  @override  Widget build(BuildContext context) {    return StreamBuilder(      initialData: bloc.initial(),      stream: bloc.stream,      builder: (BuildContext context, AsyncSnapshot<FlexibleHeaderState> stream) {        FlexibleHeaderState state = stream.data;        // Main widget responsible for the effect        return SliverOverlapAbsorber(          handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),          child: SliverAppBar(              expandedHeight: 254,              pinned: true,              primary: true,              leading: Opacity(                opacity: state.opacityAppBar,                child: Icon(Icons.arrow_back),              ),              title: Opacity(                opacity: state.opacityAppBar,                child: Text('Fade'),              ),              forceElevated: widget.innerBoxIsScrolled,              flexibleSpace: LayoutBuilder(                builder: (BuildContext context, BoxConstraints constraints) {                  // LayoutBuilder allows us to receive the max height of                  // the widget, the first value is stored in the bloc which                  // allows later values to easily be compared to it.                  //                  // Simply put one can easily turn it to a double from 0-1 for                  // opacity.                  print("BoxConstraint - Max Height: ${constraints.maxHeight}");                  if (widget.allowBlocStateUpdates) {                    bloc.update(state, constraints.maxHeight);                  }                  return Opacity(                    opacity: state.opacityFlexible,                    child: FlexibleSpaceBar(                      collapseMode: CollapseMode.parallax,                      centerTitle: true,                      title: Column(                        mainAxisAlignment: MainAxisAlignment.end,                        children: <Widget>[                          // Remove flexible for constant width of the                          // CircleAvatar, but only if you want to introduce a                          // RenderFlex overflow error for the text, but it is                          // only visible when opacity is very low.                          Flexible(                            child: CircleAvatar(                                radius: 36.0,                                child: Text('N',                                    style: TextStyle(color: Colors.white)),                                backgroundColor: Colors.green),                          ),                          Flexible(child: Text('My Name')),                        ],                      ),                      background: Container(color: Colors.purple),                    ),                  );                },              )),        );      },    );  }}

The third file is a bloc, bloc.dart. To obtain the opacity effect some math had to be done, and checking that the opacity value was between 0 to 1, the solution is not perfect, but it works.

import 'dart:async';/// The variables necessary for proper functionality in the FlexibleHeaderclass FlexibleHeaderState{  double initialHeight;  double currentHeight;  double opacityFlexible = 1;  double opacityAppBar = 0;  FlexibleHeaderState();}/// Used in a StreamBuilder to provide business logic with how the opacity is updated./// depending on changes to the height initially/// available when flutter builds the widget the first time.class FlexibleHeaderBloc{  StreamController<FlexibleHeaderState> controller = StreamController<FlexibleHeaderState>();  Sink get sink => controller.sink;  Stream<FlexibleHeaderState> get stream => controller.stream;  FlexibleHeaderBloc();  _updateOpacity(FlexibleHeaderState state) {    if (state.initialHeight == null || state.currentHeight == null){      state.opacityFlexible = 1;      state.opacityAppBar = 0;    } else {      double offset = (1 / 3) * state.initialHeight;      double opacity = (state.currentHeight - offset) / (state.initialHeight - offset);      //Lines below prevents exceptions      opacity <= 1 ? opacity = opacity : opacity = 1;      opacity >= 0 ? opacity = opacity : opacity = 0;      state.opacityFlexible = opacity;      state.opacityAppBar = (1-opacity).abs(); // Inverse the opacity    }  }  update(FlexibleHeaderState state, double currentHeight){    state.initialHeight ??= currentHeight;    state.currentHeight = currentHeight;    _updateOpacity(state);    _update(state);  }  FlexibleHeaderState initial(){    return FlexibleHeaderState();  }  void dispose(){    controller.close();  }  void _update(FlexibleHeaderState state){    sink.add(state);  }}

Hope this helps somebody :)