SliverAppBar底部小部件如何实现动态高度?

12
SliverAppBar有一个bottom属性,必须设置为preferredSize。
现在我让它返回一个固定值:
  ...
  new SliverAppBar(
    expandedHeight: _kFlexibleSpaceMaxHeight,
    flexibleSpace: new FlexibleSpaceBar(.....)
    ...                   
    bottom: new BottomBar(...), // has to have preferredSize
  ),
  ...

class BottomBar extends StatelessWidget implements PreferredSizeWidget {
    ...
    @override
      Size get preferredSize {
        return new Size.fromHeight(my_constant_height_value);
      }

    ...
    }

我想将文本放在底部小部件中,但我不知道它内部的文本有多长。

如何实现底部小部件的动态高度?

有没有办法在小部件布局之前测量小部件的高度?

编辑 25/04/2018

最终,我按照Thibault的说明操作,并最终得到了这个:

// 'as rendering' to avoid conflict with 'package:intl/intl.dart'
import 'package:flutter/rendering.dart' as rendering; 

...

// this is the function that returns the height of a Text widget
// given the text
double getHeight(String text, BuildContext context, bool isTitle) {
  var rp = rendering.RenderParagraph(
    new TextSpan(
        style: isTitle
            ? Theme.of(context).primaryTextTheme.title
            : Theme.of(context).primaryTextTheme.subhead,
        text: text,
        children: null,
        recognizer: null),

    // important as the user can have increased text on his device
    textScaleFactor: MediaQuery.of(context).textScaleFactor, 

    textDirection: rendering.TextDirection.ltr,
  );
  var horizontalPaddingSum = 20; // optional 
  var width = MediaQuery.of(context).size.width - horizontalPaddingSum;
  // if your Text widget has horizontal padding then you have to 
  // subtract it from available width to get the needed results
  var ret = rp.computeMinIntrinsicHeight(width);
  return ret;
}

...


  _kPreferredBBTextHeight =
      getHeight(mTitle ?? "", context, true);

  var verticalPaddingSum = 10;
  _kPreferredBBSubTextHeight = getHeight(mSubtitle ?? "", context,false) + verticalPaddingSum;

  _kPreferredBottomBarSize =
      _kPreferredBBTextHeight + _kPreferredBBSubTextHeight + 48;

  _kFlexibleSpaceMaxHeight =
      _kPreferredBottomBarSize + _kPreferredBottomBarSize + kToolbarHeight;

  _backgroudBottomPadding = _kPreferredBottomBarSize;

...
new CustomSliverAppBar(
                pinned: true,
                automaticallyImplyLeading: false,
                primary: true,
                expandedHeight: _kFlexibleSpaceMaxHeight,
                flexibleSpace: new FlexibleSpaceBar(
                  background: new Padding(
                      padding:
                          new EdgeInsets.only(bottom: _backgroudBottomPadding),
                      child: new Image(
                        image: new NetworkImage(mImageUrl),
                        fit: BoxFit.cover,
                      )),
                ),
                bottom: new BottomBar(
                  fixedHeight: _kPreferredBottomBarSize,
                ),
              ),

...

class BottomBar extends StatelessWidget implements PreferredSizeWidget {
  final double fixedHeight;

  BottomBar({this.fixedHeight});

  @override
  Size get preferredSize {
    return new Size.fromHeight(this.fixedHeight);
  }

  @override
  Widget build(BuildContext context) {
    // https://github.com/flutter/flutter/issues/3782
    return new Container(
        height: this.fixedHeight,
        child: new Material(
            color: Theme.of(context).primaryColor,
            child: new Column(
              children: <Widget>[
                new Row(
                  children: <Widget>[
                    new IconButton(
                      icon: new Icon(Icons.arrow_back, color: Colors.white),
                      onPressed: () {
                        Navigator.of(context).pop();
                      },
                    ),
                    new Expanded(
                      child: new Container(),
                    ),
                    new IconButton(
                      icon: new Icon(Icons.share, color: Colors.white),
                      onPressed: () {
                        print("share pressed");
                      },
                    )
                  ],
                ),
                new Column(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: <Widget>[
                    new Padding(
                        padding: new EdgeInsets.only(left: 10.0, right: 10.0),
                        child: new Container(
                          child: new Container(
                            alignment: Alignment.centerLeft,
                            child: new Text(
                              mTitle ?? "",
                              style: Theme.of(context).primaryTextTheme.title,
                            ),
                          ),
                        )),
                    new Container(
                      padding: new EdgeInsets.only(
                          left: 10.0, right: 10.0, top: 5.0, bottom: 5.0),
                      alignment: Alignment.centerLeft,
                      child: new Text(
                        mSubtitle ?? "",
                        style: Theme.of(context).primaryTextTheme.subhead,
                      ),
                    ),
                  ],
                ),
              ],
            )));
  }
5个回答

4
PreferredSizeWidget 的整个意义在于,不能 动态调整此小部件的大小。
原因是 Scaffold 使用首选大小来进行一些计算。如果 appbar 大小直到呈现之前才知道,这将是不可能的。
您需要相应地重新考虑您的 UI。

2
如果我能确定给定宽度和文本样式的 Text 组件的高度,那么在这种情况下将非常有用。 - Jerzy Kiler
1
@JerzyKiler 在文本小部件中使用“textScaleFactor: 1”使其字体大小独立于设备字体大小。 - Omar Fayad

4
有没有一种方法在 widget 布局之前测量其高度? 一般情况下,您可以在 build() 方法中构建 UI 时使用 LayoutBuilder,但这可能对您在此情况下不起作用。 在这里,您可以尝试使用 RenderParagraph 渲染文本并在构建 Scaffold 之前测量其大小。您可以使用屏幕宽度作为宽度约束,布置 RenderParagraph,检索高度,并将其用作首选大小。 也就是说,如果您的文本在 Scaffold 生命周期内发生更改,则无法稍后更改首选高度。

3

我的小部件尺寸不是静态的,所以我需要另一种解决方法。我借助于SizeChangedLayoutNotification改进了mahdi shahbazi的答案。感谢他的帮助,SizedBox.shrink方法非常巧妙。

class DynamicSliverAppBar extends StatefulWidget {
  final Widget child;
  final double maxHeight;

  const DynamicSliverAppBar({
    required this.child,
    required this.maxHeight,
    Key? key,
  }) : super(key: key);

  @override
  _DynamicSliverAppBarState createState() => _DynamicSliverAppBarState();
}

class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
  final GlobalKey _childKey = GlobalKey();
  bool isHeightCalculated = false;
  double? height;

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (!isHeightCalculated) {
        isHeightCalculated = true;
        setState(() {
          height = (_childKey.currentContext?.findRenderObject() as RenderBox)
              .size
              .height;
        });
      }
    });

    return SliverAppBar(
      expandedHeight: isHeightCalculated ? height : widget.maxHeight,
      flexibleSpace: FlexibleSpaceBar(
        background: Column(
          children: [
            NotificationListener<SizeChangedLayoutNotification>(
              onNotification: (notification) {
                WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
                  isHeightCalculated = true;
                  setState(() {
                    height = (_childKey.currentContext?.findRenderObject()
                            as RenderBox)
                        .size
                        .height;
                  });
                });
                return false;
              },
              child: SizeChangedLayoutNotifier(
                child: Container(
                  key: _childKey,
                  child: widget.child,
                ),
              ),
            ),
            const Expanded(
              child: SizedBox.shrink(),
            )
          ],
        ),
      ),
    );
  }
}

1
您可以使用Spacer()函数。 - Ng Lok Chun
1
这个解决方案非常不错,但在交互测试时遇到了性能问题。我使用 device_preview 进行了测试,并通过极端变化屏幕大小的方式进行了测试。 - Septian Dika

3

您可以使用此小部件作为解决此问题的变通方法。

class DynamicSliverAppBar extends StatefulWidget {
  final Widget child;
  final double maxHeight;

  DynamicSliverAppBar({
    @required this.child,
    @required this.maxHeight,
    Key key,
  }) : super(key: key);

  @override
  _DynamicSliverAppBarState createState() => _DynamicSliverAppBarState();
}

class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
  final GlobalKey _childKey = GlobalKey();
  bool isHeightCalculated = false;
  double height;

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (!isHeightCalculated) {
        isHeightCalculated = true;
        setState(() {
          height = (_childKey.currentContext.findRenderObject() as RenderBox)
              .size
              .height;
        });
      }
    });

    return SliverAppBar(
      expandedHeight: isHeightCalculated ? height : widget.maxHeight,
      flexibleSpace: FlexibleSpaceBar(
        background: Column(
          children: [
            Container(
              key: _childKey,
              child: widget.child,
            ),
            Expanded(child: SizedBox.shrink()),
          ],
        ),
      ),
    );
  }
}

它适用于静态小部件。感谢您的答案,我发布了一种新的方法,适用于动态大小的小部件。 - Ataberk

0
我使用以下代码来解决这个问题。 toolbarHeight 是文本高度(是动态的)。
注意:此页面会渲染两次。
  var toolbarHeight;
  BuildContext? renderBoxContext;

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
      var renderBox = renderBoxContext?.findRenderObject() as RenderBox;
      toolbarHeight = renderBox.size.height;
      setState(() {});
    });
  }

@override
  Widget build(BuildContext context) {
    
    return Material(
          child: getBody(context),
        );
  }


getBody(BuildContext context) {
  var mediaQuery = MediaQuery.of(context).size;
  state.toolbarHeight ??= mediaQuery.height;

  return SizedBox(
    width: mediaQuery.width,
    height: mediaQuery.height,
    child: CustomScrollView(
      slivers: <Widget>[

        SliverAppBar(
          pinned: false,
          floating: true,
          snap: false,
          backwardsCompatibility: true,
          centerTitle: true,
          bottom: PreferredSize(
            preferredSize: Size(mediaQuery.width, state.toolbarHeight),
            child: Builder(
              builder: (ctx){
                state.renderBoxContext = ctx;

                return Align(
                  alignment: Alignment.topLeft,
                  child: ColoredBox(
                    color: Colors.green,
                    child: Text('line1\nline2\nline3'),
                  ),
                );
              },
            ),
          ),
          flexibleSpace: FlexibleSpaceBar(
            title: Text('FlexibleSpaceBar'),
            centerTitle: true,
            collapseMode: CollapseMode.pin,
          ),
        ),

        SliverPadding(
          padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
          sliver: SliverFixedExtentList(
            itemExtent: 110,
            delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    return Text('   item  $index');
                  },
              childCount: 10,
            ),
          ),
        ),
      ],
    ),
  );
}

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