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

时间:2018-07-13 09:20:58

标签: mobile flutter fadein fadeout flutter-sliver

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

这是我想做的事的一个例子:

enter image description here

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

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

2 个答案:

答案 0 :(得分:1)

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

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

Image shows the effect of the code below

我已将解决方案分为三个文件。第一个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,它们与块紧密交互以使用新的不透明度值更新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);
  }

}

希望这对某人有帮助:)

答案 1 :(得分:1)

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

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