【问题标题】:How to fade in/out a widget from SliverAppBar while scrolling?滚动时如何从 SliverAppBar 淡入/淡出小部件?
【发布时间】:2018-12-21 15:20:08
【问题描述】:

当用户在屏幕上滚动时,我想从 SliverAppBar 中“淡入”和“淡出”一个小部件。

这是我想做的一个例子:

这是我没有“褪色”的代码:

https://gist.github.com/nesscx/721cd823350848e3d594ba95df68a7fa

导入'package:flutter/material.dart';

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fading out CircleAvatar',
      theme: ThemeData(
        primarySwatch: Colors.purple,
      ),
      home: Scaffold(
        body: DefaultTabController(
          length: 2,
          child: NestedScrollView(
            headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
              return <Widget>[
                SliverOverlapAbsorber(
                  handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                  child: new SliverAppBar(
                    expandedHeight: 254.0,
                    pinned: false,
                    leading: Icon(Icons.arrow_back),
                    title:Text('Fade'),
                    forceElevated: innerBoxIsScrolled, 
                    flexibleSpace: new FlexibleSpaceBar(
                      centerTitle: true,
                      title: Column(
                        mainAxisAlignment: MainAxisAlignment.end,
                        children: <Widget>[
                          CircleAvatar(
                            radius: 36.0,
                            child: Text(
                              'N',
                              style: TextStyle(
                                color: Colors.white,
                              ),
                            ),
                            backgroundColor: Colors.green,
                          ),
                          Text('My Name'),
                        ],
                      ),
                      background: Container(
                        color: Colors.purple,
                      ),
                    ),
                  ),
                ),
                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)),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

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;
  }
}

【问题讨论】:

    标签: mobile flutter fadein fadeout flutter-sliver


    【解决方案1】:

    此解决方案使用带有 StreamBuilder 的 bloc 模式,以及 LayoutBuilder 来测量第一次 Flutter 构建小部件时可用的高度。该解决方案可能并不完美,因为需要锁定信号量来防止颤动不断重建 StreamBuilder 中的小部件。该解决方案不依赖动画,因此您可以在中途停止滑动并拥有部分可见的 AppBar 和 CircleAvatar & Text。

    最初,我尝试使用 setState 创建此效果,但由于在 LayoutBuilder 的 return 语句之前调用 setState 时构建尚未完成,因此状态变脏了,因此该效果不起作用。

    我已将解决方案分成三个文件。第一个 main.dart 大部分类似于 nesscx 发布的内容,其中的更改使小部件有状态,并使用了第二个文件中显示的自定义小部件。

    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;
      }
    }
    

    第二个文件 flexible_header.dart 包含 StreamBuilder 和 LayoutBuilder,它们与 bloc 密切交互以使用新的不透明度值更新 UI。新的高度值被传递给块,后者又更新不透明度。

    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),
                        ),
                      );
                    },
                  )),
            );
          },
        );
      }
    }
    

    第三个文件是一个块,bloc.dart。要获得不透明度效果,必须进行一些数学运算,并检查不透明度值是否在 0 到 1 之间,该解决方案并不完美,但它确实有效。

    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);
      }
    
    }
    

    希望这对某人有帮助:)

    【讨论】:

      【解决方案2】:

      使用ScrollControllerOpacity 小部件实际上非常简单。这是一个基本示例:

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

      【讨论】:

      • 你不应该使用Opacity 来制作动画。改用FadeTransition
      猜你喜欢
      • 2019-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-14
      • 1970-01-01
      • 1970-01-01
      • 2013-10-05
      • 2012-03-15
      相关资源
      最近更新 更多