0

I am trying to create a draggable bottom sheet that can be expanded or collapsed by the user. Additionally, the user should be able to temporarily collapse the sheet by holding and swiping it.

The code I have so far works well, except for one issue: I am unable to make the height of the bottom sheet the height of its contents. In the following screenshot the bottom of the widget is exactly below "Line 6":

enter image description here

This is the code I come up with so far:

library draggable_bottom_sheet;

import 'package:flutter/material.dart';

class DraggableTest extends StatefulWidget {
  const DraggableTest({super.key});

  @override
  State<DraggableTest> createState() => _DraggableTestState();
}

class _DraggableTestState extends State<DraggableTest> {
  bool _collapsed = false;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Bottom Sheet')),
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            OutlinedButton(
                onPressed: () {
                  setState(() {
                    _collapsed = !_collapsed;
                  });
                },
                child: Text(_collapsed ? 'Expand' : 'Collapse')),
            DraggableBottomSheet(
              expandedWidget: _expandedWidget(),
              backgroundColor: Colors.yellow,
              collapsed: _collapsed,
              maxExtent: 250,
            ),
          ],
        ),
      ),
    );
  }

  Widget _expandedWidget() {
    return ListView(
      physics: const NeverScrollableScrollPhysics(),
      children: const [
        Text('Title', style: TextStyle(fontSize: 22)),
        Text('Line 1', style: TextStyle(fontSize: 18)),
        Text('Line 2', style: TextStyle(fontSize: 18)),
        Text('Line 3', style: TextStyle(fontSize: 18)),
        Text('Line 4', style: TextStyle(fontSize: 18)),
        Text('Line 5', style: TextStyle(fontSize: 18)),
        Text('Line 6', style: TextStyle(fontSize: 18)),
      ],
    );
  }
}

class DraggableBottomSheet extends StatefulWidget {
  final Widget expandedWidget;
  final Color backgroundColor;
  final bool collapsed;
  final double maxExtent;

  const DraggableBottomSheet({
    required this.expandedWidget,
    required this.backgroundColor,
    required this.collapsed,
    required this.maxExtent,
    super.key,
  });

  @override
  State<DraggableBottomSheet> createState() => _DraggableBottomSheetState();
}

class _DraggableBottomSheetState extends State<DraggableBottomSheet> {
  double _currentExtent = 0.0;
  final _minExtent = 50.0;
  bool _isDragging = false;

  @override
  void initState() {
    _currentExtent =
        widget.collapsed ? _minExtent : widget.maxExtent; // <<< This is the line that is causing the problem
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: widget.backgroundColor,
      child: Align(alignment: Alignment.bottomCenter, child: _sheet()),
    );
  }

  Widget _sheet() {
    return GestureDetector(
      onVerticalDragUpdate: _onVerticalDragUpdate,
      onVerticalDragStart: _onVerticalDragStart,
      onVerticalDragEnd: _onVerticalDragEnd,
      child: AnimatedContainer(
        curve: Curves.bounceOut,
        duration: _isDragging ? Duration.zero : const Duration(milliseconds: 500),
        height: widget.collapsed ? _minExtent : _currentExtent,
        child: widget.collapsed ? null : widget.expandedWidget,
      ),
    );
  }

  void _onVerticalDragEnd(DragEndDetails details) {
    setState(() {
      _isDragging = false;
      _currentExtent = widget.maxExtent;
    });
  }

  void _onVerticalDragStart(DragStartDetails details) {
    setState(() {
      _isDragging = true;
    });
  }

  void _onVerticalDragUpdate(DragUpdateDetails details) {
    if (widget.collapsed) return;

    final newExtent = (_currentExtent - details.delta.dy).roundToDouble();
    if (newExtent >= _minExtent && newExtent <= widget.maxExtent) {
      setState(() => _currentExtent = newExtent);
    }
  }
}
Denis
  • 17
  • 4

1 Answers1

1

Inside AnimatedContainer set the height to null when expanded. Use constraints if the bottom sheet height should not exceed maxExtent.

AnimatedContainer(
        curve: Curves.bounceOut,
        duration:
            _isDragging ? Duration.zero : const Duration(milliseconds: 500),
        height: widget.collapsed ? _minExtent : null,
        constraints: BoxConstraints(maxHeight: widget.maxExtent),
        child: widget.collapsed ? null : widget.expandedWidget,
      ),

and don't forget to set shrinkWrap: true for ListView

    return ListView(
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      children: const [
        Text('Title', style: TextStyle(fontSize: 22)),
        Text('Line 1', style: TextStyle(fontSize: 18)),
        ...
      ],
    );
Soliev
  • 1,180
  • 1
  • 1
  • 12
  • Thanks, but now dragging doesn't work anymore. I think this is because _currentExtend is hardcoded to maxExtent in initState. – Denis Feb 08 '23 at 10:45
  • I'm still looking for a solution. I attempted using RenderBox and others, however, nothing worked. – Denis Feb 08 '23 at 23:03