skip to Main Content

I have a layout with three Slivers:

  1. A SliverAppBar (not pinned).
  2. A second SliverAppBar (pinned, containing a SearchBar).
  3. A SliverList.

The issue is with the second SliverAppBar. I want it to be pinned below the status bar, respecting the safe area, but currently, it goes under the status bar, as shown in the second image:

reality vs expectation sliverappbar

The first drawing represents the default state on launch.
When scrolling, the first SliverAppBar disappears as expected, and the second SliverAppBar (with the SearchBar) gets pinned at the top.
However, I want the second SliverAppBar to stop just below the status bar instead of going behind it.

What I’ve Tried:

  1. Wrapping the entire CustomScrollView in a SafeArea:

    • This also shifts my first SliverAppBar, which I don’t want.
  2. Wrapping only the second SliverAppBar in a SafeArea or SliverSafeArea:

    • This adds padding between the two SliverAppBars, which is not the desired behavior.
  3. Dynamic padding using a SliverPersistentHeader and working with its shrinkOffset:

    • However, shrinkOffset starts growing only when the header reaches the top of the screen, so it doesn’t solve the problem.
  4. Using only 1 SliverAppBar with a flexibleSpace and a bottom:

    • This is close to the result I need but I didn’t manage to get the proper sizes.

Sample code

class MyPage extends StatefulWidget {
  const MyPage({super.key});

  @override
  State<MyPage> createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> {
  late ScrollController _scrollController;

  @override
  void initState() {
    _scrollController = ScrollController();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PrimaryScrollController(
        controller: _scrollController,
        child: CustomScrollView(
          slivers: <Widget>[
            SliverAppBar(
              pinned: false,
              expandedHeight: 200,
              flexibleSpace: FlexibleSpaceBar(
                background: Image.network(
                  'https://picsum.photos/250?image=9',
                  fit: BoxFit.cover,
                ),
              ),
            ),
            const SliverAppBar(
              pinned: true,
              flexibleSpace: Padding(
                padding: EdgeInsets.only(top: 12.0),
                child: SearchBar(),
              ),
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return ListTile(
                    title: Text('Item $index'),
                  );
                },
                childCount: 20,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

How can I achieve this behavior? Is there a proper way to pin a SliverAppBar while respecting the safe area for just that Sliver?

2

Answers


  1. enter image description here
    1、try this:

    Widget buildPage1() {
        MediaQueryData mediaQuery = MediaQuery.of(context);
        final min = mediaQuery.viewPadding.top + kToolbarHeight;
        final max = 200.0;
    
        return Scaffold(
          body: CustomScrollView(
            slivers: [
              NSliverPersistentHeaderBuilder(
                pinned: true,
                min: min,
                max: max,
                builder: (BuildContext context, double shrinkOffset, bool overlapsContent) {
                  final double opacity = 1 - (shrinkOffset / (max - min));
    
                  return Container(
                    alignment: Alignment.bottomCenter,
                    decoration: BoxDecoration(
                      color: Colors.blue,
                      image: DecorationImage(
                        opacity: opacity,
                        image: ExtendedNetworkImageProvider(
                          'https://picsum.photos/250?image=9',
                        ),
                        fit: BoxFit.cover,
                      ),
                    ),
                    // child: Text(
                    //   "Pinned Header ${{
                    //     "shrinkOffset": shrinkOffset.toStringAsFixed(2),
                    //   }} ",
                    //   style: TextStyle(color: Colors.white, fontSize: 20),
                    // ),
                    child: SearchBar(
                      hintText: "search",
                    ),
                  );
                },
              ),
              // 用 SliverList 填充内容
              SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) => ListTile(
                    title: Text('Item #$index'),
                  ),
                  childCount: 20,
                ),
              ),
            ],
          ),
        );
      }
    

    2、NSliverPersistentHeaderBuilder

    /// custom SliverPersistentHeader
    class NSliverPersistentHeaderBuilder extends SliverPersistentHeader {
      NSliverPersistentHeaderBuilder({
        Key? key,
        double max = 48,
        double min = 48,
        bool pinned = false,
        bool floating = false,
        required Widget Function(
                BuildContext context, double shrinkOffset, bool overlapsContent)
            builder,
      }) : super(
              key: key,
              pinned: pinned,
              floating: floating,
              delegate: NSliverPersistentHeaderDelegate(
                max: max,
                min: min,
                builder: builder,
              ),
            );
    }
    
        /// 自定义 SliverPersistentHeaderDelegate
    class NSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
      NSliverPersistentHeaderDelegate({
        this.min = 48,
        this.max = 80,
        required this.builder,
      });
    
      /// 默认 48 是 TabBar 的默认高度
      double min;
      double max;
    
      Widget Function(BuildContext context, double offset, bool overlapsContent)
          builder;
    
      @override
      Widget build(
          BuildContext context, double shrinkOffset, bool overlapsContent) {
        return builder(context, shrinkOffset, overlapsContent);
      }
    
      //SliverPersistentHeader最大高度
      @override
      double get maxExtent => max;
    
      //SliverPersistentHeader最小高度
      @override
      double get minExtent => min;
    
      @override
      bool shouldRebuild(covariant NSliverPersistentHeaderDelegate oldDelegate) {
        return min != oldDelegate.min ||
            max != oldDelegate.max ||
            builder != oldDelegate.builder;
      }
    }
    
    Login or Signup to reply.
  2. Let both the shrinkage area and the search bar be implemented with SliverPersistentHeader:

    enter image description here
    enter image description here

    Widget buildPage1() {
    MediaQueryData mediaQuery = MediaQuery.of(context);
    var min = mediaQuery.viewPadding.top;
    
    final max = 200.0;
    
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          NSliverPersistentHeaderBuilder(
            pinned: true,
            min: min,
            max: max,
            builder: (BuildContext context, double shrinkOffset, bool overlapsContent) {
              final double opacity = 1 - (shrinkOffset / (max - min));
    
              return Container(
                alignment: Alignment.bottomCenter,
                decoration: BoxDecoration(
                  color: Colors.blue,
                  image: DecorationImage(
                    opacity: opacity,
                    image: ExtendedNetworkImageProvider(
                      'https://picsum.photos/250?image=9',
                    ),
                    fit: BoxFit.cover,
                  ),
                ),
                // child: Text(
                //   "Pinned Header ${{
                //     "shrinkOffset": shrinkOffset.toStringAsFixed(2),
                //   }} ",
                //   style: TextStyle(color: Colors.white, fontSize: 20),
                // ),
                // child: SearchBar(
                //   hintText: "search",
                // ),
              );
            },
          ),
          NSliverPersistentHeaderBuilder(
            pinned: true,
            min: 40,
            max: 40,
            builder: (BuildContext context, double shrinkOffset, bool overlapsContent) {
              return Container(
                padding: EdgeInsets.symmetric(horizontal: 12),
                decoration: BoxDecoration(
                  color: Colors.green,
                ),
                child: SearchBar(
                  backgroundColor: MaterialStateProperty.all(Colors.white),
                  hintText: "search",
                ),
              );
            },
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) {
                return ListTile(
                  title: Text('Item #$index'),
                );
              },
              childCount: 20,
            ),
          ),
        ],
      ),
    );
    

    }

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search