How to change pinned of Sliver Persistent Header
Asked Answered
L

3

6

I am using 2 Sliver Headers above GridView and a ListView on a CustomScrollView . I want only 1 of the headers (the one I am scrolling over) to be pinned When I scroll down. I want to be able to scroll down and only one of the headers is pinned when I pass over Gridview.

EDIT: Added _SliverAppBarDelegate

Scaffold(
body: SafeArea(
        child: DefaultTabController(
          length: 2,
          child: CustomScrollView(
            slivers: [              
              makeHeader('Categories', false),
              SliverGrid(
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2,
                  childAspectRatio: 1.5,
                ),
                delegate: SliverChildBuilderDelegate(
                    (context, index) => Container(
                          margin: EdgeInsets.all(5.0),
                          color: Colors.blue,
                        ),
                    childCount: 10),
              ),
              makeHeader('Watchlist', false),
              SliverGrid(
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2,
                  childAspectRatio: 1.5,
                ),
                delegate: SliverChildBuilderDelegate(
                    (context, index) => Container(
                          margin: EdgeInsets.all(5.0),
                          color: Colors.red,
                        ),
                    childCount: 10),
              ),
            ],
          ),
        ),
  ),
)


SliverPersistentHeader makeHeader(String headerText, bool pinned) {
  return SliverPersistentHeader(
    pinned: pinned,
     floating: true,
    delegate: _SliverAppBarDelegate(
      minHeight: 40.0,
      maxHeight: 60.0,
      child: Container(
          child: Text(
            headerText,
            style: TextStyle(fontSize: 24, color: Colors.green,fontWeight: FontWeight.bold),
          )),
    ),
  );
}

///////////////////////////EDIT

class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate({
    @required this.minHeight,
    @required this.maxHeight,
    this.child,
  });
  final double minHeight;
  final double maxHeight;
  final Widget child;
  @override
  double get minExtent => minHeight;
  @override
  double get maxExtent => math.max(maxHeight, minHeight);
  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return new SizedBox.expand(child: child);
  }

  @override
  bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight ||
        minHeight != oldDelegate.minHeight ||
        child != oldDelegate.child;
  }
}
Legacy answered 1/8, 2019 at 15:13 Comment(1)
what's the code of _SliverAppBarDelegateNativeborn
I
4

This is an old question but I'm putting my solution here in case anyone else needs the sticky header effect without a plugin.

My solution is to have the sliver headers' minExtent value in a map where the key is the number of sliverList items above the header.

final _headersMinExtent = <int, double>{};

We can then reduce the minExtent when the header needs to be pushed out of the view. To accomplish that we listen to the scrollController. Note that we could also update the pinned status but we wouldn't get a transition.

We calculate the minExtent from :

  • The number of items above the header : key.
  • The number of headers already pushed out the view : n.
_scrollListener() {
    var n = 0;
    setState(() {
      _headersMinExtent.forEach((key, value) {
        _headersMinExtent[key] = (key * 30 + n * 40 + 190 - _scrollController.offset).clamp(0, 40);
        n++;
      });
    });
  }

When we construct our widget list we have to pass the minExtent parameter to the SliverPersistentHeaderDelegate :

List<Widget> _constructList() {
    var widgetList = <Widget>[];

    for (var i = 0; i < itemList.length; i++) {
      // We want a header every 5th item
      if (i % 5 == 0) {
        // Don't forget to init the minExtent value.
        _headersMinExtent[i] = _headersMinExtent[i] ?? 40;
        // We pass the minExtent as a parameter.
        widgetList.add(SliverPersistentHeader(pinned: true, delegate: HeaderDelegate(_headersMinExtent[i]!)));
      }
      widgetList.add(SliverList(
          delegate: SliverChildBuilderDelegate(
        (context, index) => Container(
          decoration: BoxDecoration(color: Colors.yellow, border: Border.all(width: 0.5)),
          height: 30,
        ),
        childCount: 1,
      )));
    }
    return widgetList;
}

This is the result :

screen capture

Full application code :

import 'package:flutter/material.dart';

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

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _scrollController = ScrollController();
  final _headersMinExtent = <int, double>{};
  final itemList = List.filled(40, "item");

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      _scrollListener();
    });
  }

  _scrollListener() {
    var n = 0;
    setState(() {
      _headersMinExtent.forEach((key, value) {
        _headersMinExtent[key] = (key * 30 + n * 40 + 190 - _scrollController.offset).clamp(0, 40);
        n++;
      });
    });
  }

  List<Widget> _constructList() {
    var widgetList = <Widget>[];

    for (var i = 0; i < itemList.length; i++) {
      // We want a header every 5th item
      if (i % 5 == 0) {
        // Don't forget to init the minExtent value.
        _headersMinExtent[i] = _headersMinExtent[i] ?? 40;
        // We pass the minExtent as a parameter.
        widgetList.add(SliverPersistentHeader(pinned: true, delegate: HeaderDelegate(_headersMinExtent[i]!)));
      }
      widgetList.add(SliverList(
          delegate: SliverChildBuilderDelegate(
        (context, index) => Container(
          decoration: BoxDecoration(color: Colors.yellow, border: Border.all(width: 0.5)),
          height: 30,
        ),
        childCount: 1,
      )));
    }
    return widgetList;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(),
        body: CustomScrollView(
          controller: _scrollController,
          slivers: _constructList(),
        ),
      ),
    );
  }
}

class HeaderDelegate extends SliverPersistentHeaderDelegate {
  final double _minExtent;
  HeaderDelegate(this._minExtent);

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      decoration: BoxDecoration(color: Colors.green, border: Border.all(width: 0.5)),
    );
  }

  @override
  double get minExtent => _minExtent;

  @override
  double get maxExtent => 40;

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

class ListChildDelegate extends SliverChildDelegate {
  @override
  Widget? build(BuildContext context, int index) {
    // TODO: implement build
    throw UnimplementedError();
  }

  @override
  bool shouldRebuild(covariant SliverChildDelegate oldDelegate) => true;
}
Illogicality answered 29/9, 2021 at 8:31 Comment(0)
L
2

@diegoveloper I found this plugin. https://pub.dev/packages/flutter_sticky_header I wish there is still an easier way to do it without a plugin. However, this plugin exactly solves the issue I am describing.

Legacy answered 1/8, 2019 at 17:44 Comment(0)
H
0

For anyone that's wondering the same thing now, there's a new set of widgets, SliverMainAxisGroup, SliverCrossAxisGroup, and SliverCrossAxisExpanded, that will allow you to do this easily without a plugin.

https://github.com/flutter/flutter/pull/126596

https://api.flutter.dev/flutter/widgets/SliverMainAxisGroup-class.html

You can copy paste this code example in the code playground inside the doc linked to see its behavior:

import 'package:flutter/material.dart';

void main() => runApp(const SliverMainAxisGroupExampleApp());

class SliverMainAxisGroupExampleApp extends StatelessWidget {
  const SliverMainAxisGroupExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('SliverMainAxisGroup Sample')),
        body: const SliverMainAxisGroupExample(),
      ),
    );
  }
}

class SliverMainAxisGroupExample extends StatelessWidget {
  const SliverMainAxisGroupExample({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: <Widget>[
        SliverMainAxisGroup(
          slivers: <Widget>[
            const SliverAppBar(
              title: Text('Section Title 1'),
              expandedHeight: 70.0,
              pinned: true,
            ),
            SliverList.builder(
              itemBuilder: (BuildContext context, int index) {
                return Container(
                  color: index.isEven ? Colors.amber[300] : Colors.blue[300],
                  height: 100.0,
                  child: Center(
                    child: Text(
                      'Item $index',
                      style: const TextStyle(fontSize: 24),
                    ),
                  ),
                );
              },
              itemCount: 5,
            ),
            SliverToBoxAdapter(
              child: Container(
                color: Colors.cyan,
                height: 100,
                child: const Center(
                  child: Text('Another sliver child',
                      style: TextStyle(fontSize: 24)),
                ),
              ),
            ),
          ],
        ),
        SliverMainAxisGroup(
          slivers: <Widget>[
            const SliverAppBar(
              title: Text('Section Title 2'),
              expandedHeight: 70.0,
              pinned: true,
            ),
            SliverList.builder(
              itemBuilder: (BuildContext context, int index) {
                return Container(
                  color: index.isEven ? Colors.amber[300] : Colors.blue[300],
                  height: 100.0,
                  child: Center(
                    child: Text(
                      'Item $index',
                      style: const TextStyle(fontSize: 24),
                    ),
                  ),
                );
              },
              itemCount: 5,
            ),
            SliverToBoxAdapter(
              child: Container(
                color: Colors.cyan,
                height: 100,
                child: const Center(
                  child: Text('Another sliver child',
                      style: TextStyle(fontSize: 24)),
                ),
              ),
            ),
          ],
        ),
        SliverToBoxAdapter(
          child: Container(
            height: 1000,
            decoration: const BoxDecoration(color: Colors.greenAccent),
            child: const Center(
              child: Text('Hello World!', style: TextStyle(fontSize: 24)),
            ),
          ),
        ),
      ],
    );
  }
}
Horick answered 24/10 at 19:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.