I haven't delved that deep into scrolling yet, so i can only tell you what my approach would be. And i hope i understand your wanted behaviour correctly.
Similar to your example snippet use a CustomScrollView with SliverFixedExtentList for the parts of your outer list.
If you now want an inner list in the middle that is scrollable as well, use SliverToBoxAdapter with SizedBox.
And if you want an inner scrollable list at the end that is expanded to the remaining screen space, then use SliverFillRemaining.
To now scroll the outer list if the inner lists reach the scroll end, use a ScrollController with OverscrollNotification.
And then you have inner scrollable lists that scroll the outer list when they reach the end of their own scroll:
Widget _build(BuildContext context) {
final ScrollController outerController = ScrollController(); // todo: this scroll controller should be created
// inside of your state object instead!
return CustomScrollView(
controller: outerController,
slivers: <Widget>[
SliverFixedExtentList(
itemExtent: 100,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => Container(color: Colors.red[(index % 4) * 200 + 200], height: 100),
childCount: 10,
),
),
SliverToBoxAdapter(
child: SizedBox(
height: 300,
child: NotificationListener<OverscrollNotification>(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) =>
Container(color: Colors.green[(index % 4) * 200 + 200], height: 100),
itemCount: 10,
),
onNotification: (OverscrollNotification notification) {
final double newOffset = outerController.offset + notification.overscroll;
outerController.jumpTo(newOffset);
return true;
},
),
),
),
SliverFillRemaining(
hasScrollBody: true,
child: NotificationListener<OverscrollNotification>(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) =>
Container(color: Colors.blue[(index % 4) * 200 + 200], height: 100),
itemCount: 10,
),
onNotification: (OverscrollNotification notification) {
final double newOffset = outerController.offset + notification.overscroll;
if (newOffset < outerController.position.maxScrollExtent &&
newOffset > outerController.position.minScrollExtent) {
// todo: this if condition prevents bouncy scrolling which is a bit weird without better physics
// calculations
outerController.jumpTo(newOffset);
}
return true;
},
),
),
],
);
}
Edit: is this getting close to what you want? (Still has a bit buggy bouncy scrolling and not the best physics).
class CustomScroll extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {PointerDeviceKind.touch, PointerDeviceKind.mouse};
@override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
return child;
}
@override
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
return child;
}
}
void main() {
runApp(MaterialApp(
scrollBehavior: CustomScroll(),
home: Scaffold(body: _build()),
));
}
extension RandomColorExt on Random {
Color nextColor() {
return Color.fromARGB(255, nextInt(256), nextInt(256), nextInt(256));
}
}
final Random random = Random(DateTime.now().microsecondsSinceEpoch);
final ScrollController outerController = ScrollController();
Widget _build() {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics(),
),
controller: outerController,
child: SizedBox(
height: constraints.maxHeight,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text('Title'),
),
),
Flexible(
child: NotificationListener<OverscrollNotification>(
child: ListView.builder(
itemCount: 20,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.all(8),
child: ColoredBox(
color: random.nextColor(),
child: const SizedBox(height: 50),
),
);
},
),
onNotification: (OverscrollNotification notification) {
final double newOffset = outerController.offset + notification.overscroll;
outerController.jumpTo(newOffset);
return true;
},
),
),
],
),
),
);
},
);
}