Skip to content

Instantly share code, notes, and snippets.

@andrelsmoraes
Created June 22, 2018 21:03
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save andrelsmoraes/9e4af0133bff8960c1feeb0ead7fd749 to your computer and use it in GitHub Desktop.
Save andrelsmoraes/9e4af0133bff8960c1feeb0ead7fd749 to your computer and use it in GitHub Desktop.
Modal Bottom Sheet with Input Fields fix for Flutter (Fix issue with overlap with keyboard and fix for tapping to dismiss) - Flutter Version: Channel beta, v0.5.1
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'scaffold.dart';
import 'theme.dart';
const Duration _kBottomSheetDuration = const Duration(milliseconds: 200);
const double _kMinFlingVelocity = 700.0;
const double _kCloseProgressThreshold = 0.5;
/// A material design bottom sheet.
///
/// There are two kinds of bottom sheets in material design:
///
/// * _Persistent_. A persistent bottom sheet shows information that
/// supplements the primary content of the app. A persistent bottom sheet
/// remains visible even when the user interacts with other parts of the app.
/// Persistent bottom sheets can be created and displayed with the
/// [ScaffoldState.showBottomSheet] function.
///
/// * _Modal_. A modal bottom sheet is an alternative to a menu or a dialog and
/// prevents the user from interacting with the rest of the app. Modal bottom
/// sheets can be created and displayed with the [showModalBottomSheet]
/// function.
///
/// The [BottomSheet] widget itself is rarely used directly. Instead, prefer to
/// create a persistent bottom sheet with [ScaffoldState.showBottomSheet] and a modal
/// bottom sheet with [showModalBottomSheet].
///
/// See also:
///
/// * [ScaffoldState.showBottomSheet]
/// * [showModalBottomSheet]
/// * <https://material.google.com/components/bottom-sheets.html>
class BottomSheet extends StatefulWidget {
/// Creates a bottom sheet.
///
/// Typically, bottom sheets are created implicitly by
/// [ScaffoldState.showBottomSheet], for persistent bottom sheets, or by
/// [showModalBottomSheet], for modal bottom sheets.
const BottomSheet({
Key key,
this.animationController,
@required this.onClosing,
@required this.builder
}) : assert(onClosing != null),
assert(builder != null),
super(key: key);
/// The animation that controls the bottom sheet's position.
///
/// The BottomSheet widget will manipulate the position of this animation, it
/// is not just a passive observer.
final AnimationController animationController;
/// Called when the bottom sheet begins to close.
///
/// A bottom sheet might be be prevented from closing (e.g., by user
/// interaction) even after this callback is called. For this reason, this
/// callback might be call multiple times for a given bottom sheet.
final VoidCallback onClosing;
/// A builder for the contents of the sheet.
///
/// The bottom sheet will wrap the widget produced by this builder in a
/// [Material] widget.
final WidgetBuilder builder;
@override
_BottomSheetState createState() => new _BottomSheetState();
/// Creates an animation controller suitable for controlling a [BottomSheet].
static AnimationController createAnimationController(TickerProvider vsync) {
return new AnimationController(
duration: _kBottomSheetDuration,
debugLabel: 'BottomSheet',
vsync: vsync,
);
}
}
class _BottomSheetState extends State<BottomSheet> {
final GlobalKey _childKey = new GlobalKey(debugLabel: 'BottomSheet child');
double get _childHeight {
final RenderBox renderBox = _childKey.currentContext.findRenderObject();
return renderBox.size.height;
}
bool get _dismissUnderway => widget.animationController.status == AnimationStatus.reverse;
void _handleDragUpdate(DragUpdateDetails details) {
if (_dismissUnderway)
return;
widget.animationController.value -= details.primaryDelta / (_childHeight ?? details.primaryDelta);
}
void _handleDragEnd(DragEndDetails details) {
if (_dismissUnderway)
return;
if (details.velocity.pixelsPerSecond.dy > _kMinFlingVelocity) {
final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
if (widget.animationController.value > 0.0)
widget.animationController.fling(velocity: flingVelocity);
if (flingVelocity < 0.0)
widget.onClosing();
} else if (widget.animationController.value < _kCloseProgressThreshold) {
if (widget.animationController.value > 0.0)
widget.animationController.fling(velocity: -1.0);
widget.onClosing();
} else {
widget.animationController.forward();
}
}
@override
Widget build(BuildContext context) {
return new GestureDetector(
onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: _handleDragEnd,
child: new Material(
key: _childKey,
child: widget.builder(context)
)
);
}
}
// PERSISTENT BOTTOM SHEETS
// See scaffold.dart
// MODAL BOTTOM SHEETS
class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
_ModalBottomSheetLayout(this.progress, this.bottomInset);
final double progress;
final double bottomInset;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return new BoxConstraints(
minWidth: constraints.maxWidth,
maxWidth: constraints.maxWidth,
minHeight: 0.0,
maxHeight: constraints.maxHeight * 9.0 / 16.0
);
}
@override
Offset getPositionForChild(Size size, Size childSize) {
return new Offset(0.0, size.height - bottomInset - childSize.height * progress);
}
@override
bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) {
return progress != oldDelegate.progress || bottomInset != oldDelegate.bottomInset;
}
}
class _ModalBottomSheet<T> extends StatefulWidget {
const _ModalBottomSheet({ Key key, this.route }) : super(key: key);
final _ModalBottomSheetRoute<T> route;
@override
_ModalBottomSheetState<T> createState() => new _ModalBottomSheetState<T>();
}
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
@override
Widget build(BuildContext context) {
return new GestureDetector(
onTap: widget.route.dismissOnTap ? () => Navigator.pop(context) : null,
child: new AnimatedBuilder(
animation: widget.route.animation,
builder: (BuildContext context, Widget child) {
double bottomInset = widget.route.resizeToAvoidBottomPadding
? MediaQuery.of(context).viewInsets.bottom : 0.0;
return new ClipRect(
child: new CustomSingleChildLayout(
delegate: new _ModalBottomSheetLayout(widget.route.animation.value, bottomInset),
child: new BottomSheet(
animationController: widget.route._animationController,
onClosing: () => Navigator.pop(context),
builder: widget.route.builder
)
)
);
}
)
);
}
}
class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
_ModalBottomSheetRoute({
this.builder,
this.theme,
this.barrierLabel,
RouteSettings settings,
this.resizeToAvoidBottomPadding,
this.dismissOnTap,
}) : super(settings: settings);
final WidgetBuilder builder;
final ThemeData theme;
final bool resizeToAvoidBottomPadding;
final bool dismissOnTap;
@override
Duration get transitionDuration => _kBottomSheetDuration;
@override
bool get barrierDismissible => true;
@override
final String barrierLabel;
@override
Color get barrierColor => Colors.black54;
AnimationController _animationController;
@override
AnimationController createAnimationController() {
assert(_animationController == null);
_animationController = BottomSheet.createAnimationController(navigator.overlay);
return _animationController;
}
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
// By definition, the bottom sheet is aligned to the bottom of the page
// and isn't exposed to the top padding of the MediaQuery.
Widget bottomSheet = new MediaQuery.removePadding(
context: context,
removeTop: true,
child: new _ModalBottomSheet<T>(route: this),
);
if (theme != null)
bottomSheet = new Theme(data: theme, child: bottomSheet);
return bottomSheet;
}
}
/// Shows a modal material design bottom sheet.
///
/// A modal bottom sheet is an alternative to a menu or a dialog and prevents
/// the user from interacting with the rest of the app.
///
/// A closely related widget is a persistent bottom sheet, which shows
/// information that supplements the primary content of the app without
/// preventing the use from interacting with the app. Persistent bottom sheets
/// can be created and displayed with the [showBottomSheet] function or the
/// [ScaffoldState.showBottomSheet] method.
///
/// The `context` argument is used to look up the [Navigator] and [Theme] for
/// the bottom sheet. It is only used when the method is called. Its
/// corresponding widget can be safely removed from the tree before the bottom
/// sheet is closed.
///
/// Returns a `Future` that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the modal bottom sheet was closed.
///
/// See also:
///
/// * [BottomSheet], which is the widget normally returned by the function
/// passed as the `builder` argument to [showModalBottomSheet].
/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
/// non-modal bottom sheets.
/// * <https://material.google.com/components/bottom-sheets.html#bottom-sheets-modal-bottom-sheets>
Future<T> showModalBottomSheet<T>({
@required BuildContext context,
@required WidgetBuilder builder,
bool dismissOnTap: true,
bool resizeToAvoidBottomPadding : true,
}) {
assert(context != null);
assert(builder != null);
return Navigator.push(context, new _ModalBottomSheetRoute<T>(
builder: builder,
theme: Theme.of(context, shadowThemeOnly: true),
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
resizeToAvoidBottomPadding: resizeToAvoidBottomPadding,
dismissOnTap: dismissOnTap,
));
}
/// Shows a persistent material design bottom sheet in the nearest [Scaffold].
///
/// A persistent bottom sheet shows information that supplements the primary
/// content of the app. A persistent bottom sheet remains visible even when the
/// user interacts with other parts of the app. A [Scaffold] is required in the
/// given `context`; its [ScaffoldState.showBottomSheet] method is used to
/// actually show the bottom sheet.
///
/// A closely related widget is a modal bottom sheet, which is an alternative
/// to a menu or a dialog and prevents the user from interacting with the rest
/// of the app. Modal bottom sheets can be created and displayed with the
/// [showModalBottomSheet] function.
///
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
/// To rebuild the bottom sheet (e.g. if it is stateful), call
/// [PersistentBottomSheetController.setState] on the value returned from this
/// method.
///
/// The `context` argument is used to look up the [Scaffold] for the bottom
/// sheet. It is only used when the method is called. Its corresponding widget
/// can be safely removed from the tree before the bottom sheet is closed.
///
/// See also:
///
/// * [BottomSheet], which is the widget typically returned by the `builder`.
/// * [showModalBottomSheet], which can be used to display a modal bottom
/// sheet.
/// * [Scaffold.of], for information about how to obtain the [BuildContext].
/// * <https://material.google.com/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets>
PersistentBottomSheetController<T> showBottomSheet<T>({
@required BuildContext context,
@required WidgetBuilder builder,
}) {
assert(context != null);
assert(builder != null);
return Scaffold.of(context).showBottomSheet<T>(builder);
}
@andrelsmoraes
Copy link
Author

@ROTGP
Copy link

ROTGP commented Jul 12, 2018

@andrelsmoraes could you provide an example of how to use this?

@lucasjinreal
Copy link

@andreslmoraes Could u provide some more codes for that Youtube demo?

@andrelsmoraes
Copy link
Author

andrelsmoraes commented Jul 15, 2018

@jinfagang @b354c502 Sure, first, you have to paste the content of this gist into the file "packages/flutter/lib/src/material/bottom_sheet.dart".

Note that my Flutter version is "Channel beta, v0.5.1". If you're using another version, maybe you'll need to modify it.

After that, just use the ModalBottomSheet adding a TextField to it. Take a look at this file: https://github.com/andrelsmoraes/three_hundred_english/blob/master/lib/phrase_details.dart

Search for "showModalBottomSheet" and "buildSheetAddNote" for a full example. Please note that I'm still learning the language, so the code might be a little "ugly". Currently I'm refactoring this project to use the MVP pattern, in a few days the code might look totally different (but I'm still using the input text into the modal bottom).

@andrelsmoraes
Copy link
Author

andrelsmoraes commented Jul 15, 2018

new FlatButton(
    child: Text('COLLABORATE'),
    onPressed: () {
        showModalBottomSheet(
            context: context,
            builder: (BuildContext context) {
                return buildSheetAddNote(context);
            }
        );
    }
)
Widget buildSheetAddNote(BuildContext context) {
    return new Container(
        child: new Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              children: <Widget>[
                new Expanded(
                    child: new TextField(
                      autofocus: true,
                      style: new TextStyle(
                        fontSize: 18.0,
                        color: Colors.grey[800],
                      ),
                      decoration: new InputDecoration(
                        hintText: "Add a note, tip or example...",
                        hintStyle: new TextStyle(color: Colors.grey[800]),
                      ),
                    )
                ),
                new IconButton(
                    icon: Icon(
                      Icons.send,
                      color: Colors.red,
                    ),
                    onPressed: () {
                      //TODO send
                      Navigator.pop(context);
                    }
                )
              ],
            )
        )
    );
  }

@AlbCM
Copy link

AlbCM commented Oct 22, 2018

Thank you!

@crimsonsuv
Copy link

Thanks alot, I spend 3 days searching a fix for this issue, I have created a seperate dart file and changed the name of showModalBottomSheet to showModalBottomSheetApp and everything working smoothly, thanks :)

@QSKOBE24
Copy link

great! thank you!!!

@valterh4ck3r
Copy link

valterh4ck3r commented Jan 28, 2019

Great. Thanks a lot <3

@valterh4ck3r
Copy link

I/flutter (14388): ══╡ EXCEPTION CAUGHT BY FOUNDATION LIBRARY ╞════════════════════════════════════════════════════════
I/flutter (14388): The following assertion was thrown while dispatching notifications for TextEditingController:
I/flutter (14388): setState() or markNeedsBuild() called during build.
I/flutter (14388): This EditableText widget cannot be marked as needing to build because the framework is already in
I/flutter (14388): the process of building widgets. A widget can be marked as needing to be built during the build
I/flutter (14388): phase only if one of its ancestors is currently building. This exception is allowed because the
I/flutter (14388): framework builds parent widgets before children, which means a dirty descendant will always be
I/flutter (14388): built. Otherwise, the framework might not visit this widget during this build phase.
I/flutter (14388): The widget on which setState() or markNeedsBuild() was called was:
I/flutter (14388): EditableText-[LabeledGlobalKey#954c7](controller:

@0510210004
Copy link

There is anomaly, bro. I can use it once. But, when i restart my codes once more, that's not happened.

Compiler message:
file:///D:/flutter/packages/flutter/lib/src/material/scaffold.dart:1923:11: Error: No named parameter with the name 'enableDrag'.
enableDrag: widget.enableDrag,
^^^^^^^^^^
file:///D:/flutter/packages/flutter/lib/src/material/bottom_sheet.dart:51:9: Context: Found this candidate, but the arguments don't match.
const BottomSheet({

@hkakutalua
Copy link

Life saviour.
Thanks buddy!!

@daudimeck
Copy link

is there a callback on close?

@rodydavis
Copy link

rodydavis commented Jun 19, 2019

Create a new file in your project called bottom_sheet_custom.dart then import it into another file and call showModalCustomBottomSheet

// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

const Duration _kCustomBottomSheetDuration = const Duration(milliseconds: 200);
const double _kMinFlingVelocity = 700.0;
const double _kCloseProgressThreshold = 0.5;

/// A material design bottom sheet.
///
/// There are two kinds of bottom sheets in material design:
///
///  * _Persistent_. A persistent bottom sheet shows information that
///    supplements the primary content of the app. A persistent bottom sheet
///    remains visible even when the user interacts with other parts of the app.
///    Persistent bottom sheets can be created and displayed with the
///    [ScaffoldState.showCustomBottomSheet] function.
///
///  * _Modal_. A modal bottom sheet is an alternative to a menu or a dialog and
///    prevents the user from interacting with the rest of the app. Modal bottom
///    sheets can be created and displayed with the [showModalCustomBottomSheet]
///    function.
///
/// The [CustomBottomSheet] widget itself is rarely used directly. Instead, prefer to
/// create a persistent bottom sheet with [ScaffoldState.showCustomBottomSheet] and a modal
/// bottom sheet with [showModalCustomBottomSheet].
///
/// See also:
///
///  * [ScaffoldState.showCustomBottomSheet]
///  * [showModalCustomBottomSheet]
///  * <https://material.google.com/components/bottom-sheets.html>
class CustomBottomSheet extends StatefulWidget {
  /// Creates a bottom sheet.
  ///
  /// Typically, bottom sheets are created implicitly by
  /// [ScaffoldState.showCustomBottomSheet], for persistent bottom sheets, or by
  /// [showModalCustomBottomSheet], for modal bottom sheets.
  const CustomBottomSheet(
      {Key key,
      this.animationController,
      @required this.onClosing,
      @required this.builder})
      : assert(onClosing != null),
        assert(builder != null),
        super(key: key);

  /// The animation that controls the bottom sheet's position.
  ///
  /// The CustomBottomSheet widget will manipulate the position of this animation, it
  /// is not just a passive observer.
  final AnimationController animationController;

  /// Called when the bottom sheet begins to close.
  ///
  /// A bottom sheet might be be prevented from closing (e.g., by user
  /// interaction) even after this callback is called. For this reason, this
  /// callback might be call multiple times for a given bottom sheet.
  final VoidCallback onClosing;

  /// A builder for the contents of the sheet.
  ///
  /// The bottom sheet will wrap the widget produced by this builder in a
  /// [Material] widget.
  final WidgetBuilder builder;

  @override
  _CustomBottomSheetState createState() => new _CustomBottomSheetState();

  /// Creates an animation controller suitable for controlling a [CustomBottomSheet].
  static AnimationController createAnimationController(TickerProvider vsync) {
    return new AnimationController(
      duration: _kCustomBottomSheetDuration,
      debugLabel: 'CustomBottomSheet',
      vsync: vsync,
    );
  }
}

class _CustomBottomSheetState extends State<CustomBottomSheet> {
  final GlobalKey _childKey =
      new GlobalKey(debugLabel: 'CustomBottomSheet child');

  double get _childHeight {
    final RenderBox renderBox = _childKey.currentContext.findRenderObject();
    return renderBox.size.height;
  }

  bool get _dismissUnderway =>
      widget.animationController.status == AnimationStatus.reverse;

  void _handleDragUpdate(DragUpdateDetails details) {
    if (_dismissUnderway) return;
    widget.animationController.value -=
        details.primaryDelta / (_childHeight ?? details.primaryDelta);
  }

  void _handleDragEnd(DragEndDetails details) {
    if (_dismissUnderway) return;
    if (details.velocity.pixelsPerSecond.dy > _kMinFlingVelocity) {
      final double flingVelocity =
          -details.velocity.pixelsPerSecond.dy / _childHeight;
      if (widget.animationController.value > 0.0)
        widget.animationController.fling(velocity: flingVelocity);
      if (flingVelocity < 0.0) widget.onClosing();
    } else if (widget.animationController.value < _kCloseProgressThreshold) {
      if (widget.animationController.value > 0.0)
        widget.animationController.fling(velocity: -1.0);
      widget.onClosing();
    } else {
      widget.animationController.forward();
    }
  }

  @override
  Widget build(BuildContext context) {
    return new GestureDetector(
        onVerticalDragUpdate: _handleDragUpdate,
        onVerticalDragEnd: _handleDragEnd,
        child: new Material(key: _childKey, child: widget.builder(context)));
  }
}

// PERSISTENT BOTTOM SHEETS

// See scaffold.dart

// MODAL BOTTOM SHEETS

class _ModalCustomBottomSheetLayout extends SingleChildLayoutDelegate {
  _ModalCustomBottomSheetLayout(this.progress, this.bottomInset);

  final double progress;
  final double bottomInset;

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return new BoxConstraints(
        minWidth: constraints.maxWidth,
        maxWidth: constraints.maxWidth,
        minHeight: 0.0,
        maxHeight: constraints.maxHeight * 9.0 / 16.0);
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return new Offset(
        0.0, size.height - bottomInset - childSize.height * progress);
  }

  @override
  bool shouldRelayout(_ModalCustomBottomSheetLayout oldDelegate) {
    return progress != oldDelegate.progress ||
        bottomInset != oldDelegate.bottomInset;
  }
}

class _ModalCustomBottomSheet<T> extends StatefulWidget {
  const _ModalCustomBottomSheet({Key key, this.route}) : super(key: key);

  final _ModalCustomBottomSheetRoute<T> route;

  @override
  _ModalCustomBottomSheetState<T> createState() =>
      new _ModalCustomBottomSheetState<T>();
}

class _ModalCustomBottomSheetState<T>
    extends State<_ModalCustomBottomSheet<T>> {
  @override
  Widget build(BuildContext context) {
    return new GestureDetector(
        onTap: widget.route.dismissOnTap ? () => Navigator.pop(context) : null,
        child: new AnimatedBuilder(
            animation: widget.route.animation,
            builder: (BuildContext context, Widget child) {
              double bottomInset = widget.route.resizeToAvoidBottomPadding
                  ? MediaQuery.of(context).viewInsets.bottom
                  : 0.0;
              return new ClipRect(
                  child: new CustomSingleChildLayout(
                      delegate: new _ModalCustomBottomSheetLayout(
                          widget.route.animation.value, bottomInset),
                      child: new CustomBottomSheet(
                          animationController:
                              widget.route._animationController,
                          onClosing: () => Navigator.pop(context),
                          builder: widget.route.builder)));
            }));
  }
}

class _ModalCustomBottomSheetRoute<T> extends PopupRoute<T> {
  _ModalCustomBottomSheetRoute({
    this.builder,
    this.theme,
    this.barrierLabel,
    RouteSettings settings,
    this.resizeToAvoidBottomPadding,
    this.dismissOnTap,
  }) : super(settings: settings);

  final WidgetBuilder builder;
  final ThemeData theme;
  final bool resizeToAvoidBottomPadding;
  final bool dismissOnTap;

  @override
  Duration get transitionDuration => _kCustomBottomSheetDuration;

  @override
  bool get barrierDismissible => true;

  @override
  final String barrierLabel;

  @override
  Color get barrierColor => Colors.black54;

  AnimationController _animationController;

  @override
  AnimationController createAnimationController() {
    assert(_animationController == null);
    _animationController =
        CustomBottomSheet.createAnimationController(navigator.overlay);
    return _animationController;
  }

  @override
  Widget buildPage(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation) {
    // By definition, the bottom sheet is aligned to the bottom of the page
    // and isn't exposed to the top padding of the MediaQuery.
    Widget bottomSheet = new MediaQuery.removePadding(
      context: context,
      removeTop: true,
      child: new _ModalCustomBottomSheet<T>(route: this),
    );
    if (theme != null) bottomSheet = new Theme(data: theme, child: bottomSheet);
    return bottomSheet;
  }
}

/// Shows a modal material design bottom sheet.
///
/// A modal bottom sheet is an alternative to a menu or a dialog and prevents
/// the user from interacting with the rest of the app.
///
/// A closely related widget is a persistent bottom sheet, which shows
/// information that supplements the primary content of the app without
/// preventing the use from interacting with the app. Persistent bottom sheets
/// can be created and displayed with the [showCustomBottomSheet] function or the
/// [ScaffoldState.showCustomBottomSheet] method.
///
/// The `context` argument is used to look up the [Navigator] and [Theme] for
/// the bottom sheet. It is only used when the method is called. Its
/// corresponding widget can be safely removed from the tree before the bottom
/// sheet is closed.
///
/// Returns a `Future` that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the modal bottom sheet was closed.
///
/// See also:
///
///  * [CustomBottomSheet], which is the widget normally returned by the function
///    passed as the `builder` argument to [showModalCustomBottomSheet].
///  * [showCustomBottomSheet] and [ScaffoldState.showCustomBottomSheet], for showing
///    non-modal bottom sheets.
///  * <https://material.google.com/components/bottom-sheets.html#bottom-sheets-modal-bottom-sheets>
Future<T> showModalCustomBottomSheet<T>({
  @required BuildContext context,
  @required WidgetBuilder builder,
  bool dismissOnTap: true,
  bool resizeToAvoidBottomPadding: true,
}) {
  assert(context != null);
  assert(builder != null);
  return Navigator.push(
      context,
      new _ModalCustomBottomSheetRoute<T>(
        builder: builder,
        theme: Theme.of(context, shadowThemeOnly: true),
        barrierLabel:
            MaterialLocalizations.of(context).modalBarrierDismissLabel,
        resizeToAvoidBottomPadding: resizeToAvoidBottomPadding,
        dismissOnTap: dismissOnTap,
      ));
}

/// Shows a persistent material design bottom sheet in the nearest [Scaffold].
///
/// A persistent bottom sheet shows information that supplements the primary
/// content of the app. A persistent bottom sheet remains visible even when the
/// user interacts with other parts of the app. A [Scaffold] is required in the
/// given `context`; its [ScaffoldState.showCustomBottomSheet] method is used to
/// actually show the bottom sheet.
///
/// A closely related widget is a modal bottom sheet, which is an alternative
/// to a menu or a dialog and prevents the user from interacting with the rest
/// of the app. Modal bottom sheets can be created and displayed with the
/// [showModalCustomBottomSheet] function.
///
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
/// To rebuild the bottom sheet (e.g. if it is stateful), call
/// [PersistentCustomBottomSheetController.setState] on the value returned from this
/// method.
///
/// The `context` argument is used to look up the [Scaffold] for the bottom
/// sheet. It is only used when the method is called. Its corresponding widget
/// can be safely removed from the tree before the bottom sheet is closed.
///
/// See also:
///
///  * [CustomBottomSheet], which is the widget typically returned by the `builder`.
///  * [showModalCustomBottomSheet], which can be used to display a modal bottom
///    sheet.
///  * [Scaffold.of], for information about how to obtain the [BuildContext].
///  * <https://material.google.com/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets>
PersistentBottomSheetController<T> showCustomBottomSheet<T>({
  @required BuildContext context,
  @required WidgetBuilder builder,
}) {
  assert(context != null);
  assert(builder != null);
  return Scaffold.of(context).showBottomSheet<T>(builder);
}

@richardoey
Copy link

Thank you very much, this is very helpful. I only take some parts of the code, because we have already made our Future builder.
Here are the things when you want to apply this approach.

Find all of the resizeToAvoidBottomPadding and bottomInset in the above code. Those 2 variables are the key to make the keyboard dynamic follow the textfield

bool resizeToAvoidBottomPadding = true,


_RoundedCornerModalRoute<T>(
        builder: builder,
        color: color,
        radius: radius,
        resizeToAvoidBottomPadding: resizeToAvoidBottomPadding,
        barrierLabel:
            MaterialLocalizations.of(context).modalBarrierDismissLabel,
      ));



_RoundedModalBottomSheetLayout(this.progress, this.bottomInset);

  final double progress;
  final double bottomInset;


 
@override
  Offset getPositionForChild(Size size, Size childSize) {
    return new Offset(0.0, size.height - bottomInset - childSize.height * progress);
  }

  @override
  bool shouldRelayout(_RoundedModalBottomSheetLayout oldDelegate) {
    return progress != oldDelegate.progress || bottomInset != oldDelegate.bottomInset;
  }


 return MediaQuery.removePadding(
      context: context,
      removeTop: true,
      child: Theme(
        data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
        child: AnimatedBuilder(
          animation: animation,
          // builder: (context, child) => 
          builder: (context, child) { double bottomInset = resizeToAvoidBottomPadding
          ? MediaQuery.of(context).viewInsets.bottom : 0.0;
            return new CustomSingleChildLayout(
              delegate: _RoundedModalBottomSheetLayout(animation.value, bottomInset),
              child: BottomSheet(
                enableDrag: false,
                animationController: _animationController,
                onClosing: () => Navigator.pop(context),
                builder: (context) => Container(
                      decoration: BoxDecoration(
                        color: this.color,
                        borderRadius: BorderRadius.only(
                          topLeft: Radius.circular(this.radius),
                          topRight: Radius.circular(this.radius),
                        ),
                      ),
                      child: SafeArea(child: Builder(builder: this.builder)),
                    ),
              ),
            );
          }
        ),
      ),
    );

@semutnest
Copy link

@adewu
Copy link

adewu commented Dec 16, 2019

Great, thx a lot

@nicolasvahidzein
Copy link

this is no longer working in 2020....

@yushinzm
Copy link

yushinzm commented Apr 6, 2020

Still Works in 2020. Thanks a lot @

Create a new file in your project called bottom_sheet_custom.dart then import it into another file and call showModalCustomBottomSheet

// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

const Duration _kCustomBottomSheetDuration = const Duration(milliseconds: 200);
const double _kMinFlingVelocity = 700.0;
const double _kCloseProgressThreshold = 0.5;

/// A material design bottom sheet.
///
/// There are two kinds of bottom sheets in material design:
///
///  * _Persistent_. A persistent bottom sheet shows information that
///    supplements the primary content of the app. A persistent bottom sheet
///    remains visible even when the user interacts with other parts of the app.
///    Persistent bottom sheets can be created and displayed with the
///    [ScaffoldState.showCustomBottomSheet] function.
///
///  * _Modal_. A modal bottom sheet is an alternative to a menu or a dialog and
///    prevents the user from interacting with the rest of the app. Modal bottom
///    sheets can be created and displayed with the [showModalCustomBottomSheet]
///    function.
///
/// The [CustomBottomSheet] widget itself is rarely used directly. Instead, prefer to
/// create a persistent bottom sheet with [ScaffoldState.showCustomBottomSheet] and a modal
/// bottom sheet with [showModalCustomBottomSheet].
///
/// See also:
///
///  * [ScaffoldState.showCustomBottomSheet]
///  * [showModalCustomBottomSheet]
///  * <https://material.google.com/components/bottom-sheets.html>
class CustomBottomSheet extends StatefulWidget {
  /// Creates a bottom sheet.
  ///
  /// Typically, bottom sheets are created implicitly by
  /// [ScaffoldState.showCustomBottomSheet], for persistent bottom sheets, or by
  /// [showModalCustomBottomSheet], for modal bottom sheets.
  const CustomBottomSheet(
      {Key key,
      this.animationController,
      @required this.onClosing,
      @required this.builder})
      : assert(onClosing != null),
        assert(builder != null),
        super(key: key);

  /// The animation that controls the bottom sheet's position.
  ///
  /// The CustomBottomSheet widget will manipulate the position of this animation, it
  /// is not just a passive observer.
  final AnimationController animationController;

  /// Called when the bottom sheet begins to close.
  ///
  /// A bottom sheet might be be prevented from closing (e.g., by user
  /// interaction) even after this callback is called. For this reason, this
  /// callback might be call multiple times for a given bottom sheet.
  final VoidCallback onClosing;

  /// A builder for the contents of the sheet.
  ///
  /// The bottom sheet will wrap the widget produced by this builder in a
  /// [Material] widget.
  final WidgetBuilder builder;

  @override
  _CustomBottomSheetState createState() => new _CustomBottomSheetState();

  /// Creates an animation controller suitable for controlling a [CustomBottomSheet].
  static AnimationController createAnimationController(TickerProvider vsync) {
    return new AnimationController(
      duration: _kCustomBottomSheetDuration,
      debugLabel: 'CustomBottomSheet',
      vsync: vsync,
    );
  }
}

class _CustomBottomSheetState extends State<CustomBottomSheet> {
  final GlobalKey _childKey =
      new GlobalKey(debugLabel: 'CustomBottomSheet child');

  double get _childHeight {
    final RenderBox renderBox = _childKey.currentContext.findRenderObject();
    return renderBox.size.height;
  }

bool get _dismissUnderway =>
widget.animationController.status == AnimationStatus.reverse;

void _handleDragUpdate(DragUpdateDetails details) {
if (_dismissUnderway) return;
widget.animationController.value -=
details.primaryDelta / (_childHeight ?? details.primaryDelta);
}

void _handleDragEnd(DragEndDetails details) {
if (_dismissUnderway) return;
if (details.velocity.pixelsPerSecond.dy > _kMinFlingVelocity) {
final double flingVelocity =
-details.velocity.pixelsPerSecond.dy / _childHeight;
if (widget.animationController.value > 0.0)
widget.animationController.fling(velocity: flingVelocity);
if (flingVelocity < 0.0) widget.onClosing();
} else if (widget.animationController.value < _kCloseProgressThreshold) {
if (widget.animationController.value > 0.0)
widget.animationController.fling(velocity: -1.0);
widget.onClosing();
} else {
widget.animationController.forward();
}
}

@OverRide
Widget build(BuildContext context) {
return new GestureDetector(
onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: _handleDragEnd,
child: new Material(key: _childKey, child: widget.builder(context)));
}
}

// PERSISTENT BOTTOM SHEETS

// See scaffold.dart

// MODAL BOTTOM SHEETS

class _ModalCustomBottomSheetLayout extends SingleChildLayoutDelegate {
_ModalCustomBottomSheetLayout(this.progress, this.bottomInset);

final double progress;
final double bottomInset;

@OverRide
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return new BoxConstraints(
minWidth: constraints.maxWidth,
maxWidth: constraints.maxWidth,
minHeight: 0.0,
maxHeight: constraints.maxHeight * 9.0 / 16.0);
}

@OverRide
Offset getPositionForChild(Size size, Size childSize) {
return new Offset(
0.0, size.height - bottomInset - childSize.height * progress);
}

@OverRide
bool shouldRelayout(_ModalCustomBottomSheetLayout oldDelegate) {
return progress != oldDelegate.progress ||
bottomInset != oldDelegate.bottomInset;
}
}

class _ModalCustomBottomSheet extends StatefulWidget {
const _ModalCustomBottomSheet({Key key, this.route}) : super(key: key);

final _ModalCustomBottomSheetRoute route;

@OverRide
_ModalCustomBottomSheetState createState() =>
new _ModalCustomBottomSheetState();
}

class _ModalCustomBottomSheetState
extends State<_ModalCustomBottomSheet> {
@OverRide
Widget build(BuildContext context) {
return new GestureDetector(
onTap: widget.route.dismissOnTap ? () => Navigator.pop(context) : null,
child: new AnimatedBuilder(
animation: widget.route.animation,
builder: (BuildContext context, Widget child) {
double bottomInset = widget.route.resizeToAvoidBottomPadding
? MediaQuery.of(context).viewInsets.bottom
: 0.0;
return new ClipRect(
child: new CustomSingleChildLayout(
delegate: new _ModalCustomBottomSheetLayout(
widget.route.animation.value, bottomInset),
child: new CustomBottomSheet(
animationController:
widget.route._animationController,
onClosing: () => Navigator.pop(context),
builder: widget.route.builder)));
}));
}
}

class _ModalCustomBottomSheetRoute extends PopupRoute {
_ModalCustomBottomSheetRoute({
this.builder,
this.theme,
this.barrierLabel,
RouteSettings settings,
this.resizeToAvoidBottomPadding,
this.dismissOnTap,
}) : super(settings: settings);

final WidgetBuilder builder;
final ThemeData theme;
final bool resizeToAvoidBottomPadding;
final bool dismissOnTap;

@OverRide
Duration get transitionDuration => _kCustomBottomSheetDuration;

@OverRide
bool get barrierDismissible => true;

@OverRide
final String barrierLabel;

@OverRide
Color get barrierColor => Colors.black54;

AnimationController _animationController;

@OverRide
AnimationController createAnimationController() {
assert(_animationController == null);
_animationController =
CustomBottomSheet.createAnimationController(navigator.overlay);
return _animationController;
}

@OverRide
Widget buildPage(BuildContext context, Animation animation,
Animation secondaryAnimation) {
// By definition, the bottom sheet is aligned to the bottom of the page
// and isn't exposed to the top padding of the MediaQuery.
Widget bottomSheet = new MediaQuery.removePadding(
context: context,
removeTop: true,
child: new _ModalCustomBottomSheet(route: this),
);
if (theme != null) bottomSheet = new Theme(data: theme, child: bottomSheet);
return bottomSheet;
}
}

/// Shows a modal material design bottom sheet.
///
/// A modal bottom sheet is an alternative to a menu or a dialog and prevents
/// the user from interacting with the rest of the app.
///
/// A closely related widget is a persistent bottom sheet, which shows
/// information that supplements the primary content of the app without
/// preventing the use from interacting with the app. Persistent bottom sheets
/// can be created and displayed with the [showCustomBottomSheet] function or the
/// [ScaffoldState.showCustomBottomSheet] method.
///
/// The context argument is used to look up the [Navigator] and [Theme] for
/// the bottom sheet. It is only used when the method is called. Its
/// corresponding widget can be safely removed from the tree before the bottom
/// sheet is closed.
///
/// Returns a Future that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the modal bottom sheet was closed.
///
/// See also:
///
/// * [CustomBottomSheet], which is the widget normally returned by the function
/// passed as the builder argument to [showModalCustomBottomSheet].
/// * [showCustomBottomSheet] and [ScaffoldState.showCustomBottomSheet], for showing
/// non-modal bottom sheets.
/// * https://material.google.com/components/bottom-sheets.html#bottom-sheets-modal-bottom-sheets
Future showModalCustomBottomSheet({
@required BuildContext context,
@required WidgetBuilder builder,
bool dismissOnTap: true,
bool resizeToAvoidBottomPadding: true,
}) {
assert(context != null);
assert(builder != null);
return Navigator.push(
context,
new _ModalCustomBottomSheetRoute(
builder: builder,
theme: Theme.of(context, shadowThemeOnly: true),
barrierLabel:
MaterialLocalizations.of(context).modalBarrierDismissLabel,
resizeToAvoidBottomPadding: resizeToAvoidBottomPadding,
dismissOnTap: dismissOnTap,
));
}

/// Shows a persistent material design bottom sheet in the nearest [Scaffold].
///
/// A persistent bottom sheet shows information that supplements the primary
/// content of the app. A persistent bottom sheet remains visible even when the
/// user interacts with other parts of the app. A [Scaffold] is required in the
/// given context; its [ScaffoldState.showCustomBottomSheet] method is used to
/// actually show the bottom sheet.
///
/// A closely related widget is a modal bottom sheet, which is an alternative
/// to a menu or a dialog and prevents the user from interacting with the rest
/// of the app. Modal bottom sheets can be created and displayed with the
/// [showModalCustomBottomSheet] function.
///
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
/// To rebuild the bottom sheet (e.g. if it is stateful), call
/// [PersistentCustomBottomSheetController.setState] on the value returned from this
/// method.
///
/// The context argument is used to look up the [Scaffold] for the bottom
/// sheet. It is only used when the method is called. Its corresponding widget
/// can be safely removed from the tree before the bottom sheet is closed.
///
/// See also:
///
/// * [CustomBottomSheet], which is the widget typically returned by the builder.
/// * [showModalCustomBottomSheet], which can be used to display a modal bottom
/// sheet.
/// * [Scaffold.of], for information about how to obtain the [BuildContext].
/// * https://material.google.com/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets
PersistentBottomSheetController showCustomBottomSheet({
@required BuildContext context,
@required WidgetBuilder builder,
}) {
assert(context != null);
assert(builder != null);
return Scaffold.of(context).showBottomSheet(builder);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment