idootop / nested_scroll_view_plus

📜 An enhanced NestedScrollView with support for overscrolling for both the inner and outer scrollviews.
https://flutter-nested-scroll-view-plus.vercel.app
MIT License
25 stars 2 forks source link

Multi headers are misbehaving #2

Closed vanvixi closed 11 months ago

vanvixi commented 11 months ago

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

https://github.com/idootop/nested_scroll_view_plus/assets/75975945/b5910a2b-dcee-4873-9016-225e6bf43ac0

idootop commented 11 months ago

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.

vanvixi commented 11 months ago

@idootop Thank you. The problem has been resolved.

vanvixi commented 11 months ago

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

idootop commented 11 months ago

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

vanvixi commented 11 months ago

@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:

https://github.com/idootop/nested_scroll_view_plus/assets/75975945/b1e9b0d6-43d4-4cbd-9d07-1ae9c7d1d51c

https://github.com/idootop/nested_scroll_view_plus/assets/75975945/b6ad96bd-a614-4910-87c5-8174c2a6eec1

idootop commented 11 months ago

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.

vanvixi commented 11 months ago

@idootop

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 createState() => _ExampleState(); }

class _ExampleState extends State { Widget tabView([bool reverse = false]) => CustomScrollView( key: PageStorageKey('$reverse'), physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics(), ), slivers: [ 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) => [ // 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: [Text('Tab1.1'), Text('Tab2.2')], ), ), ), ], body: TabBarView( children: [ _tabView(), _tabView(true), ], ), ), ), ); }

final GlobalKey myKey = GlobalKey();

@override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { // use GlobalKey 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.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, ); } }

idootop commented 11 months ago

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

idootop commented 11 months ago

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.

https://github.com/idootop/nested_scroll_view_plus/assets/35302658/add5aad4-fe09-41e6-a82b-c4f7b51c84f2

vanvixi commented 11 months ago

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