I was initially not confident making a custom draggable bottom sheet because it seemed like it’d be difficult. Turns out, it’s pretty easy and like I always do, decided to share it. We’ll be using an animated container along with GestureDetector’s onVerticalDragUpdate and onVerticalDragEnd.
I’ll go over two ways you can play with this — (1) fully controller by user (follows user’s drag) and (2) semi-automated where if the user swipes up, it goes all the way up and vice versa.
Layout
Here’s a very simple layout to work with.
import 'package:flutter/material.dart';
class Main extends StatefulWidget {
const Main({Key? key}) : super(key: key);
@override
State<Main> createState() => _MainState();
}
class _MainState extends State<Main> {
final double _headerHeight = 40.0;
final double _maxHeight = 600.0;
bool _isDragUp = true;
double _bodyHeight = 0.0;
@override
Widget build(BuildContext context) {
final Size _size = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: <Widget>[
Center(child: Text("Custom Scrollable Bottom Sheet ")),
Positioned(
bottom: 0.0,
child: AnimatedContainer(
constraints: BoxConstraints(
maxHeight: this._maxHeight,
minHeight: this._headerHeight,
),
curve: Curves.easeOut,
height: this._headerHeight,
duration: const Duration(milliseconds: 600),
child: GestureDetector(
onVerticalDragUpdate: (DragUpdateDetails data) {
},
onVerticalDragEnd: (DragEndDetails data){
},
child: Column(
children: <Widget>[
Container(
width: _size.width,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.lightBlue,
borderRadius: BorderRadius.only(
topRight: Radius.circular(20.0),
topLeft: Radius.circular(20.0),
),
boxShadow: <BoxShadow>[
BoxShadow(color: Colors.grey, spreadRadius: 2.0, blurRadius: 4.0),
]
),
height: this._headerHeight,
child: Text("drag me"),
),
Expanded(
child: Container(
width: _size.width,
color: Colors.greenAccent,
alignment: Alignment.center,
child: Text("it worked!"),
),
),
],
),
)),
),
],
),
);
}
}
Following User’s Drag
This one is very simple because all you need to use is onVerticalDragUpdate. But first, notice that the height of the animated container is headerHeight + bodyHeight — body is at 0.0 when the bottom sheet has not been dragged. DragUpdateDetail gives you information on the y-axis’s global position. As we drag the header, we’ll setState the bodyHeight. (I’ve also set up a maximum height so it doesn’t go up forever).
onVerticalDragUpdate: (DragUpdateDetails data) {
double _draggedAmount = _size.height - data.globalPosition.dy;
this.setState(() {
this._bodyHeight = _draggedAmount;
});
},
Side Note: the duration of the animated container decides how fast the drag animation is!
Automatic Drag
For this one, you need a boolean that tells you whether the drag direction is up or down. Decide on how much you want the user to drag up before it becomes automatic, and after that point, just set the bodyHeight to the maxHeight.
onVerticalDragUpdate: (DragUpdateDetails data) {
double _draggedAmount = _size.height - data.globalPosition.dy;
if (this._isDragUp){
if (_draggedAmount < 100.0) this._bodyHeight = _draggedAmount;
if (_draggedAmount > 100.0) this._bodyHeight = this._maxHeight;
} else {
/// the _draggedAmount cannot be higher than maxHeight b/c maxHeight is _dragged Amount + header Height
double _downDragged = this._maxHeight - _draggedAmount;
if (_downDragged < 100.0) this._bodyHeight = _draggedAmount;
if (_downDragged > 100.0) this._bodyHeight = 0.0;
}
this.setState(() {});
},
onVerticalDragEnd: (DragEndDetails data){
if (_isDragUp) {
this._isDragUp = false;
} else {
this._isDragUp = true;
}
this.setState(() {});
},