idootop/nested_scroll_view_plus

Multi headers are misbehaving

vanvixi opened this issue · 10 comments

After I changed your headerSliverBuilder to like below the error occurred:
-> Part of the text is lost

 headerSliverBuilder: (context, innerScrolled) => <Widget>[
            // use OverlapAbsorberPlus to wrap your SliverAppBar
            const OverlapAbsorberPlus(
              sliver: MySliverAppBar(),
            ),
            SliverPadding(
              padding: EdgeInsets.symmetric(horizontal: 16),
              sliver: SliverToBoxAdapter(
                child: Text(
                  'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
                  'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
                  'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.',
                  maxLines: 5,
                  textAlign: TextAlign.center,
                  overflow: TextOverflow.visible,
                ),
              ),
            ),
          ],

RPReplay_Final1699441144.MP4

Thank you for reporting the issue!

We have addressed it in the latest version, v1.0.1.

Please let me know if the problem has been resolved on your end.

@idootop
Thank you.
The problem has been resolved.

@idootop
I'm sorry for reopening issues
If I have more than 1 pinned header, the body scroll height is not correct

It appears to be a separate issue. Could you please provide a simplified demo that reproduces the problem?

@idootop
When scrolling list, the other list will be scrolled by a distance equal to the height of the TabBar pinned

Code and demo video:

MySliverTabBarDelegate:

class MySliverTabBarDelegate extends SliverPersistentHeaderDelegate {
  MySliverTabBarDelegate({required this.tabBar});

  final TabBar tabBar;

  @override
  double get minExtent => tabBar.preferredSize.height;

  @override
  double get maxExtent => tabBar.preferredSize.height;

  @override
  bool shouldRebuild(MySliverTabBarDelegate oldDelegate) => false;

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Colors.black38,
      height: tabBar.preferredSize.height,
      child: tabBar,
    );
  }
}

Add to header:

          headerSliverBuilder: (context, innerScrolled) => <Widget>[
            // use OverlapAbsorberPlus to wrap your SliverAppBar
            const OverlapAbsorberPlus(
              sliver: MySliverAppBar(),
            ),
            SliverPadding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              sliver: SliverToBoxAdapter(
                child: Text(
                  'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
                  'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
                  'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.',
                  maxLines: 5,
                  textAlign: TextAlign.center,
                  overflow: TextOverflow.visible,
                ),
              ),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: MySliverTabBarDelegate(
                tabBar: TabBar(
                  labelColor: Colors.black,
                  unselectedLabelColor: Colors.black,
                  tabs: <Widget>[Text('Tab1.1'), Text('Tab2.2')],
                ),
              ),
            ),
          ],

Video:

RPReplay_Final1699518884.MOV
RPReplay_Final1699518884.2.MOV

Upon reviewing your layout, I noticed duplicate TabBar widgets in the pinned header. Please clarify:

  1. The purpose of two TabBar widgets in the header.
  2. The locations of the corresponding TabView widgets in your code. Sharing the entire layout code would be ideal.
  3. Your intended final layout design.

Also, confirm that your headerSliverBuilder contains a single SliverAppBar. For multiple SliverAppBars, you can use multiple NestedScrollViews to link those headers and bodies.

@idootop

  • The above is just a sample demo, I only need to display the bottom TabBar.
  • In my headerSliverBuilder there is SliverAppBar, SliverPadding, SliverPersistentHeader(TabBar)
    Full code
// ignore_for_file: avoid_print

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

void main() => runApp(
  SafeArea(
    top: true,
    child: MaterialApp(
      theme: ThemeData.light(useMaterial3: true).copyWith(
        primaryColor: Colors.black,
        tabBarTheme: const TabBarTheme(
          labelColor: Colors.white,
          unselectedLabelColor: Colors.white70,
          indicatorColor: Colors.white,
        ),
        appBarTheme: const AppBarTheme(backgroundColor: Colors.black),
      ),
      home: const Example(),
    ),
  ),
);

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

  @override
  State<Example> createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  Widget _tabView([bool reverse = false]) => CustomScrollView(
    key: PageStorageKey<String>('$reverse'),
    physics: const BouncingScrollPhysics(
      parent: AlwaysScrollableScrollPhysics(),
    ),
    slivers: <Widget>[
      const OverlapInjectorPlus(),
      SliverFixedExtentList(
        delegate: SliverChildBuilderDelegate(
              (_, index) => Container(
            key: Key('$reverse-$index'),
            color: index.isEven ? Colors.white : Colors.grey[100],
            child: Center(
              child: Text('ListTile ${reverse ? 30 - index : index + 1}'),
            ),
          ),
          childCount: 30,
        ),
        itemExtent: 60,
      ),
    ],
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: DefaultTabController(
        length: 2,
        child: NestedScrollViewPlus(
          // use key to access NestedScrollViewStatePlus
          key: myKey,
          headerSliverBuilder: (context, innerScrolled) => <Widget>[
            // use OverlapAbsorberPlus to wrap your SliverAppBar
            const OverlapAbsorberPlus(
              sliver: MySliverAppBar(),
            ),
            SliverPadding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              sliver: SliverToBoxAdapter(
                child: Text(
                  'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
                      'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.'
                      'After hiking towards the roaring ocean with no idea what was ahead of us, this view opened up before our eyes.',
                  maxLines: 5,
                  textAlign: TextAlign.center,
                  overflow: TextOverflow.visible,
                ),
              ),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: MySliverTabBarDelegate(
                tabBar: TabBar(
                  labelColor: Colors.black,
                  unselectedLabelColor: Colors.black,
                  tabs: <Widget>[Text('Tab1.1'), Text('Tab2.2')],
                ),
              ),
            ),
          ],
          body: TabBarView(
            children: [
              _tabView(),
              _tabView(true),
            ],
          ),
        ),
      ),
    );
  }

  final GlobalKey<NestedScrollViewStatePlus> myKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      // use GlobalKey<NestedScrollViewStatePlus> to access inner or outer scroll controller
      myKey.currentState?.innerController.addListener(() {
        final innerController = myKey.currentState!.innerController;
        if (innerController.positions.length == 1) {
          print('Scrolling inner nested scrollview: ${innerController.offset}');
        }
      });
      myKey.currentState?.outerController.addListener(() {
        final outerController = myKey.currentState!.outerController;
        if (outerController.positions.length == 1) {
          print('Scrolling outer nested scrollview: ${outerController.offset}');
        }
      });
    });
  }
}

class MySliverAppBar extends StatelessWidget {
  ///Header collapsed height
  final minHeight = 60.0;

  ///Header expanded height
  final maxHeight = 320.0;

  const MySliverAppBar({super.key});

  @override
  Widget build(BuildContext context) {
    final topPadding = MediaQuery.of(context).padding.top;
    return SliverAppBar(
      pinned: true,
      stretch: true,
      toolbarHeight: minHeight - topPadding,
      collapsedHeight: minHeight - topPadding,
      expandedHeight: maxHeight - topPadding,
      titleSpacing: 0,
      flexibleSpace: FlexibleSpaceBar(
        stretchModes: const <StretchMode>[
          StretchMode.zoomBackground,
          StretchMode.blurBackground,
        ],
        background: Image.network(
          'https://pic1.zhimg.com/80/v2-fc35089cfe6c50f97324c98f963930c9_720w.jpg',
          fit: BoxFit.cover,
          alignment: const Alignment(0.0, 0.4),
        ),
      ),
    );
  }
}

class MySliverTabBarDelegate extends SliverPersistentHeaderDelegate {
  MySliverTabBarDelegate({required this.tabBar});

  final TabBar tabBar;

  @override
  double get minExtent => tabBar.preferredSize.height;

  @override
  double get maxExtent => tabBar.preferredSize.height;

  @override
  bool shouldRebuild(MySliverTabBarDelegate oldDelegate) => false;

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Colors.black38,
      height: tabBar.preferredSize.height,
      child: tabBar,
    );
  }
}

@vanvixi Unfortunately, the NestedScrollViewPlus does not support the use of pinned or floating slivers in the headerSliverBuilder, which is a known issue (flutter/flutter#79067) similar to the original NestedScrollView.

Drawing from my experience, you can enhance the structure by moving the dynamic height SliverPadding and TabBar within the SliverAppBar. By pre-computing the dynamic height for SliverPadding and subsequently setting the appropriate height for the SliverAppBar, you can achieve an equivalent outcome.

20231109-234422.mp4

@idootop
Thank you for the experiences you have shared. I will consider that and apply it to my project.
I will close this issue.