SliverPersistentHeaderDelegate not fully collapsed

1,826

When it's possible..Use dependend on parent size widgets insted of calcualting children sizes manualy.

  1. FittedBox. Can scale the font size of the children widget according on parents size and fit parameter.
  2. ConstrainedBox, BoxConstraints.tightFor
  3. Expanded. Probably you know how it works.

enter image description here

import 'dart:math';

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        body: SafeArea(
          child: MyHomePage(),
        ),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        SliverPersistentHeader(
          pinned: true,
          floating: false,
          delegate: DashboardHeaderPersistentDelegate(),
        ),
        SliverList(
          delegate: SliverChildBuilderDelegate(
            (_, i) => Card(
              margin: const EdgeInsets.all(10),
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    Expanded(
                      flex: 1,
                      child: Text(
                        i.toString(),
                      ),
                    ),
                    const Expanded(
                      flex: 3,
                      child: Text('Text'),
                    ),
                  ],
                ),
              ),
            ),
            childCount: 100,
          ),
        ),
      ],
    );
  }
}

const categories = [
  'Grocieries',
  'Transport',
  'House Rent',
  'Shopping',
  'Career'
];
const categoriesIcons = [
  Icons.ac_unit,
  Icons.access_alarms,
  Icons.dashboard,
  Icons.accessible_forward,
  Icons.backspace,
];

class DashboardHeaderPersistentDelegate extends SliverPersistentHeaderDelegate {
  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    double shrinkPercentage = min(1, shrinkOffset / (maxExtent - minExtent));

    return Container(
      decoration: BoxDecoration(
        border: Border.all(
          color: Colors.blueAccent,
        ),
      ),
      child: Material(
        elevation: 0,
        shadowColor: Colors.white,
        child: SafeArea(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              ConstrainedBox(
                constraints: BoxConstraints.tightFor(
                  height: max(
                    60,
                    100 * (1 - shrinkPercentage),
                  ),
                ),
                child: FittedBox(
                  child: Container(
                    padding: EdgeInsets.all(20),
                    width: 200,
                    child: const Text(
                      '\$ 5329.05',
                      style: TextStyle(
                        fontFamily: 'Barlow',
                        fontSize: 30,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                  ),
                ),
              ),
              Expanded(
                child: Stack(
                  alignment: Alignment.bottomCenter,
                  children: [
                    if (shrinkPercentage != 1)
                      Opacity(
                        opacity: 1 - shrinkPercentage,
                        child: _buildInformationWidget(context),
                      ),
                    if (shrinkPercentage != 0)
                      Opacity(
                        opacity: shrinkPercentage,
                        child: Padding(
                          padding: const EdgeInsets.symmetric(horizontal: 8),
                          child: _buildCollapsedInformationWidget(),
                        ),
                      ),
                  ],
                ),
              )
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildInformationWidget(BuildContext context) => ClipRect(
        child: OverflowBox(
          maxWidth: double.infinity,
          maxHeight: double.infinity,
          child: FittedBox(
            fit: BoxFit.fitWidth,
            alignment: Alignment.center,
            child: Container(
              width: MediaQuery.of(context).size.width,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.center,
                children: <Widget>[
                  const Text(
                    'AVAILABLE BALANCE',
                    style: TextStyle(
                        fontSize: 12,
                        fontWeight: FontWeight.w900,
                        color: Colors.black26),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 16),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: <Widget>[
                        Container(
                          width: 100,
                          child: Text(
                            '\$ 11200',
                            textAlign: TextAlign.right,
                            style: TextStyle(
                              fontFamily: 'Barlow',
                              fontSize: 18,
                              fontWeight: FontWeight.w700,
                              color: Colors.green[400],
                            ),
                          ),
                        ),
                        const Text(
                          ' I ',
                          style: TextStyle(
                            fontSize: 20,
                            color: Colors.black12,
                          ),
                        ),
                        Container(
                          width: 100,
                          child: Text(
                            '\$ 400',
                            style: TextStyle(
                              fontFamily: 'Barlow',
                              fontSize: 18,
                              fontWeight: FontWeight.w700,
                              color: Colors.red[400],
                            ),
                          ),
                        )
                      ],
                    ),
                  ),
                  Container(
                    margin: EdgeInsets.only(left: 12, top: 12),
                    alignment: Alignment.centerLeft,
                    child: const Text(
                      "CATEGORIES",
                      style: TextStyle(
                        fontSize: 12,
                        fontWeight: FontWeight.w900,
                        color: Colors.black26,
                      ),
                    ),
                  ),
                  Container(
                    height: 88,
                    child: ListView.builder(
                      scrollDirection: Axis.horizontal,
                      itemCount: categories.length,
                      itemBuilder: (context, index) {
                        return Padding(
                          padding: EdgeInsets.only(
                              left: (index == 0) ? 24.0 : 8.0,
                              right: (index == categories.length - 1)
                                  ? 24.0
                                  : 8.0),
                          child: _buildCategoryItem(
                            categoriesIcons[index],
                            categories[index],
                            .9,
                          ),
                        );
                      },
                    ),
                  )
                ],
              ),
            ),
          ),
        ),
      );

  Widget _buildCollapsedInformationWidget() => Row(
        children: [
          const Text("Recent"),
          const Spacer(),
          Container(
            child: Text(
              '\$ 11200',
              textAlign: TextAlign.right,
              style: TextStyle(
                fontFamily: 'Barlow',
                fontSize: 14,
                fontWeight: FontWeight.w700,
                color: Colors.green[400],
              ),
            ),
          ),
          const Text(
            ' I ',
            style: TextStyle(
              fontSize: 20,
              color: Colors.black12,
            ),
          ),
          Container(
            child: Text(
              '\$ 400',
              style: TextStyle(
                fontFamily: 'Barlow',
                fontSize: 14,
                fontWeight: FontWeight.w700,
                color: Colors.red[400],
              ),
            ),
          )
        ],
      );

  Widget _buildCategoryItem(
          IconData data, String categoryTitle, double percentage) =>
      Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Stack(
            alignment: Alignment.center,
            children: <Widget>[
              Container(
                decoration: BoxDecoration(
                  border: Border.all(
                    width: 1,
                    color: Colors.black12,
                  ),
                  borderRadius: BorderRadius.circular(28),
                  color: Colors.blue[400],
                ),
                child: Padding(
                  padding: const EdgeInsets.all(10.0),
                  child: Icon(
                    data,
                    size: 28,
                    color: Colors.white,
                  ),
                ),
              ),
              Container(
                width: 40,
                height: 40,
                child: CircularProgressIndicator(
                  value: percentage,
                  strokeWidth: 2,
                  valueColor: AlwaysStoppedAnimation(Colors.white),
                ),
              )
            ],
          ),
          Container(
            width: 72,
            alignment: Alignment.center,
            child: Text(
              categoryTitle,
              overflow: TextOverflow.ellipsis,
              textAlign: TextAlign.center,
              maxLines: 1,
              style: const TextStyle(
                fontSize: 14,
                fontWeight: FontWeight.w400,
                color: Colors.black45,
              ),
            ),
          )
        ],
      );

  @override
  double get maxExtent => 300;

  @override
  double get minExtent => 80;

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


    
Share:
1,826
Hohenheim
Author by

Hohenheim

I am Hohenheim Ironborne, working full-time in FullSpeed Technologies as an Android Developer in Cebu PH. Currently digesting rxjava2+kotlin+mvp+dagger2 for the upcoming project. Im also touching a bit of Image Processing specifically on face detection and face landmark detection. Just recently discovered that I like listening to epic classical musics played by an orchestra.

Updated on November 25, 2022

Comments

  • Hohenheim
    Hohenheim over 1 year

    I'm having a hard time making a custom collapsing toolbar, attached below is a video for a normal case.

    no problem but not snappy

    Then here's a screen record of the misbehavior, most of the time this happens.

    not collapsed

    Aside from the fact that the scrolling is not so snappy, you'll see in the second video that the sliver on the top is not completely collapsed.

    Do you have any suggestion to improve the performance of the app and a solution for the bug?

    here's my code inside the SliverPersistentHeaderDelegate

    class DashboardHeaderPersistentDelegate extends SliverPersistentHeaderDelegate {
    
    ...
    
      @override
      Widget build(
          BuildContext context, double shrinkOffset, bool overlapsContent) {
    
        double shrinkPercentage = min(1, shrinkOffset / (maxExtent - minExtent));
        double titleTopMargin = titleCollapsedTopPadding +
            (titleExpandedTopPadding - titleCollapsedTopPadding) *
                (1 - shrinkPercentage);
        double titleFontSize = titleCollapsedFontSize +
            (titleExpandedFontSize - titleCollapsedFontSize) *
                (1 - shrinkPercentage);
        double infoWidgetHeight = minExtent +
            (maxExtent - minExtent) -
            shrinkOffset -
            titleTopMargin -
            titleFontSize -
            44;
        double collapasedInfoOpacity = max(0, shrinkPercentage-.7)/.3;
    
        return Material(
          elevation: 0,
          shadowColor: Colors.white,
          child: SafeArea(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Container(
                  height: titleFontSize,
                  alignment: Alignment.center,
                  child: Text(
                    '\$ 5329.05',
                    style: TextStyle(
                        fontFamily: 'Barlow',
                        fontSize: titleFontSize,
                        fontWeight: FontWeight.w500),
                  ),
                  margin: EdgeInsets.only(top: titleTopMargin, bottom: 8),
                ),
                Container(
                  height: shrinkPercentage == 1 ? 20 : infoWidgetHeight,
                  width: MediaQuery.of(context).size.width,
                  alignment: Alignment.center,
                  child: Stack(
                    alignment: Alignment.bottomCenter,
                    children: [
                      Opacity(
                        opacity: 1 - shrinkPercentage,
                        child: _buildInformationWidget(context),
                      ),
                      Opacity(
                        opacity: collapasedInfoOpacity,
                        child: Padding(
                          padding: const EdgeInsets.symmetric(horizontal: 8),
                          child: _buildCollapsedInformationWidget(),
                        ),
                      )
                    ],
                  ),
                )
              ],
            ),
          ),
        );
      }
    
      Widget _buildInformationWidget(BuildContext context) => ClipRect(
            child: OverflowBox(
              maxWidth: double.infinity,
              maxHeight: double.infinity,
              child: FittedBox(
                fit: BoxFit.fitWidth,
                alignment: Alignment.center,
                child: Container(
                  width: MediaQuery.of(context).size.width,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: <Widget>[
                      Text(
                        'AVAILABLE BALANCE',
                        style: TextStyle(
                            fontSize: 12,
                            fontWeight: FontWeight.w900,
                            color: Colors.black26),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(top: 16),
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          crossAxisAlignment: CrossAxisAlignment.center,
                          children: <Widget>[
                            Container(
                              width: 100,
                              child: Text(
                                '\$ 11200',
                                textAlign: TextAlign.right,
                                style: TextStyle(
                                    fontFamily: 'Barlow',
                                    fontSize: 18,
                                    fontWeight: FontWeight.w700,
                                    color: Colors.green[400]),
                              ),
                            ),
                            Text(
                              ' I ',
                              style: TextStyle(fontSize: 20, color: Colors.black12),
                            ),
                            Container(
                              width: 100,
                              child: Text(
                                '\$ 400',
                                style: TextStyle(
                                    fontFamily: 'Barlow',
                                    fontSize: 18,
                                    fontWeight: FontWeight.w700,
                                    color: Colors.red[400]),
                              ),
                            )
                          ],
                        ),
                      ),
                      Container(
                        margin: EdgeInsets.only(left: 12, top: 12),
                        alignment: Alignment.centerLeft,
                        child: Text(
                          "CATEGORIES",
                          style: TextStyle(
                              fontSize: 12,
                              fontWeight: FontWeight.w900,
                              color: Colors.black26),
                        ),
                      ),
                      Container(
                        height: 88,
                        child: ListView.builder(
                            scrollDirection: Axis.horizontal,
                            itemCount: categories.length,
                            itemBuilder: (context, index) {
                              return Padding(
                                padding: EdgeInsets.only(
                                    left: (index == 0) ? 24.0 : 8.0,
                                    right: (index == categories.length - 1)
                                        ? 24.0
                                        : 8.0),
                                child: _buildCategoryItem(
                                    categoriesIcons[index], categories[index], .9),
                              );
                            }),
                      )
                    ],
                  ),
                ),
              ),
            ),
          );
    
      Widget _buildCollapsedInformationWidget() => Row(
            children: [
              Text("Recent"),
              Spacer(),
              Container(
                child: Text(
                  '\$ 11200',
                  textAlign: TextAlign.right,
                  style: TextStyle(
                      fontFamily: 'Barlow',
                      fontSize: 14,
                      fontWeight: FontWeight.w700,
                      color: Colors.green[400]),
                ),
              ),
              Text(
                ' I ',
                style: TextStyle(fontSize: 20, color: Colors.black12),
              ),
              Container(
                child: Text(
                  '\$ 400',
                  style: TextStyle(
                      fontFamily: 'Barlow',
                      fontSize: 14,
                      fontWeight: FontWeight.w700,
                      color: Colors.red[400]),
                ),
              )
            ],
          );
    
      Widget _buildCategoryItem(
              IconData data, String categoryTitle, double percentage) =>
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Stack(
                alignment: Alignment.center,
                children: <Widget>[
                  Container(
                    decoration: BoxDecoration(
                        border: Border.all(width: 1, color: Colors.black12),
                        borderRadius: BorderRadius.circular(28),
                        color: Colors.blue[400]),
                    child: Padding(
                      padding: const EdgeInsets.all(10.0),
                      child: Icon(
                        data,
                        size: 28,
                        color: Colors.white,
                      ),
                    ),
                  ),
                  Container(
                    width: 40,
                    height: 40,
                    child: CircularProgressIndicator(
                      value: percentage,
                      strokeWidth: 2,
                      valueColor: AlwaysStoppedAnimation(Colors.white),
                    ),
                  )
                ],
              ),
              Container(
                width: 72,
                alignment: Alignment.center,
                child: Text(categoryTitle,
                    overflow: TextOverflow.ellipsis,
                    textAlign: TextAlign.center,
                    maxLines: 1,
                    style: TextStyle(
                        fontSize: 14,
                        fontWeight: FontWeight.w400,
                        color: Colors.black45)),
              )
            ],
          );
    
    ...
    
    }
    
  • Hohenheim
    Hohenheim over 3 years
    Hey! Thanks, your solution definitely aided the laggy scrolling and the bug. This was three months ago but still helpful, Flutter community must be growing.