A NestedScrollView that supports outer scroller to top overscroll.
Web demo 👉 Click Here
Problem: NestedScrollView does not support outer scroller to top overscroll, so its SliverAppBar cannot be stretched.
Related issue: flutter/flutter#54059
Fixed by:
-
Override the applyUserOffset method of _NestedScrollCoordinator to allow over-scroll the top of _outerPosition.
-
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
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_master(Highly recommended)
Thanks to fluttercandies's extended_nested_scroll_view.