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

后端 未结 2 1102
情深已故
情深已故 2021-02-04 08:28

I want to \'fade in\' and \'fade out\' a widget from SliverAppBar when user scrolls on the screen.

This is an example of what I want to do:

Here is my c

相关标签:
2条回答
  • 2021-02-04 08:56

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

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

    0 讨论(0)
  • 2021-02-04 09:14

    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.

    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 listing
    
    void 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 modified
    class _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 FlexibleHeader
    class 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 :)

    0 讨论(0)
提交回复
热议问题