Mike
Mike

Reputation: 354

How to set DraggableScrollableSheet maximum size dynamically, according to its content?

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"),
            ),
          ],
        ),
      ),
    );
  }
}

Upvotes: 3

Views: 7855

Answers (3)

LadyBug
LadyBug

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.

The solution

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;
                ),
              ),
            ),
    );

  }
}

Measured size

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;
  }
}

Background:

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

FadyFouad
FadyFouad

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

enter image description here

Upvotes: 0

Mike
Mike

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

Related Questions