2

Problem

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.

Any Solution?

Does anyone know some hack to set DragabbleScrollableSheet maxChildSize based on its content size or give any alternatives to solve this issue?

Test project

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"),
            ),
          ],
        ),
      ),
    );
  }
}
  • I tried with GlobalKey to get the size of Listview, which I don't think is possible, because it's just buggy and slow.
  • I also tried LayoutBuilder, but with no serious results.
Havlik Miki
  • 41
  • 1
  • 5
  • You said you tried `GlobalKey` directly on a `ListView`? That won't work because `ListView` is not a `RenderBox`. If you wrap a `Container` on the list and get the size of that, it might work. – WSBT Mar 25 '22 at 17:09
  • Hey @user1032613, if I wrap Container around ListView it is still buggy and slow. I calculate size in initState, and the `maxChildSize` is calculated like this `(_size?.height ?? height * 0.8) / height`, and height is MediaQuery.of(context)... height. The Container doesn't know how big the ListView will be, so it cannot determine the max height The problem still remains. – Havlik Miki Mar 25 '22 at 18:09
  • Yep, you are on the right track: `MediaQuery` to get screen height, `GlobalKey` with a shrink-wrapped list (or just use a `Column`) to get children's height. If `DraggableScrollableSheet` is too far from what you want to achieve, consider not using this widget and just write your own. – WSBT Mar 25 '22 at 18:12

2 Answers2

1

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"),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
Havlik Miki
  • 41
  • 1
  • 5
  • The way you're doing is exactly what I (we) need. but the efficiency with using the stack and the opacity, it's really painful – Septian Dika Nov 21 '22 at 04:59
1

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

enter image description here

FadyFouad
  • 815
  • 6
  • 12