/custom_nested_scroll_view

A NestedScrollView that supports outer scroller to top overscroll.

Primary LanguageDartMIT LicenseMIT

custom_nested_scroll_view

A NestedScrollView that supports outer scroller to top overscroll.

🌍 Preview

Web demo 👉 Click Here

🐛 Problem

NestedScrollView with pinned and stretch SliverAppBar

Problem: NestedScrollView does not support outer scroller to top overscroll, so its SliverAppBar cannot be stretched.

Related issue: flutter/flutter#54059

⚡️ Solution

Fixed by:

  1. Override the applyUserOffset method of _NestedScrollCoordinator to allow over-scroll the top of _outerPosition.

  2. Override the unnestOffset, nestOffset, _getMetrics methods of _NestedScrollCoordinator to fix the mapping between _innerPosition and _outerPosition to _NestedScrollPosition (Coordinator).

For more information, see:

  • example/lib/main.dart
  • lib/src/custom_nested_scroll_view.dart

💡 Usage

dependencies:
  ...
  custom_nested_scroll_view:
    git:
      url: https://github.com/idootop/custom_nested_scroll_view.git
      # Which branch to use is based on your local flutter version
      ref: main # flutter-2.x flutter-3.0 flutter-3.4-pre
Git branch Supported flutter versions
main >=3.4.0-27.0.pre
flutter-3.4-pre >=3.4.0-17.0.pre <3.4.0-27.0.pre
flutter-3.0 >=2.12.0-4.0.pre <3.4.0-17.0.pre
flutter-2.x <2.12.0-4.0.pre
import 'package:flutter/material.dart';
import 'package:custom_nested_scroll_view/custom_nested_scroll_view.dart';

void main() => runApp(
      MaterialApp(
        title: 'Example',
        home: Example(),
      ),
    );

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

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

class _ExampleState extends State<Example> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: DefaultTabController(
        length: 2,
        child: CustomNestedScrollView(
          // use key to access CustomNestedScrollViewState
          key: myKey,
          headerSliverBuilder: (context, innerScrolled) => <Widget>[
            // use CustomOverlapAbsorber to wrap your SliverAppBar
            CustomOverlapAbsorber(
              sliver: MySliverAppBar(),
            ),
          ],
          body: TabBarView(
            children: [
              CustomScrollView(
                slivers: <Widget>[
                  // use CustomOverlapInjector on top of your inner CustomScrollView
                  CustomOverlapInjector(),
                  _tabBody1,
                ],
              ),
              CustomScrollView(
                slivers: <Widget>[
                  // use CustomOverlapInjector on top of your inner CustomScrollView
                  CustomOverlapInjector(),
                  _tabBody2,
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  final GlobalKey<CustomNestedScrollViewState> myKey = GlobalKey();

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

  final _tabBody1 = SliverFixedExtentList(
    delegate: SliverChildBuilderDelegate(
      (_, index) => ListTile(
        key: Key('$index'),
        title: Center(
          child: Text('ListTile ${index + 1}'),
        ),
      ),
      childCount: 30,
    ),
    itemExtent: 50,
  );

  final _tabBody2 = const SliverFillRemaining(
    child: Center(
      child: Text('Test'),
    ),
  );
}

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

  ///Header expanded height
  final maxHeight = 400.0;

  final tabBar = const TabBar(
    tabs: <Widget>[Text('Tab1'), Text('Tab2')],
  );

  @override
  Widget build(BuildContext context) {
    final topHeight = MediaQuery.of(context).padding.top;
    return SliverAppBar(
      pinned: true,
      stretch: true,
      toolbarHeight: minHeight - tabBar.preferredSize.height - topHeight,
      collapsedHeight: minHeight - tabBar.preferredSize.height - topHeight,
      expandedHeight: maxHeight - topHeight,
      flexibleSpace: FlexibleSpaceBar(
        centerTitle: true,
        title: const Center(child: Text('Example')),
        stretchModes: <StretchMode>[
          StretchMode.zoomBackground,
          StretchMode.blurBackground,
        ],
        background: Image.network(
          'https://pic1.zhimg.com/80/v2-fc35089cfe6c50f97324c98f963930c9_720w.jpg',
          fit: BoxFit.cover,
        ),
      ),
      bottom: tabBar,
    );
  }
}

For more examples, see https://github.com/idootop/scroll_masterHighly recommended

❤️ Acknowledgements

Thanks to fluttercandies's extended_nested_scroll_view.

📖 References