|
| 1 | +import 'package:flutter/material.dart'; |
| 2 | + |
| 3 | +class GFBottomSheet extends StatefulWidget { |
| 4 | + GFBottomSheet({ |
| 5 | + Key key, |
| 6 | + @required this.stickyHeader, |
| 7 | + @required this.contentBody, |
| 8 | + this.stickyFooter, |
| 9 | + this.controller, |
| 10 | + this.minContentHeight = 0, |
| 11 | + this.maxContentHeight = 300, |
| 12 | + this.elevation = 0.0, |
| 13 | + this.stickyFooterHeight, |
| 14 | + }) : assert(elevation >= 0.0), |
| 15 | + assert(minContentHeight >= 0.0), |
| 16 | + super(key: key) { |
| 17 | + controller.height = minContentHeight; |
| 18 | + controller.smoothness = 500; |
| 19 | +// controller == null ? controller = GFBottomSheetController() : Container(); |
| 20 | + } |
| 21 | + |
| 22 | + /// [minContentHeight] controls the minimum height of the content body. |
| 23 | + /// It Must be greater or equal to 0. Default value is 0. |
| 24 | + final double minContentHeight; |
| 25 | + |
| 26 | + /// [maxContentHeight] controls the maximum height of the content body. |
| 27 | + /// It Must be greater or equal to 0. Default value is 300. |
| 28 | + final double maxContentHeight; |
| 29 | + |
| 30 | + /// [stickyHeader] is the header of GFBottomSheet. |
| 31 | + /// User can interact by swiping or tapping the [stickyHeader] |
| 32 | + final Widget stickyHeader; |
| 33 | + |
| 34 | + /// [contentBody] is the body of GFBottomSheet. |
| 35 | + /// User can interact by tapping the [contentBody] |
| 36 | + final Widget contentBody; |
| 37 | + |
| 38 | + /// [stickyFooter] is the footer of GFBottomSheet. |
| 39 | + /// User can interact by swiping or tapping the [stickyFooter] |
| 40 | + final Widget stickyFooter; |
| 41 | + |
| 42 | + /// [stickyFooterHeight] defines the height of GFBottokSheet footer. |
| 43 | + final double stickyFooterHeight; |
| 44 | + |
| 45 | + /// [elevation] controls shadow below the GFBottomSheet material. |
| 46 | + /// Must be greater or equalto 0. Default value is 0. |
| 47 | + final double elevation; |
| 48 | + |
| 49 | + /// [controller] used to control GFBottomSheet behavior like hide/show |
| 50 | + final GFBottomSheetController controller; |
| 51 | + |
| 52 | + @override |
| 53 | + _GFBottomSheetState createState() => _GFBottomSheetState(); |
| 54 | +} |
| 55 | + |
| 56 | +class _GFBottomSheetState extends State<GFBottomSheet> |
| 57 | + with TickerProviderStateMixin { |
| 58 | + bool isDragDirectionUp; |
| 59 | + bool showBottomSheet = false; |
| 60 | + Function _controllerListener; |
| 61 | + |
| 62 | + void _onVerticalDragUpdate(data) { |
| 63 | + _setNativeSmoothness(); |
| 64 | + if (((widget.controller.height - data.delta.dy) > |
| 65 | + widget.minContentHeight) && |
| 66 | + ((widget.controller.height - data.delta.dy) < |
| 67 | + widget.maxContentHeight)) { |
| 68 | + isDragDirectionUp = data.delta.dy <= 0; |
| 69 | + widget.controller.height -= data.delta.dy; |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + void _onVerticalDragEnd(data) { |
| 74 | + _setUsersSmoothness(); |
| 75 | + |
| 76 | + if (isDragDirectionUp && widget.controller.value) { |
| 77 | + _showBottomSheet(); |
| 78 | + } else if (!isDragDirectionUp && !widget.controller.value) { |
| 79 | + _hideBottomSheet(); |
| 80 | + } else { |
| 81 | + widget.controller.value = isDragDirectionUp; |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + void _onTap() { |
| 86 | + final bool isBottomSheetOpened = |
| 87 | + widget.controller.height == widget.maxContentHeight; |
| 88 | + widget.controller.value = !isBottomSheetOpened; |
| 89 | + } |
| 90 | + |
| 91 | + @override |
| 92 | + void initState() { |
| 93 | + super.initState(); |
| 94 | + widget.controller.value = showBottomSheet; |
| 95 | + _controllerListener = () { |
| 96 | + widget.controller.value ? _showBottomSheet() : _hideBottomSheet(); |
| 97 | + }; |
| 98 | + widget.controller.addListener(_controllerListener); |
| 99 | + } |
| 100 | + |
| 101 | + @override |
| 102 | + Widget build(BuildContext context) { |
| 103 | + final Widget bottomSheet = Column( |
| 104 | + mainAxisSize: MainAxisSize.min, |
| 105 | + children: <Widget>[ |
| 106 | + widget.stickyHeader == null |
| 107 | + ? Container() |
| 108 | + : GestureDetector( |
| 109 | + onVerticalDragUpdate: _onVerticalDragUpdate, |
| 110 | + onVerticalDragEnd: _onVerticalDragEnd, |
| 111 | + onTap: _onTap, |
| 112 | + child: widget.stickyHeader, |
| 113 | + ), |
| 114 | + AnimatedBuilder( |
| 115 | + animation: widget.controller, |
| 116 | + builder: (_, Widget child) => AnimatedContainer( |
| 117 | + curve: Curves.easeOut, |
| 118 | + duration: Duration(milliseconds: widget.controller.smoothness), |
| 119 | + height: widget.controller.height, |
| 120 | + child: GestureDetector( |
| 121 | + onVerticalDragUpdate: _onVerticalDragUpdate, |
| 122 | + onVerticalDragEnd: _onVerticalDragEnd, |
| 123 | + onTap: _onTap, |
| 124 | + child: widget.contentBody, |
| 125 | + ), |
| 126 | + ), |
| 127 | + ), |
| 128 | + widget.stickyFooter != null |
| 129 | + ? AnimatedBuilder( |
| 130 | + animation: widget.controller, |
| 131 | + builder: (_, Widget child) => AnimatedContainer( |
| 132 | + curve: Curves.easeOut, |
| 133 | + duration: |
| 134 | + Duration(milliseconds: widget.controller.smoothness), |
| 135 | + height: widget.controller.height != widget.minContentHeight |
| 136 | + ? widget.stickyFooterHeight |
| 137 | + : 0.0, |
| 138 | + child: GestureDetector( |
| 139 | + onVerticalDragUpdate: _onVerticalDragUpdate, |
| 140 | + onVerticalDragEnd: _onVerticalDragEnd, |
| 141 | + onTap: _onTap, |
| 142 | + child: widget.stickyFooter, |
| 143 | + ), |
| 144 | + ), |
| 145 | + ) |
| 146 | + : Container(), |
| 147 | + ], |
| 148 | + ); |
| 149 | + return Material( |
| 150 | + elevation: widget.elevation, |
| 151 | + child: bottomSheet, |
| 152 | + ); |
| 153 | + } |
| 154 | + |
| 155 | + void _hideBottomSheet() { |
| 156 | + widget.controller.height = widget.minContentHeight; |
| 157 | + } |
| 158 | + |
| 159 | + void _showBottomSheet() { |
| 160 | + widget.controller.height = widget.maxContentHeight; |
| 161 | + } |
| 162 | + |
| 163 | + @override |
| 164 | + void dispose() { |
| 165 | + widget.controller.removeListener(_controllerListener); |
| 166 | + super.dispose(); |
| 167 | + } |
| 168 | + |
| 169 | + void _setUsersSmoothness() { |
| 170 | + widget.controller.smoothness = 500; |
| 171 | + } |
| 172 | + |
| 173 | + void _setNativeSmoothness() { |
| 174 | + widget.controller.smoothness = 500; |
| 175 | + } |
| 176 | +} |
| 177 | + |
| 178 | +class GFBottomSheetController extends ValueNotifier<bool> { |
| 179 | + GFBottomSheetController() : super(false); |
| 180 | + |
| 181 | + /// Defines the height of the GFBottomSheet's contentBody |
| 182 | + double _height; |
| 183 | + |
| 184 | + /// Defines the drag animation smoothness of the GFBottomSheet |
| 185 | + int smoothness; |
| 186 | + |
| 187 | + // ignore: unnecessary_getters_setters |
| 188 | + set height(double value) => _height = value; |
| 189 | + |
| 190 | + // ignore: unnecessary_getters_setters |
| 191 | + double get height => _height; |
| 192 | + |
| 193 | + bool get isBottomSheetOpened => value; |
| 194 | + |
| 195 | + void hideBottomSheet() => value = false; |
| 196 | + void showBottomSheet() => value = true; |
| 197 | +} |
0 commit comments