I have a code that has these main widgets:
CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics(),
),
slivers: [
SliverFillRemaining(
hasScrollBody: true,
child: ListView(
children: [],
),
),
],
),
I have it this way because around the ListView
widget I have a Column
so that on top of I have a widget that simulates a title.
I chose to work with all of them this way so that when my list has 2-3 items, the entire list and title show on the centre of the screen, and the outer scroll is bouncing with the title.
When the list is longer, what I wanted to accomplish was almost what I got, but I want to know if I'm able to control the scrolling with these rules:
CustomScrollView
Here is a link for you to see what I mean. Test on Chrome mobile view so you can actually see the physics in place.
What I'm asking is that when my list has more items than can fit the screen, I mainly scroll my list, but when it gets to the bottom or the top, it lets my CustomScrollView
handle the physics.
More explanations on the code are here.
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;
},
),
),
],
),
),
);
},
);
}