Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automatically compute `maxExtent` of `SliverPersistentHeaderDelegate`

Consider the following example that is intended to render a pinned title bar (with a potentially custom long/short text) inside of a 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),
            ),
          ],
        ),
      ),
    );
  }
}

The question is: How can I compute maxExtent based on the actual TitleBar. The problem is that the actual TitleBar's size depends on the text and is hence to generally computable in advance.

Note that TitleBar might also have some more complex layout than it has in the example above. So the general question is how to 'shrink-wrap' an SliverPersistentHeaderDelegate.

like image 551
user3612643 Avatar asked Oct 26 '19 10:10

user3612643


1 Answers

I recently faced the same issue and came up with the following "dirty" solution, assuming the header only contains text with a given text style and you provide it with a max width value.

The SliverPersistentHeaderDelegate looks like this:

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

}

Using this class is simple:

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

);

like image 103
Cliffus Avatar answered Sep 25 '22 17:09

Cliffus