mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Convert Persistent Bottom Sheets to a Scaffold API
- `Scaffold.of(context).showBottomSheet(widget);` - Returns an object with .closed Future and .close() method. - Uses a StateRoute to handle back button. - Take the Navigator logic out of the BottomSheet widget. - Support showing a sheet while an old one is going away. - Add Navigator.remove().
This commit is contained in:
parent
6a2bd421a1
commit
03e094aa1b
@ -20,7 +20,6 @@ class StockHome extends StatefulComponent {
|
||||
class StockHomeState extends State<StockHome> {
|
||||
|
||||
final GlobalKey scaffoldKey = new GlobalKey();
|
||||
final GlobalKey<PlaceholderState> _bottomSheetPlaceholderKey = new GlobalKey<PlaceholderState>();
|
||||
bool _isSearching = false;
|
||||
String _searchQuery;
|
||||
|
||||
@ -202,11 +201,7 @@ class StockHomeState extends State<StockHome> {
|
||||
Navigator.of(context).pushNamed('/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys);
|
||||
},
|
||||
onShow: (Stock stock, Key arrowKey) {
|
||||
showBottomSheet(
|
||||
placeholderKey: _bottomSheetPlaceholderKey,
|
||||
context: context,
|
||||
child: new StockSymbolBottomSheet(stock: stock)
|
||||
);
|
||||
scaffoldKey.currentState.showBottomSheet((BuildContext context) => new StockSymbolBottomSheet(stock: stock));
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -256,12 +251,14 @@ class StockHomeState extends State<StockHome> {
|
||||
showModalBottomSheet(
|
||||
// TODO(ianh): Fill this out.
|
||||
context: context,
|
||||
child: new Column([
|
||||
new Input(
|
||||
key: companyNameKey,
|
||||
placeholder: 'Company Name'
|
||||
),
|
||||
])
|
||||
builder: (BuildContext context) {
|
||||
return new Column([
|
||||
new Input(
|
||||
key: companyNameKey,
|
||||
placeholder: 'Company Name'
|
||||
),
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -278,7 +275,6 @@ class StockHomeState extends State<StockHome> {
|
||||
key: scaffoldKey,
|
||||
toolBar: _isSearching ? buildSearchBar() : buildToolBar(),
|
||||
body: buildTabNavigator(),
|
||||
bottomSheet: new Placeholder(key: _bottomSheetPlaceholderKey),
|
||||
floatingActionButton: buildFloatingActionButton()
|
||||
);
|
||||
}
|
||||
|
@ -80,11 +80,11 @@ class StockSymbolBottomSheet extends StatelessComponent {
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new Container(
|
||||
child: new StockSymbolView(stock: stock),
|
||||
padding: new EdgeDims.all(10.0),
|
||||
decoration: new BoxDecoration(
|
||||
border: new Border(top: new BorderSide(color: Colors.black26, width: 1.0))
|
||||
)
|
||||
),
|
||||
child: new StockSymbolView(stock: stock)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -18,17 +18,31 @@ const double _kCloseProgressThreshold = 0.5;
|
||||
const Color _kTransparent = const Color(0x00000000);
|
||||
const Color _kBarrierColor = Colors.black54;
|
||||
|
||||
class _BottomSheetDragController extends StatelessComponent {
|
||||
_BottomSheetDragController({
|
||||
class BottomSheet extends StatelessComponent {
|
||||
BottomSheet({
|
||||
Key key,
|
||||
this.performance,
|
||||
this.child,
|
||||
this.childHeight
|
||||
}) : super(key: key);
|
||||
this.onClosing,
|
||||
this.childHeight,
|
||||
this.builder
|
||||
}) : super(key: key) {
|
||||
assert(onClosing != null);
|
||||
}
|
||||
|
||||
/// The performance that controls the bottom sheet's position. The BottomSheet
|
||||
/// widget will manipulate the position of this performance, it is not just a
|
||||
/// passive observer.
|
||||
final Performance performance;
|
||||
final Widget child;
|
||||
final VoidCallback onClosing;
|
||||
final double childHeight;
|
||||
final WidgetBuilder builder;
|
||||
|
||||
static Performance createPerformance() {
|
||||
return new Performance(
|
||||
duration: _kBottomSheetDuration,
|
||||
debugLabel: 'BottomSheet'
|
||||
);
|
||||
}
|
||||
|
||||
bool get _dismissUnderway => performance.direction == AnimationDirection.reverse;
|
||||
|
||||
@ -42,13 +56,11 @@ class _BottomSheetDragController extends StatelessComponent {
|
||||
if (_dismissUnderway)
|
||||
return;
|
||||
if (velocity.dy > _kMinFlingVelocity) {
|
||||
performance.fling(velocity: -velocity.dy / childHeight).then((_) {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
performance.fling(velocity: -velocity.dy / childHeight);
|
||||
onClosing();
|
||||
} else if (performance.progress < _kCloseProgressThreshold) {
|
||||
performance.fling(velocity: -1.0).then((_) {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
performance.fling(velocity: -1.0);
|
||||
onClosing();
|
||||
} else {
|
||||
performance.forward();
|
||||
}
|
||||
@ -58,46 +70,19 @@ class _BottomSheetDragController extends StatelessComponent {
|
||||
return new GestureDetector(
|
||||
onVerticalDragUpdate: _handleDragUpdate,
|
||||
onVerticalDragEnd: (Offset velocity) { _handleDragEnd(velocity, context); },
|
||||
child: child
|
||||
child: new Material(
|
||||
child: builder(context)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomSheetRoute extends OverlayRoute {
|
||||
_BottomSheetRoute({ this.completer, this.child });
|
||||
// PERSISTENT BOTTOM SHEETS
|
||||
|
||||
final Completer completer;
|
||||
final Widget child;
|
||||
Performance performance;
|
||||
// See scaffold.dart
|
||||
|
||||
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
|
||||
performance = new Performance(duration: _kBottomSheetDuration, debugLabel: debugLabel)
|
||||
..forward();
|
||||
super.didPush(overlay, insertionPoint);
|
||||
}
|
||||
|
||||
void didPop(dynamic result) {
|
||||
void finish() {
|
||||
super.didPop(result); // clear the overlay entries
|
||||
completer.complete(result);
|
||||
}
|
||||
if (performance.isDismissed)
|
||||
finish();
|
||||
else
|
||||
performance.reverse().then((_) { finish(); });
|
||||
}
|
||||
|
||||
String get debugLabel => '$runtimeType';
|
||||
String toString() => '$runtimeType(performance: $performance)';
|
||||
}
|
||||
|
||||
class _ModalBottomSheet extends StatefulComponent {
|
||||
_ModalBottomSheet({ Key key, this.route }) : super(key: key);
|
||||
|
||||
final _ModalBottomSheetRoute route;
|
||||
|
||||
_ModalBottomSheetState createState() => new _ModalBottomSheetState();
|
||||
}
|
||||
// MODAL BOTTOM SHEETS
|
||||
|
||||
class _ModalBottomSheetLayout extends OneChildLayoutDelegate {
|
||||
// The distance from the bottom of the parent to the top of the BottomSheet child.
|
||||
@ -118,6 +103,14 @@ class _ModalBottomSheetLayout extends OneChildLayoutDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
class _ModalBottomSheet extends StatefulComponent {
|
||||
_ModalBottomSheet({ Key key, this.route }) : super(key: key);
|
||||
|
||||
final _ModalBottomSheetRoute route;
|
||||
|
||||
_ModalBottomSheetState createState() => new _ModalBottomSheetState();
|
||||
}
|
||||
|
||||
class _ModalBottomSheetState extends State<_ModalBottomSheet> {
|
||||
|
||||
final _ModalBottomSheetLayout _layout = new _ModalBottomSheetLayout();
|
||||
@ -133,10 +126,11 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> {
|
||||
child: new CustomOneChildLayout(
|
||||
delegate: _layout,
|
||||
token: _layout.childTop.value,
|
||||
child: new _BottomSheetDragController(
|
||||
child: new BottomSheet(
|
||||
performance: config.route.performance,
|
||||
child: new Material(child: config.route.child),
|
||||
childHeight: _layout.childTop.end
|
||||
onClosing: () { Navigator.of(context).pop(); },
|
||||
childHeight: _layout.childTop.end,
|
||||
builder: config.route.builder
|
||||
)
|
||||
)
|
||||
);
|
||||
@ -146,9 +140,30 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ModalBottomSheetRoute extends _BottomSheetRoute {
|
||||
_ModalBottomSheetRoute({ Completer completer, Widget child })
|
||||
: super(completer: completer, child: child);
|
||||
class _ModalBottomSheetRoute extends OverlayRoute {
|
||||
_ModalBottomSheetRoute({ this.completer, this.builder });
|
||||
|
||||
final Completer completer;
|
||||
final WidgetBuilder builder;
|
||||
Performance performance;
|
||||
|
||||
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
|
||||
performance = BottomSheet.createPerformance()
|
||||
..forward();
|
||||
super.didPush(overlay, insertionPoint);
|
||||
}
|
||||
|
||||
void _finish(dynamic result) {
|
||||
super.didPop(result); // clear the overlay entries
|
||||
completer.complete(result);
|
||||
}
|
||||
|
||||
void didPop(dynamic result) {
|
||||
if (performance.isDismissed)
|
||||
_finish(result);
|
||||
else
|
||||
performance.reverse().then((_) { _finish(result); });
|
||||
}
|
||||
|
||||
Widget _buildModalBarrier(BuildContext context) {
|
||||
return new AnimatedModalBarrier(
|
||||
@ -168,61 +183,18 @@ class _ModalBottomSheetRoute extends _BottomSheetRoute {
|
||||
_buildModalBarrier,
|
||||
_buildBottomSheet,
|
||||
];
|
||||
|
||||
String get debugLabel => '$runtimeType';
|
||||
String toString() => '$runtimeType(performance: $performance)';
|
||||
}
|
||||
|
||||
Future showModalBottomSheet({ BuildContext context, Widget child }) {
|
||||
assert(child != null);
|
||||
Future showModalBottomSheet({ BuildContext context, WidgetBuilder builder }) {
|
||||
assert(context != null);
|
||||
assert(builder != null);
|
||||
final Completer completer = new Completer();
|
||||
Navigator.of(context).pushEphemeral(new _ModalBottomSheetRoute(
|
||||
completer: completer,
|
||||
child: child
|
||||
builder: builder
|
||||
));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
class _PersistentBottomSheet extends StatefulComponent {
|
||||
_PersistentBottomSheet({ Key key, this.route }) : super(key: key);
|
||||
|
||||
final _BottomSheetRoute route;
|
||||
|
||||
_PersistentBottomSheetState createState() => new _PersistentBottomSheetState();
|
||||
}
|
||||
|
||||
class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {
|
||||
|
||||
double _childHeight;
|
||||
void _updateChildHeight(Size newSize) {
|
||||
setState(() {
|
||||
_childHeight = newSize.height;
|
||||
});
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new AlignTransition(
|
||||
performance: config.route.performance,
|
||||
alignment: new AnimatedValue<FractionalOffset>(const FractionalOffset(0.0, 0.0)),
|
||||
heightFactor: new AnimatedValue<double>(0.0, end: 1.0),
|
||||
child: new _BottomSheetDragController(
|
||||
performance: config.route.performance,
|
||||
childHeight: _childHeight,
|
||||
child: new Material(
|
||||
child: new SizeObserver(child: config.route.child, onSizeChanged: _updateChildHeight)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future showBottomSheet({ BuildContext context, GlobalKey<PlaceholderState> placeholderKey, Widget child }) {
|
||||
assert(child != null);
|
||||
assert(placeholderKey != null);
|
||||
final Completer completer = new Completer();
|
||||
_BottomSheetRoute route = new _BottomSheetRoute(child: child, completer: completer);
|
||||
placeholderKey.currentState.child = new _PersistentBottomSheet(route: route);
|
||||
Navigator.of(context).pushEphemeral(route);
|
||||
return completer.future.then((_) {
|
||||
// If our overlay has been obscured by an opaque OverlayEntry then currentState
|
||||
// will have been cleared already.
|
||||
placeholderKey.currentState?.child = null;
|
||||
});
|
||||
}
|
||||
|
@ -11,9 +11,10 @@ import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'bottom_sheet.dart';
|
||||
import 'material.dart';
|
||||
import 'tool_bar.dart';
|
||||
import 'snack_bar.dart';
|
||||
import 'tool_bar.dart';
|
||||
|
||||
const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent
|
||||
|
||||
@ -57,7 +58,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
||||
|
||||
if (isChild(_Child.bottomSheet)) {
|
||||
bottomSheetSize = layoutChild(_Child.bottomSheet, fullWidthConstraints);
|
||||
positionChild(_Child.bottomSheet, new Point(0.0, size.height - bottomSheetSize.height));
|
||||
positionChild(_Child.bottomSheet, new Point((size.width - bottomSheetSize.width) / 2.0, size.height - bottomSheetSize.height));
|
||||
}
|
||||
|
||||
if (isChild(_Child.snackBar)) {
|
||||
@ -85,13 +86,11 @@ class Scaffold extends StatefulComponent {
|
||||
Key key,
|
||||
this.toolBar,
|
||||
this.body,
|
||||
this.bottomSheet,
|
||||
this.floatingActionButton
|
||||
}) : super(key: key);
|
||||
|
||||
final ToolBar toolBar;
|
||||
final Widget body;
|
||||
final Widget bottomSheet; // this is for non-modal bottom sheets
|
||||
final Widget floatingActionButton;
|
||||
|
||||
static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(ScaffoldState);
|
||||
@ -101,6 +100,8 @@ class Scaffold extends StatefulComponent {
|
||||
|
||||
class ScaffoldState extends State<Scaffold> {
|
||||
|
||||
// SNACKBAR API
|
||||
|
||||
Queue<SnackBar> _snackBars = new Queue<SnackBar>();
|
||||
Performance _snackBarPerformance;
|
||||
Timer _snackBarTimer;
|
||||
@ -108,6 +109,10 @@ class ScaffoldState extends State<Scaffold> {
|
||||
void showSnackBar(SnackBar snackbar) {
|
||||
_snackBarPerformance ??= SnackBar.createPerformance()
|
||||
..addStatusListener(_handleSnackBarStatusChange);
|
||||
if (_snackBars.isEmpty) {
|
||||
assert(_snackBarPerformance.isDismissed);
|
||||
_snackBarPerformance.forward();
|
||||
}
|
||||
setState(() {
|
||||
_snackBars.addLast(snackbar.withPerformance(_snackBarPerformance));
|
||||
});
|
||||
@ -120,6 +125,8 @@ class ScaffoldState extends State<Scaffold> {
|
||||
setState(() {
|
||||
_snackBars.removeFirst();
|
||||
});
|
||||
if (_snackBars.isNotEmpty)
|
||||
_snackBarPerformance.forward();
|
||||
break;
|
||||
case PerformanceStatus.completed:
|
||||
setState(() {
|
||||
@ -138,6 +145,63 @@ class ScaffoldState extends State<Scaffold> {
|
||||
_snackBarTimer = null;
|
||||
}
|
||||
|
||||
|
||||
// PERSISTENT BOTTOM SHEET API
|
||||
|
||||
List<Widget> _dismissedBottomSheets;
|
||||
BottomSheetController _currentBottomSheet;
|
||||
|
||||
BottomSheetController showBottomSheet(WidgetBuilder builder) {
|
||||
if (_currentBottomSheet != null) {
|
||||
_currentBottomSheet.close();
|
||||
assert(_currentBottomSheet == null);
|
||||
}
|
||||
Completer completer = new Completer();
|
||||
GlobalKey<_PersistentBottomSheetState> bottomSheetKey = new GlobalKey<_PersistentBottomSheetState>();
|
||||
Performance performance = BottomSheet.createPerformance()
|
||||
..forward();
|
||||
_PersistentBottomSheet bottomSheet;
|
||||
Route route = new StateRoute(
|
||||
onPop: () {
|
||||
assert(_currentBottomSheet._widget == bottomSheet);
|
||||
assert(bottomSheetKey.currentState != null);
|
||||
bottomSheetKey.currentState.close();
|
||||
_dismissedBottomSheets ??= <Widget>[];
|
||||
_dismissedBottomSheets.add(bottomSheet);
|
||||
_currentBottomSheet = null;
|
||||
completer.complete();
|
||||
}
|
||||
);
|
||||
bottomSheet = new _PersistentBottomSheet(
|
||||
key: bottomSheetKey,
|
||||
performance: performance,
|
||||
onClosing: () {
|
||||
assert(_currentBottomSheet._widget == bottomSheet);
|
||||
Navigator.of(context).remove(route);
|
||||
},
|
||||
onDismissed: () {
|
||||
assert(_dismissedBottomSheets != null);
|
||||
setState(() {
|
||||
_dismissedBottomSheets.remove(bottomSheet);
|
||||
});
|
||||
},
|
||||
builder: builder
|
||||
);
|
||||
Navigator.of(context).push(route);
|
||||
setState(() {
|
||||
_currentBottomSheet = new BottomSheetController._(
|
||||
bottomSheet,
|
||||
completer.future,
|
||||
() => Navigator.of(context).remove(route),
|
||||
setState
|
||||
);
|
||||
});
|
||||
return _currentBottomSheet;
|
||||
}
|
||||
|
||||
|
||||
// INTERNALS
|
||||
|
||||
void dispose() {
|
||||
_snackBarPerformance?.stop();
|
||||
_snackBarPerformance = null;
|
||||
@ -156,8 +220,6 @@ class ScaffoldState extends State<Scaffold> {
|
||||
final Widget materialBody = config.body != null ? new Material(child: config.body) : null;
|
||||
|
||||
if (_snackBars.length > 0) {
|
||||
if (_snackBarPerformance.isDismissed)
|
||||
_snackBarPerformance.forward();
|
||||
ModalRoute route = ModalRoute.of(context);
|
||||
if (route == null || route.isCurrent) {
|
||||
if (_snackBarPerformance.isCompleted && _snackBarTimer == null)
|
||||
@ -171,12 +233,105 @@ class ScaffoldState extends State<Scaffold> {
|
||||
final List<LayoutId>children = new List<LayoutId>();
|
||||
_addIfNonNull(children, materialBody, _Child.body);
|
||||
_addIfNonNull(children, paddedToolBar, _Child.toolBar);
|
||||
_addIfNonNull(children, config.bottomSheet, _Child.bottomSheet);
|
||||
|
||||
if (_currentBottomSheet != null ||
|
||||
(_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty)) {
|
||||
List<Widget> bottomSheets = <Widget>[];
|
||||
if (_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty)
|
||||
bottomSheets.addAll(_dismissedBottomSheets);
|
||||
if (_currentBottomSheet != null)
|
||||
bottomSheets.add(_currentBottomSheet._widget);
|
||||
Widget stack = new Stack(
|
||||
bottomSheets,
|
||||
alignment: const FractionalOffset(0.5, 1.0) // bottom-aligned, centered
|
||||
);
|
||||
_addIfNonNull(children, stack, _Child.bottomSheet);
|
||||
}
|
||||
|
||||
if (_snackBars.isNotEmpty)
|
||||
_addIfNonNull(children, _snackBars.first, _Child.snackBar);
|
||||
|
||||
_addIfNonNull(children, config.floatingActionButton, _Child.floatingActionButton);
|
||||
|
||||
return new CustomMultiChildLayout(children, delegate: _scaffoldLayout);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class BottomSheetController {
|
||||
const BottomSheetController._(this._widget, this.closed, this.close, this.setState);
|
||||
final Widget _widget;
|
||||
final Future closed;
|
||||
final VoidCallback close; // call this to close the bottom sheet
|
||||
final StateSetter setState;
|
||||
}
|
||||
|
||||
class _PersistentBottomSheet extends StatefulComponent {
|
||||
_PersistentBottomSheet({
|
||||
Key key,
|
||||
this.performance,
|
||||
this.onClosing,
|
||||
this.onDismissed,
|
||||
this.builder
|
||||
}) : super(key: key);
|
||||
|
||||
final Performance performance;
|
||||
final VoidCallback onClosing;
|
||||
final VoidCallback onDismissed;
|
||||
final WidgetBuilder builder;
|
||||
|
||||
_PersistentBottomSheetState createState() => new _PersistentBottomSheetState();
|
||||
}
|
||||
|
||||
class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {
|
||||
|
||||
// We take ownership of the performance given in the first configuration.
|
||||
// We also share control of that performance with out BottomSheet widget.
|
||||
|
||||
void initState() {
|
||||
super.initState();
|
||||
assert(config.performance.status == PerformanceStatus.forward);
|
||||
config.performance.addStatusListener(_handleStatusChange);
|
||||
}
|
||||
|
||||
void didUpdateConfig(_PersistentBottomSheet oldConfig) {
|
||||
super.didUpdateConfig(oldConfig);
|
||||
assert(config.performance == oldConfig.performance);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
config.performance.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void close() {
|
||||
config.performance.reverse();
|
||||
}
|
||||
|
||||
void _handleStatusChange(PerformanceStatus status) {
|
||||
if (status == PerformanceStatus.dismissed && config.onDismissed != null)
|
||||
config.onDismissed();
|
||||
}
|
||||
|
||||
double _childHeight;
|
||||
void _updateChildHeight(Size newSize) {
|
||||
setState(() {
|
||||
_childHeight = newSize.height;
|
||||
});
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new AlignTransition(
|
||||
performance: config.performance,
|
||||
alignment: new AnimatedValue<FractionalOffset>(const FractionalOffset(0.0, 0.0)),
|
||||
heightFactor: new AnimatedValue<double>(0.0, end: 1.0),
|
||||
child: new BottomSheet(
|
||||
performance: config.performance,
|
||||
onClosing: config.onClosing,
|
||||
childHeight: _childHeight,
|
||||
builder: (BuildContext context) => new SizeObserver(child: config.builder(context), onSizeChanged: _updateChildHeight)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,10 +21,10 @@ const Color _kSnackBackground = const Color(0xFF323232);
|
||||
|
||||
// TODO(ianh): Implement the Tablet version of snackbar if we're "on a tablet".
|
||||
|
||||
const Duration kSnackBarTransitionDuration = const Duration(milliseconds: 250);
|
||||
const Duration _kSnackBarTransitionDuration = const Duration(milliseconds: 250);
|
||||
const Duration kSnackBarShortDisplayDuration = const Duration(milliseconds: 1500);
|
||||
const Duration kSnackBarMediumDisplayDuration = const Duration(milliseconds: 2750);
|
||||
const Curve snackBarFadeCurve = const Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
|
||||
const Curve _snackBarFadeCurve = const Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
|
||||
|
||||
class SnackBarAction extends StatelessComponent {
|
||||
SnackBarAction({Key key, this.label, this.onPressed }) : super(key: key) {
|
||||
@ -91,7 +91,7 @@ class SnackBar extends StatelessComponent {
|
||||
style: new TextStyle(color: Theme.of(context).accentColor),
|
||||
child: new FadeTransition(
|
||||
performance: performance,
|
||||
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: snackBarFadeCurve),
|
||||
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: _snackBarFadeCurve),
|
||||
child: new Row(children)
|
||||
)
|
||||
)
|
||||
@ -105,7 +105,7 @@ class SnackBar extends StatelessComponent {
|
||||
|
||||
static Performance createPerformance() {
|
||||
return new Performance(
|
||||
duration: kSnackBarTransitionDuration,
|
||||
duration: _kSnackBarTransitionDuration,
|
||||
debugLabel: 'SnackBar'
|
||||
);
|
||||
}
|
||||
|
@ -332,6 +332,9 @@ enum _StateLifecycle {
|
||||
defunct,
|
||||
}
|
||||
|
||||
/// The signature of setState() methods.
|
||||
typedef void StateSetter(VoidCallback fn);
|
||||
|
||||
/// The logic and internal state for a StatefulComponent.
|
||||
abstract class State<T extends StatefulComponent> {
|
||||
/// The current configuration (an instance of the corresponding
|
||||
@ -377,7 +380,7 @@ abstract class State<T extends StatefulComponent> {
|
||||
/// If you just change the state directly without calling setState(), then the
|
||||
/// component will not be scheduled for rebuilding, meaning that its rendering
|
||||
/// will not be updated.
|
||||
void setState(void fn()) {
|
||||
void setState(VoidCallback fn) {
|
||||
assert(_debugLifecycleState != _StateLifecycle.defunct);
|
||||
fn();
|
||||
_element.markNeedsBuild();
|
||||
|
@ -133,6 +133,28 @@ class NavigatorState extends State<Navigator> {
|
||||
assert(_ephemeral.isEmpty);
|
||||
}
|
||||
|
||||
/// Pops the given route, if it's the current route. If it's not the current
|
||||
/// route, removes it from the list of active routes without notifying any
|
||||
/// observers or adjacent routes.
|
||||
///
|
||||
/// Do not use this for ModalRoutes, or indeed anything other than
|
||||
/// StateRoutes. Doing so would cause very odd results, e.g. ModalRoutes would
|
||||
/// get confused about who is current.
|
||||
void remove(Route route, [dynamic result]) {
|
||||
assert(_modal.contains(route));
|
||||
assert(route.overlayEntries.isEmpty);
|
||||
if (_modal.last == route) {
|
||||
pop(result);
|
||||
} else {
|
||||
setState(() {
|
||||
_modal.remove(route);
|
||||
route.didPop(result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the current route, notifying the observer (if any), and the
|
||||
/// previous routes (using [Route.didPopNext]).
|
||||
void pop([dynamic result]) {
|
||||
setState(() {
|
||||
// We use setState to guarantee that we'll rebuild, since the routes can't
|
||||
|
@ -26,7 +26,10 @@ void main() {
|
||||
tester.pump();
|
||||
expect(tester.findText('BottomSheet'), isNull);
|
||||
|
||||
showModalBottomSheet(context: context, child: new Text('BottomSheet')).then((_) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) => new Text('BottomSheet')
|
||||
).then((_) {
|
||||
showBottomSheetThenCalled = true;
|
||||
});
|
||||
|
||||
@ -42,7 +45,7 @@ void main() {
|
||||
expect(showBottomSheetThenCalled, isTrue);
|
||||
expect(tester.findText('BottomSheet'), isNull);
|
||||
|
||||
showModalBottomSheet(context: context, child: new Text('BottomSheet'));
|
||||
showModalBottomSheet(context: context, builder: (BuildContext context) => new Text('BottomSheet'));
|
||||
tester.pump(); // bottom sheet show animation starts
|
||||
tester.pump(new Duration(seconds: 1)); // animation done
|
||||
expect(tester.findText('BottomSheet'), isNotNull);
|
||||
@ -58,45 +61,60 @@ void main() {
|
||||
|
||||
test('Verify that a downwards fling dismisses a persistent BottomSheet', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
GlobalKey<PlaceholderState> _bottomSheetPlaceholderKey = new GlobalKey<PlaceholderState>();
|
||||
BuildContext context;
|
||||
GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
bool showBottomSheetThenCalled = false;
|
||||
|
||||
tester.pumpWidget(new MaterialApp(
|
||||
routes: <String, RouteBuilder>{
|
||||
'/': (RouteArguments args) {
|
||||
context = args.context;
|
||||
return new Scaffold(
|
||||
bottomSheet: new Placeholder(key: _bottomSheetPlaceholderKey),
|
||||
key: scaffoldKey,
|
||||
body: new Center(child: new Text('body'))
|
||||
);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
tester.pump();
|
||||
expect(showBottomSheetThenCalled, isFalse);
|
||||
expect(tester.findText('BottomSheet'), isNull);
|
||||
|
||||
showBottomSheet(
|
||||
context: context,
|
||||
child: new Container(child: new Text('BottomSheet'), margin: new EdgeDims.all(40.0)),
|
||||
placeholderKey: _bottomSheetPlaceholderKey
|
||||
).then((_) {
|
||||
scaffoldKey.currentState.showBottomSheet((BuildContext context) {
|
||||
return new Container(
|
||||
margin: new EdgeDims.all(40.0),
|
||||
child: new Text('BottomSheet')
|
||||
);
|
||||
}).closed.then((_) {
|
||||
showBottomSheetThenCalled = true;
|
||||
});
|
||||
|
||||
expect(_bottomSheetPlaceholderKey.currentState.child, isNotNull);
|
||||
expect(showBottomSheetThenCalled, isFalse);
|
||||
expect(tester.findText('BottomSheet'), isNull);
|
||||
|
||||
tester.pump(); // bottom sheet show animation starts
|
||||
|
||||
expect(showBottomSheetThenCalled, isFalse);
|
||||
expect(tester.findText('BottomSheet'), isNotNull);
|
||||
|
||||
tester.pump(new Duration(seconds: 1)); // animation done
|
||||
|
||||
expect(showBottomSheetThenCalled, isFalse);
|
||||
expect(tester.findText('BottomSheet'), isNotNull);
|
||||
|
||||
tester.fling(tester.findText('BottomSheet'), const Offset(0.0, 20.0), 1000.0);
|
||||
tester.pump(); // drain the microtask queue (Future completion callback)
|
||||
|
||||
expect(showBottomSheetThenCalled, isTrue);
|
||||
expect(tester.findText('BottomSheet'), isNotNull);
|
||||
|
||||
tester.pump(); // bottom sheet dismiss animation starts
|
||||
|
||||
expect(showBottomSheetThenCalled, isTrue);
|
||||
expect(tester.findText('BottomSheet'), isNotNull);
|
||||
|
||||
tester.pump(new Duration(seconds: 1)); // animation done
|
||||
tester.pump(new Duration(seconds: 1)); // rebuild frame without the bottom sheet
|
||||
|
||||
expect(showBottomSheetThenCalled, isTrue);
|
||||
expect(tester.findText('BottomSheet'), isNull);
|
||||
expect(_bottomSheetPlaceholderKey.currentState.child, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -162,7 +162,7 @@ class WidgetTester {
|
||||
assert(velocity != 0.0); // velocity is pixels/second
|
||||
final TestPointer p = new TestPointer(pointer);
|
||||
final HitTestResult result = _hitTest(startLocation);
|
||||
final kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
|
||||
const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
|
||||
final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity);
|
||||
double timeStamp = 0.0;
|
||||
_dispatchEvent(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
|
||||
|
Loading…
Reference in New Issue
Block a user