Commit 8a8bd019 authored by Hans Muller's avatar Hans Muller

Merge pull request #62 from HansMuller/bottom_sheet_drag

Support drag-to-dismiss in persistent bottom sheets

Flinging a persistent bottom sheet downwards dismisses it, per the Material spec.

The showBottomSheet() function now returns a Future, like showModalBottomSheet() does, so that you can discover when it's been dismissed - with navigator.pop() - and with what value.

Factored the drag gesture handling code into _BottomSheetDragController

This CL was flutter/engine#1997
parents d355611d da4fbdd4
...@@ -14,7 +14,53 @@ import 'material.dart'; ...@@ -14,7 +14,53 @@ import 'material.dart';
const Duration _kBottomSheetDuration = const Duration(milliseconds: 200); const Duration _kBottomSheetDuration = const Duration(milliseconds: 200);
const double _kMinFlingVelocity = 700.0; const double _kMinFlingVelocity = 700.0;
const double _kFlingVelocityScale = 1.0 / 300.0; const double _kCloseProgressThreshold = 0.5;
class _BottomSheetDragController extends StatelessComponent {
_BottomSheetDragController({
Key key,
this.performance,
this.child,
this.childHeight
}) : super(key: key);
final Performance performance;
final Widget child;
final double childHeight;
bool get _dismissUnderway => performance.direction == AnimationDirection.reverse;
void _handleDragUpdate(double delta) {
if (_dismissUnderway)
return;
performance.progress -= delta / (childHeight ?? delta);
}
void _handleDragEnd(Offset velocity, BuildContext context) {
if (_dismissUnderway)
return;
if (velocity.dy > _kMinFlingVelocity) {
performance.fling(velocity: -velocity.dy / childHeight).then((_) {
Navigator.of(context).pop();
});
} else if (performance.progress < _kCloseProgressThreshold) {
performance.fling(velocity: -1.0).then((_) {
Navigator.of(context).pop();
});
} else {
performance.forward();
}
}
Widget build(BuildContext context) {
return new GestureDetector(
onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: (Offset velocity) { _handleDragEnd(velocity, context); },
child: child
);
}
}
class _ModalBottomSheet extends StatefulComponent { class _ModalBottomSheet extends StatefulComponent {
_ModalBottomSheet({ Key key, this.route }) : super(key: key); _ModalBottomSheet({ Key key, this.route }) : super(key: key);
...@@ -46,26 +92,6 @@ class _ModalBottomSheetLayout extends OneChildLayoutDelegate { ...@@ -46,26 +92,6 @@ class _ModalBottomSheetLayout extends OneChildLayoutDelegate {
class _ModalBottomSheetState extends State<_ModalBottomSheet> { class _ModalBottomSheetState extends State<_ModalBottomSheet> {
final _ModalBottomSheetLayout _layout = new _ModalBottomSheetLayout(); final _ModalBottomSheetLayout _layout = new _ModalBottomSheetLayout();
bool _dragEnabled = false;
void _handleDragStart(Point position) {
_dragEnabled = !config.route._performance.isAnimating;
}
void _handleDragUpdate(double delta) {
if (!_dragEnabled)
return;
config.route._performance.progress -= delta / _layout.childTop.end;
}
void _handleDragEnd(Offset velocity) {
if (!_dragEnabled)
return;
if (velocity.dy > _kMinFlingVelocity)
config.route._performance.fling(velocity: -velocity.dy * _kFlingVelocityScale);
else
config.route._performance.forward();
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new GestureDetector( return new GestureDetector(
...@@ -78,11 +104,10 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> { ...@@ -78,11 +104,10 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> {
child: new CustomOneChildLayout( child: new CustomOneChildLayout(
delegate: _layout, delegate: _layout,
token: _layout.childTop.value, token: _layout.childTop.value,
child: new GestureDetector( child: new _BottomSheetDragController(
onVerticalDragStart: _handleDragStart, performance: config.route._performance,
onVerticalDragUpdate: _handleDragUpdate, child: new Material(child: config.route.child),
onVerticalDragEnd: _handleDragEnd, childHeight: _layout.childTop.end
child: new Material(child: config.route.child)
) )
) )
); );
...@@ -93,9 +118,7 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> { ...@@ -93,9 +118,7 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> {
} }
class _ModalBottomSheetRoute extends ModalRoute { class _ModalBottomSheetRoute extends ModalRoute {
_ModalBottomSheetRoute({ this.completer, this.child }) { _ModalBottomSheetRoute({ this.completer, this.child });
_performance = new Performance(duration: transitionDuration, debugLabel: 'ModalBottomSheet');
}
final Completer completer; final Completer completer;
final Widget child; final Widget child;
...@@ -129,35 +152,66 @@ Future showModalBottomSheet({ BuildContext context, Widget child }) { ...@@ -129,35 +152,66 @@ Future showModalBottomSheet({ BuildContext context, Widget child }) {
return completer.future; return completer.future;
} }
class _PersistentBottomSheet extends StatelessComponent { class _PersistentBottomSheet extends StatefulComponent {
_PersistentBottomSheet({ _PersistentBottomSheet({ Key key, this.route }) : super(key: key);
Key key,
this.child,
this.route
}) : super(key: key);
final TransitionRoute route; final _PersistentBottomSheetRoute route;
final Widget child;
_PersistentBottomSheetState createState() => new _PersistentBottomSheetState();
}
class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {
double _childHeight;
void _updateChildHeight(Size newSize) {
setState(() {
_childHeight = newSize.height;
});
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new AlignTransition( return new AlignTransition(
performance: route.performance, performance: config.route.performance,
alignment: new AnimatedValue<FractionalOffset>(const FractionalOffset(0.0, 0.0)), alignment: new AnimatedValue<FractionalOffset>(const FractionalOffset(0.0, 0.0)),
heightFactor: new AnimatedValue<double>(0.0, end: 1.0), heightFactor: new AnimatedValue<double>(0.0, end: 1.0),
child: child child: new _BottomSheetDragController(
performance: config.route._performance,
childHeight: _childHeight,
child: new Material(
child: new SizeObserver(child: config.route.child, onSizeChanged: _updateChildHeight)
)
)
); );
} }
} }
class _PersistentBottomSheetRoute extends TransitionRoute { class _PersistentBottomSheetRoute extends TransitionRoute {
_PersistentBottomSheetRoute({ this.completer, this.child });
final Widget child;
final Completer completer;
bool get opaque => false; bool get opaque => false;
Duration get transitionDuration => _kBottomSheetDuration; Duration get transitionDuration => _kBottomSheetDuration;
Performance _performance;
Performance createPerformance() {
_performance = super.createPerformance();
return _performance;
}
void didPop([dynamic result]) {
completer.complete(result);
super.didPop(result);
}
} }
void showBottomSheet({ BuildContext context, GlobalKey<PlaceholderState> placeholderKey, Widget child }) { Future showBottomSheet({ BuildContext context, GlobalKey<PlaceholderState> placeholderKey, Widget child }) {
assert(child != null); assert(child != null);
assert(placeholderKey != null); assert(placeholderKey != null);
_PersistentBottomSheetRoute route = new _PersistentBottomSheetRoute(); final Completer completer = new Completer();
placeholderKey.currentState.child = new _PersistentBottomSheet(route: route, child: child); _PersistentBottomSheetRoute route = new _PersistentBottomSheetRoute(child: child, completer: completer);
placeholderKey.currentState.child = new _PersistentBottomSheet(route: route);
Navigator.of(context).pushEphemeral(route); Navigator.of(context).pushEphemeral(route);
return completer.future;
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment