Reputation: 354
So basically it's quite an old problem, which I couldn't fix with Google. The problem is that DraggableScrollableSheet
doesn't size its maximum size based on its content size, but with only a static value maxChildSize
, which is just not good if you don't want to look at a lot of empty spaces in your sheet.
Does anyone know some hack to set DragabbleScrollableSheet maxChildSize
based on its content size or give any alternatives to solve this issue?
I created a small application just for demonstrating the issue.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () => showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (_) => DraggableScrollableSheet(
initialChildSize: 0.8,
minChildSize: 0.3,
maxChildSize: 0.8,
expand: false,
builder: (_, controller) =>
ListView(
shrinkWrap: true,
controller: controller,
children: <Widget>[
Container(
color: Colors.red,
height: 125.0
),
Container(
color: Colors.white,
height: 125.0
),
Container(
color: Colors.green,
height: 125.0
),
],
)
)
),
child: const Text("Show scrollable sheet"),
),
],
),
),
);
}
}
Upvotes: 3
Views: 7855
Reputation: 61
I might have a solution. I used a Column
in SingleChildListView
and MeasureSize
widget from here to get the height of the column. After the column is rendered the size of the column is obtained and the maximum height of the DragabbleScrollableSheet is adjusted accordingly. The initial sheet height is set to 0 and using the draggableScrollController it is possible to animate to the _maxSize After the Column is rendered.
class BottomSheet extends StatefulWidget {
const BottomSheet({required this.children, Key? key}) : super(key: key);
List<Widget> children;
@override
State<BottomSheet> createState() => _BottomSheetState();
}
class _BottomSheetState extends State<BottomSheet> {
DraggableScrollableController draggableScrollController = DraggableScrollableController();
double _maxSize = 1;
void _setMaxChildSize(Size size) {
setState(() {
// get height of the container.
double boxHeight = size.height;
// get height of the screen from mediaQuery.
double screenHeight = MediaQuery.of(context).size.height;
// get the ratio to set as max size.
double ratio = boxHeight/screenHeight;
_maxSize = ratio;
_animateToMaxHeight(ratio);
});
}
void _animateToMaxHeight(double ratio) {
draggableScrollController.animateTo(ratio, duration: Duration(seconds: 1), curve: Curves.linear);
}
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
controller: draggableScrollController,
initialChildSize: 0,
minChildSize: 0.3,
maxChildSize: _maxSize,
expand: false,
builder: (_, scrollController) =>
SingleChildScrollView(
controller: scrollController,
child: MeasureSize(
onChange: _setMaxChildSize,
child: Column(
children: widget.children;
),
),
),
);
}
}
For completeness I also provide the Measure Size widget here.
class MeasureSize extends SingleChildRenderObjectWidget {
final OnWidgetSizeChange onChange;
const MeasureSize({
Key? key,
required this.onChange,
required Widget child,
}) : super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return MeasureSizeRenderObject(onChange);
}
@override
void updateRenderObject(
BuildContext context, covariant MeasureSizeRenderObject renderObject) {
renderObject.onChange = onChange;
}
}
Getting the height of a (child) widget in flutter is not easy and can only be done after the widget is rendered.
Getting the size of the ListView
directly with a global key somehow seems to return 0. (I have tried with global key and getting the render box.) Therefore I use a single SingleChildScrollView
with a Column
as child. This makes it possible to obtain the size of the Column
, but only after Column
's layout rendered.
Upvotes: 1
Reputation: 948
Define controller to DraggableScrollableSheet
var dragController = DraggableScrollableController();
then you can use it like this:
DraggableScrollableSheet(
controller: controller.dragController,
initialChildSize: 0.25,
minChildSize: 0.25,
maxChildSize: 1,
/** Your code Here **/
],
),
now you can get the size of DraggableScrollableSheet
dragController.size
from 0 to 1
Upvotes: 0
Reputation: 354
I realized the problem is that you cannot measure the ListView size correctly in DraggableScrollableSheet, so I came up with an idea, that maybe I should measure the ListView somewhere else.
This is what I've come up with, it's quite inefficient, but it's better than nothing for now. I would also like to know a better solution though.
class _MyHomePageState extends State<MyHomePage> {
final _key = GlobalKey();
Size? _size;
@override
void initState() {
calculateSize();
super.initState();
}
void calculateSize() =>
WidgetsBinding.instance?.addPersistentFrameCallback((_) {
_size = _key.currentContext?.size;
});
double getTheRightSize(double screenHeight) {
final maxHeight = 0.8 * screenHeight;
final calculatedHeight = _size?.height ?? maxHeight;
return calculatedHeight > maxHeight ? maxHeight / screenHeight : calculatedHeight / screenHeight;
}
@override
Widget build(BuildContext context) {
final height = MediaQuery.of(context).size.height;
return Scaffold(
body: Stack(
children: [
Opacity(
key: _key,
opacity: 0.0,
child: MeasuredWidget(),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () => showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (_) => DraggableScrollableSheet(
initialChildSize: getTheRightSize(height),
minChildSize: 0.3,
maxChildSize: getTheRightSize(height),
expand: false,
builder: (_, controller) =>
MeasuredWidget(controller: controller,)
)
),
child: const Text("Show scrollable sheet"),
),
],
),
),
],
),
);
}
}
class MeasuredWidget extends StatelessWidget {
ScrollController? controller;
MeasuredWidget({ Key? key, this.controller }) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: ListView(
shrinkWrap: true,
controller: controller,
children: <Widget>[
Container(
color: Colors.red,
height: 400.0
),
Container(
color: Colors.white,
height: 400.0
),
Container(
color: Colors.green,
height: 200.0
),
],
),
);
}
}
Update:
You still want to work around the problem if your content size changes. For this reason you have to measure size during build or after build and provide PostFrameCallback in build to show the correct sized DraggableScrollableSheet. So eventually your code would look like this:
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _key = GlobalKey();
Size? _size;
double neWheight = 100.0;
bool weClicked = false;
double getTheRightSize(double screenHeight) {
final maxHeight = 0.8 * screenHeight;
final calculatedHeight = _size?.height ?? maxHeight;
return calculatedHeight > maxHeight ? maxHeight / screenHeight : calculatedHeight / screenHeight;
}
@override
Widget build(BuildContext context) {
print("build");
final height = MediaQuery.of(context).size.height;
weClicked ? SchedulerBinding.instance?.addPostFrameCallback((timeStamp) {
print("schedule");
_size = _key.currentContext?.size;
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (_) => DraggableScrollableSheet(
initialChildSize: getTheRightSize(height),
minChildSize: 0.3,
maxChildSize: getTheRightSize(height),
expand: false,
builder: (_, controller) =>
MeasuredWidget(controller: controller, height: neWheight)
)
);
}) : Container();
return Scaffold(
body: Stack(
children: [
Opacity(
key: _key,
opacity: 0.0,
child: MeasuredWidget(height: neWheight),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
setState(() {
neWheight = 100;
weClicked = true;
});
},
child: const Text("Show scrollable sheet"),
),
ElevatedButton(
onPressed: () {
setState(() {
weClicked = true;
neWheight = 200;
});
},
child: const Text("Show double scrollable sheet"),
),
],
),
),
],
),
);
}
}
Upvotes: 1