如何在滚动时从SliverAppBar淡入/淡出小部件?

19

我希望在用户滚动屏幕时从SliverAppBar中的一个小部件实现'淡入'和'淡出'效果。

以下是我想要做的示例:

enter image description here

这是没有'淡入/淡出'效果的代码:

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

import '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;
  }
}
3个回答

11

7

这个解决方案使用了bloc模式与StreamBuilder,另外还用了LayoutBuilder来获取在第一次构建flutter部件时可用的高度。由于需要防止flutter不断地重建StreamBuilder中的小部件,所以使用了一个锁定信号量,该解决方案可能并不完美。该解决方案不依赖于动画,因此您可以在滑动过程中停止,并且仍然可以看到部分可见的AppBar、CircleAvatar和Text。

最初,我尝试使用setState来创建这个效果,但由于在LayoutBuilder返回语句之前调用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。新的高度值传递给 bloc,然后更新不透明度。
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);
  }

}

希望这可以帮助到某些人 :)

0
一个示例,当应用栏折叠时,标题会淡入。标题的不透明度通过来自SliverLayoutBuilder的SliverConstraints计算得出。
class MySliverAppBar extends StatelessWidget {
  final String title;
  final double headerHeight;
  final Widget header;

  const MySliverAppBar({
    super.key,
    required this.title,
    required this.headerHeight,
    required this.header,
  });

  @override
  Widget build(BuildContext context) {
    return SliverLayoutBuilder(
      builder: (context, constraints) {
        const toolbarHeight = kToolbarHeight;
        final expandedHeight =
            headerHeight - MediaQuery.of(context).viewPadding.top;
        final isScrolledUnder =
            constraints.scrollOffset > expandedHeight - toolbarHeight;
        return SliverAppBar(
          pinned: true,
          toolbarHeight: toolbarHeight,
          title: AnimatedOpacity(
            opacity: isScrolledUnder ? 1 : 0,
            duration: const Duration(milliseconds: 500),
            curve: const Cubic(0.2, 0.0, 0.0, 1.0),
            child: Text(title),
          ),
          titleTextStyle: const TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.w600,
          ),
          centerTitle: true,
          titleSpacing: 8,
          flexibleSpace: FlexibleSpaceBar(background: header),
          expandedHeight: expandedHeight,
        );
      },
    );
  }
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接