自动计算`SliverPersistentHeaderDelegate`的`maxExtent`

9
考虑下面的示例,旨在在CustomScrollView内呈现具有潜在自定义长/短文本的固定标题栏。
class TitleBar extends StatelessWidget {
  TitleBar(this.text);

  final String text;

  @override
  Widget build(BuildContext context) => Text(
        text,
        style: TextStyle(fontSize: 30),
        maxLines: 3,
        overflow: TextOverflow.ellipsis,
      );
}

class TitleBarDelegate extends SliverPersistentHeaderDelegate {
  final String text;

  TitleBarDelegate(this.text);

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => TitleBar(text);

  @override
  bool shouldRebuild(TitleBarDelegate oldDelegate) => oldDelegate.text != text;

  @override
  double get maxExtent => ???;

  @override
  double get minExtent => maxExtent; // doesn't shrink
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            SliverPersistentHeader(
              pinned: true,
              delegate: TitleBarDelegate('Potentially very long text'),
            ),
            SliverToBoxAdapter(
              child: Text("Foo " * 1000),
            ),
          ],
        ),
      ),
    );
  }
}

问题是:如何基于实际的TitleBar计算maxExtent。问题在于实际TitleBar的大小取决于文本,因此通常无法提前计算。

请注意,TitleBar可能具有比上面示例中更复杂的布局。因此,一般问题是如何“收缩包裹”SliverPersistentHeaderDelegate

3个回答

1

这是另一种不太好的解决方案,因为它需要建造两次。

一个小部件的高度只能在建造之后才能获取,所以想法是通过其GlobalKey在构建后获取其高度并再次构建小部件,将其包装在SliverPersistentHeader中,并将其高度设置为maxExtent。 重新构建由ValueListenableBuilder和其监听器调用。

对于下面的这个示例,我希望将计算出的高度设置为minExtentmaxExtent

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

class DynamicHeightSliverPinningHeader extends StatefulWidget {
  final Widget widget;

  const DynamicHeightSliverPinningHeader({
    super.key,
    required this.widget,
  });

  @override
  State<DynamicHeightSliverPinningHeader> createState() => _DynamicHeightSliverPinningHeaderState();
}

class _DynamicHeightSliverPinningHeaderState extends State<DynamicHeightSliverPinningHeader> {
  final _widgetKey = GlobalKey<_DynamicHeightSliverPinningHeaderState>();
  final _widgetHeightNotifier = ValueNotifier<double>(-1);

  late Widget _widget;

  @override
  void initState() {
    _widget = Container(
      key: _widgetKey,
      child: widget.widget,
    );

    SchedulerBinding.instance.addPostFrameCallback((_) {
      // This would've been done after the first frame is built
      // This should invoke the ValueListenableBuilder to rebuild 
      _widgetHeightNotifier.value = _widgetKey.currentContext!.size!.height;
    });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: _widgetHeightNotifier,
      builder: (context, _, __) {
        
        // This should be returned from the start.
        if (_widgetHeightNotifier.value == -1) {
          return SliverToBoxAdapter(
            child: _widget,
          );
        }
        
        // This should be returned after the listener detects the change.
        return SliverPersistentHeader(
          pinned: true,
          delegate: SliverPinningHeaderDelegate(
            _widget,
            _widgetHeightNotifier.value,
            _widgetHeightNotifier.value,
          ),
        );
      },
    );
  }
}

class SliverPinningHeaderDelegate extends SliverPersistentHeaderDelegate {
  final Widget widget;

  @override
  final double minExtent, maxExtent;

  const SliverPinningHeaderDelegate(
    this.widget,
    this.maxExtent,
    this.minExtent,
  );

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    return widget;
  }

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true;
}

以下是渲染行为的详细说明:

  • 首先,在SliverToBoxAdapter中呈现小部件本身(与GlobalKey绑定)
  • 在第一帧呈现后,从该GlobalKey计算高度,并将该值分配给ValueNotifier
  • 随着ValueNotifier的变化,使用该高度,ValueListenableBuilder将重建包含在SliverPersistentHeader及其委托中的小部件

1
我最近遇到了同样的问题,并想出了以下“不太优雅”的解决方案,假设标题只包含具有给定文本样式的文本,并为其提供最大宽度值。 < p > < strong > SliverPersistentHeaderDelegate < /strong > 看起来像这样:< /p >
class SliverPersistentTitleDelegate extends SliverPersistentHeaderDelegate {

SliverPersistentTitleDelegate({
    @required this.width,
    @required this.text,
    this.textStyle,
    this.padding,
    this.extend = 10
}) {
    // create a text painter
    final TextPainter textPainter = TextPainter(
        textDirection: TextDirection.ltr,
    )
    ..text = TextSpan(
        text: text,
        style: _textStyle,
    );

    // layout the text with the provided width, taking the horizontal padding into account
    final double horizontalPadding = _padding.left + _padding.right;
    textPainter.layout(maxWidth: width - horizontalPadding);

    // measure minHeight and maxHeight, taking the vertical padding and text height into account
    final double verticalPadding = _padding.top + _padding.bottom;
    _minHeight = textPainter.height + verticalPadding;
    _maxHeight = minHeight + extend;
}

final double width;
final String text;
final TextStyle textStyle;
final EdgeInsets padding;
final double extend;
double _minHeight;
double _maxHeight;

final core.ThemeProvider _themeProvider = di.get<core.ThemeProvider>();

@override
double get minExtent => _minHeight;

@override
double get maxExtent => maxHeight;

TextStyle get _textStyle => this.textStyle
    ?? _themeProvider
        .defaultTheme
        .appBarTheme
        .textTheme;

EdgeInsets get _padding => padding ?? EdgeInsets.zero;

@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent)
{
    return Padding(
        padding: _padding,
        child: Text(
            text,
            style: _textStyle ,
        ),
    );
}

@override
bool shouldRebuild(SliverPersistentTitleDelegate oldDelegate) {

    return width != oldDelegate.width
        || text != oldDelegate.text
        || textStyle != oldDelegate.textStyle
        || padding != oldDelegate.padding
        || extend != oldDelegate.extend
        || _maxHeight != oldDelegate._maxHeight
        || _minHeight != oldDelegate._minHeight;
}

}

使用这个类很简单:

return LayoutBuilder(
    builder: (context, constrains) {

    return CustomScrollView(
        slivers: <Widget>[
            SliverPersistentHeader(
                pinned: false,
                floating: true,
                delegate: SliverPersistentTitleDelegate(
                    width: constrains.maxWidth,
                    text: "Some long dynamic title",
                    textStyle: titleTextStyle,
                    padding: EdgeInsets.only(
                        left: 16,
                        right: 16,
                    ),
                ),
            )
        ],
    );
},

);


-1

我认为你可能在寻找_titleBar.preferredSize.height,但我不能确定。


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