edit: refactored and maybe better version:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ExtentableTwoRowScrollable Demo',
home: Scaffold(
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return ExtentableTwoRowScrollable(
height: constraints.maxHeight,
);
}),
),
);
}
}
// sorry for the name :)
class ExtentableTwoRowScrollable extends StatefulWidget {
const ExtentableTwoRowScrollable({
super.key,
required this.height,
this.minHeight = 150.0,
});
final double height;
final double minHeight;
@override
State<ExtentableTwoRowScrollable> createState() =>
_ExtentableTwoRowScrollableState();
}
class _ExtentableTwoRowScrollableState extends State<ExtentableTwoRowScrollable>
with SingleTickerProviderStateMixin {
final upperSizeNotifier = ValueNotifier(0.0);
final lowerSizeNotifier = ValueNotifier(0.0);
var upperHeight = 0.0;
var dragOnUpper = true;
void incrementNotifier(ValueNotifier notifier, double increment) {
if (notifier.value + increment >= widget.height - widget.minHeight) return;
if (notifier.value + increment < widget.minHeight) return;
notifier.value += increment;
}
bool handleVerticalDrag(ScrollNotification notification) {
if (notification is ScrollStartNotification &&
notification.dragDetails != null) {
if (notification.dragDetails!.globalPosition.dy <
upperSizeNotifier.value) {
dragOnUpper = true;
} else {
dragOnUpper = false;
}
}
if (notification is ScrollUpdateNotification) {
final delta = notification.scrollDelta ?? 0.0;
if (dragOnUpper) {
if (notification.metrics.extentAfter != 0) {
incrementNotifier(upperSizeNotifier, delta.abs());
incrementNotifier(lowerSizeNotifier, -1 * delta.abs());
} else {
incrementNotifier(upperSizeNotifier, -1 * delta.abs());
incrementNotifier(lowerSizeNotifier, delta.abs());
}
}
if (!dragOnUpper) {
if (notification.metrics.extentBefore != 0) {
incrementNotifier(upperSizeNotifier, -1 * delta.abs());
incrementNotifier(lowerSizeNotifier, delta.abs());
} else {
incrementNotifier(upperSizeNotifier, delta.abs());
incrementNotifier(lowerSizeNotifier, -1 * delta.abs());
}
}
}
return true;
}
@override
Widget build(BuildContext context) {
// initialize ratio of lower and upper, f.e. here 50:50
upperSizeNotifier.value = widget.height / 2;
lowerSizeNotifier.value = widget.height / 2;
return NotificationListener(
onNotification: handleVerticalDrag,
child: Column(
children: [
ValueListenableBuilder<double>(
valueListenable: upperSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.greenAccent,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("upper ListView $index"));
},
),
);
},
),
ValueListenableBuilder<double>(
valueListenable: lowerSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.blueGrey,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("lower ListView $index"));
},
),
);
},
),
],
),
);
}
}
here is the older post:
so, here's my shot on this. There might be a less complicated solution of course but I think it's somewhat understandable. At least I've tried to comment good enough.
Let me know if it works for you.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ExtentableTwoRowScrollable Demo',
home: Scaffold(
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return ExtentableTwoRowScrollable(
height: constraints.maxHeight,
);
}),
),
);
}
}
// sorry for the name :)
class ExtentableTwoRowScrollable extends StatefulWidget {
const ExtentableTwoRowScrollable({
super.key,
required this.height,
this.minHeightUpper = 300.0,
this.minHeightLower = 300.0,
});
final double height;
final double minHeightUpper;
final double minHeightLower;
@override
State<ExtentableTwoRowScrollable> createState() =>
_ExtentableTwoRowScrollableState();
}
class _ExtentableTwoRowScrollableState extends State<ExtentableTwoRowScrollable>
with SingleTickerProviderStateMixin {
final upperSizeNotifier = ValueNotifier(0.0);
final lowerSizeNotifier = ValueNotifier(0.0);
var upperHeight = 0.0;
var dragOnUpper = true;
bool handleVerticalDrag(ScrollNotification notification) {
if (notification is ScrollStartNotification &&
notification.dragDetails != null)
// only act on ScrollStartNotification events with dragDetails
{
if (notification.dragDetails!.globalPosition.dy <
upperSizeNotifier.value) {
dragOnUpper = true;
} else {
dragOnUpper = false;
}
}
if (notification is ScrollUpdateNotification &&
notification.dragDetails != null)
// only act on ScrollUpdateNotification events with dragDetails
{
if (dragOnUpper) {
// dragging is going on, was started on upper ListView
if (notification.dragDetails!.delta.direction > 0)
// dragging backward/downwards
{
if (lowerSizeNotifier.value >= widget.minHeightLower)
// expand upper until minHeightLower gets hit
{
lowerSizeNotifier.value -= notification.dragDetails!.delta.distance;
upperSizeNotifier.value += notification.dragDetails!.delta.distance;
}
} else
// dragging forward/upwards
{
if (notification.metrics.extentAfter == 0.0 &&
upperSizeNotifier.value > widget.minHeightUpper)
// when at the end of upper shrink it until minHeightUpper gets hit
{
lowerSizeNotifier.value += notification.dragDetails!.delta.distance;
upperSizeNotifier.value -= notification.dragDetails!.delta.distance;
}
}
}
if (!dragOnUpper) {
// dragging is going on, was started on lower ListView
if (notification.dragDetails!.delta.direction > 0)
// dragging backward/downwards
{
if (notification.metrics.extentBefore == 0.0 &&
lowerSizeNotifier.value > widget.minHeightLower)
// when at the top of lower shrink it until minHeightLower gets hit
{
lowerSizeNotifier.value -= notification.dragDetails!.delta.distance;
upperSizeNotifier.value += notification.dragDetails!.delta.distance;
}
} else
// dragging forward/upwards
{
if (upperSizeNotifier.value >= widget.minHeightUpper)
// expand lower until minHeightUpper gets hit
{
lowerSizeNotifier.value += notification.dragDetails!.delta.distance;
upperSizeNotifier.value -= notification.dragDetails!.delta.distance;
}
}
}
}
return true;
}
@override
Widget build(BuildContext context) {
// initialize ratio of lower and upper, f.e. here 50:50
upperSizeNotifier.value = widget.height / 2;
lowerSizeNotifier.value = widget.height / 2;
return NotificationListener(
onNotification: handleVerticalDrag,
child: Column(
children: [
ValueListenableBuilder<double>(
valueListenable: upperSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.greenAccent,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("upper ListView $index"));
},
),
);
},
),
ValueListenableBuilder<double>(
valueListenable: lowerSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.blueGrey,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("lower ListView $index"));
},
),
);
},
),
],
),
);
}
}
I think it's working okayish so far but supporting the "fling" effect - I mean the acceleration when users shoot the scrollable until simulated physics slows it down again - would be really nice, too.