From 2eec30111a6f7801079353e535a43a346e6c5557 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Sun, 4 Oct 2015 14:22:35 -0700 Subject: [PATCH] Use Navigator to drive SnackBar Now SnackBar is an ephemeral route that uses a Placeholder to put itself into the Scaffold. Fixes #673 --- examples/fitness/lib/feed.dart | 41 +++------- examples/fitness/lib/main.dart | 1 - examples/fitness/lib/measurement.dart | 20 ++--- examples/stocks/lib/main.dart | 1 - examples/stocks/lib/stock_home.dart | 36 +++------ .../flutter/lib/src/widgets/framework.dart | 6 +- .../flutter/lib/src/widgets/navigator.dart | 4 - .../flutter/lib/src/widgets/placeholder.dart | 30 +++++++ .../flutter/lib/src/widgets/popup_menu.dart | 2 +- .../flutter/lib/src/widgets/snack_bar.dart | 80 +++++++++++-------- packages/flutter/lib/widgets.dart | 1 + 11 files changed, 112 insertions(+), 110 deletions(-) create mode 100644 packages/flutter/lib/src/widgets/placeholder.dart diff --git a/examples/fitness/lib/feed.dart b/examples/fitness/lib/feed.dart index b7555f92e6..eb78b29812 100644 --- a/examples/fitness/lib/feed.dart +++ b/examples/fitness/lib/feed.dart @@ -58,11 +58,9 @@ class FeedFragment extends StatefulComponent { } class FeedFragmentState extends State { + final GlobalKey _snackBarPlaceholderKey = new GlobalKey(); FitnessMode _fitnessMode = FitnessMode.feed; - PerformanceStatus _snackBarStatus = PerformanceStatus.dismissed; - bool _isShowingSnackBar = false; - void _handleFitnessModeChange(FitnessMode value) { setState(() { _fitnessMode = value; @@ -119,15 +117,17 @@ class FeedFragmentState extends State { ); } - FitnessItem _undoItem; - void _handleItemDismissed(FitnessItem item) { config.onItemDeleted(item); - setState(() { - _undoItem = item; - _isShowingSnackBar = true; - _snackBarStatus = PerformanceStatus.forward; - }); + showSnackBar( + navigator: config.navigator, + placeholderKey: _snackBarPlaceholderKey, + content: new Text("Item deleted."), + actions: [new SnackBarAction(label: "UNDO", onPressed: () { + config.onItemCreated(item); + config.navigator.pop(); + })] + ); } Widget buildChart() { @@ -198,25 +198,6 @@ class FeedFragmentState extends State { } } - void _handleUndo() { - config.onItemCreated(_undoItem); - setState(() { - _undoItem = null; - _isShowingSnackBar = false; - }); - } - - Widget buildSnackBar() { - if (_snackBarStatus == PerformanceStatus.dismissed) - return null; - return new SnackBar( - showing: _isShowingSnackBar, - content: new Text("Item deleted."), - actions: [new SnackBarAction(label: "UNDO", onPressed: _handleUndo)], - onDismissed: () { setState(() { _snackBarStatus = PerformanceStatus.dismissed; }); } - ); - } - void _handleActionButtonPressed() { showDialog(config.navigator, (NavigatorState navigator) => new AddItemDialog(navigator)).then((routeName) { if (routeName != null) @@ -240,7 +221,7 @@ class FeedFragmentState extends State { return new Scaffold( toolbar: buildToolBar(), body: buildBody(), - snackBar: buildSnackBar(), + snackBar: new Placeholder(key: _snackBarPlaceholderKey), floatingActionButton: buildFloatingActionButton() ); } diff --git a/examples/fitness/lib/main.dart b/examples/fitness/lib/main.dart index 98f87db579..ecc10820b5 100644 --- a/examples/fitness/lib/main.dart +++ b/examples/fitness/lib/main.dart @@ -5,7 +5,6 @@ library fitness; import 'package:playfair/playfair.dart' as playfair; -import 'package:sky/animation.dart'; import 'package:sky/material.dart'; import 'package:sky/painting.dart'; import 'package:sky/widgets.dart'; diff --git a/examples/fitness/lib/measurement.dart b/examples/fitness/lib/measurement.dart index 695d82a4c6..975d2766c1 100644 --- a/examples/fitness/lib/measurement.dart +++ b/examples/fitness/lib/measurement.dart @@ -112,9 +112,10 @@ class MeasurementFragment extends StatefulComponent { } class MeasurementFragmentState extends State { + final GlobalKey _snackBarPlaceholderKey = new GlobalKey(); + String _weight = ""; DateTime _when = new DateTime.now(); - String _errorMessage = null; void _handleSave() { double parsedWeight; @@ -122,9 +123,11 @@ class MeasurementFragmentState extends State { parsedWeight = double.parse(_weight); } on FormatException catch(e) { print("Exception $e"); - setState(() { - _errorMessage = "Save failed"; - }); + showSnackBar( + navigator: config.navigator, + placeholderKey: _snackBarPlaceholderKey, + content: new Text('Save failed') + ); } config.onCreated(new Measurement(when: _when, weight: parsedWeight)); config.navigator.pop(); @@ -195,18 +198,11 @@ class MeasurementFragmentState extends State { ); } - Widget buildSnackBar() { - if (_errorMessage == null) - return null; - // TODO(jackson): This doesn't show up, unclear why. - return new SnackBar(content: new Text(_errorMessage), showing: true); - } - Widget build(BuildContext context) { return new Scaffold( toolbar: buildToolBar(), body: buildBody(context), - snackBar: buildSnackBar() + snackBar: new Placeholder(key: _snackBarPlaceholderKey) ); } } diff --git a/examples/stocks/lib/main.dart b/examples/stocks/lib/main.dart index a931d547ea..61c8d62a4e 100644 --- a/examples/stocks/lib/main.dart +++ b/examples/stocks/lib/main.dart @@ -8,7 +8,6 @@ import 'dart:async'; import 'dart:math' as math; import 'dart:sky' as sky; -import 'package:sky/animation.dart'; import 'package:sky/gestures.dart'; import 'package:sky/material.dart'; import 'package:sky/painting.dart'; diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 091e379f67..f79388757a 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -6,8 +6,6 @@ part of stocks; typedef void ModeUpdater(StockMode mode); -const Duration _kSnackbarSlideDuration = const Duration(milliseconds: 200); - class StockHome extends StatefulComponent { StockHome(this.navigator, this.stocks, this.symbols, this.stockMode, this.modeUpdater); @@ -22,12 +20,10 @@ class StockHome extends StatefulComponent { class StockHomeState extends State { + final GlobalKey _snackBarPlaceholderKey = new GlobalKey(); bool _isSearching = false; String _searchQuery; - PerformanceStatus _snackBarStatus = PerformanceStatus.dismissed; - bool _isSnackBarShowing = false; - void _handleSearchBegin() { config.navigator.pushState(this, (_) { setState(() { @@ -217,28 +213,18 @@ class StockHomeState extends State { } void _handleUndo() { - setState(() { - _isSnackBarShowing = false; - }); - } - - GlobalKey snackBarKey = new GlobalKey(label: 'snackbar'); - Widget buildSnackBar() { - if (_snackBarStatus == PerformanceStatus.dismissed) - return null; - return new SnackBar( - showing: _isSnackBarShowing, - content: new Text("Stock purchased!"), - actions: [new SnackBarAction(label: "UNDO", onPressed: _handleUndo)], - onDismissed: () { setState(() { _snackBarStatus = PerformanceStatus.dismissed; }); } - ); + config.navigator.pop(); } void _handleStockPurchased() { - setState(() { - _isSnackBarShowing = true; - _snackBarStatus = PerformanceStatus.forward; - }); + showSnackBar( + navigator: config.navigator, + placeholderKey: _snackBarPlaceholderKey, + content: new Text("Stock purchased!"), + actions: [ + new SnackBarAction(label: "UNDO", onPressed: _handleUndo) + ] + ); } Widget buildFloatingActionButton() { @@ -253,7 +239,7 @@ class StockHomeState extends State { return new Scaffold( toolbar: _isSearching ? buildSearchBar() : buildToolBar(), body: buildTabNavigator(), - snackBar: buildSnackBar(), + snackBar: new Placeholder(key: _snackBarPlaceholderKey), floatingActionButton: buildFloatingActionButton() ); } diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 05ce1e691c..47478d2662 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -50,7 +50,7 @@ typedef void GlobalKeyRemoveListener(GlobalKey key); /// A GlobalKey is one that must be unique across the entire application. It is /// used by components that need to communicate with other components across the /// application's element tree. -abstract class GlobalKey extends Key { +abstract class GlobalKey extends Key { const GlobalKey.constructor() : super.constructor(); // so that subclasses can call us, since the Key() factory constructor shadows the implicit constructor /// Constructs a LabeledGlobalKey, which is a GlobalKey with a label used for debugging. @@ -96,9 +96,9 @@ abstract class GlobalKey extends Key { Element get _currentElement => _registry[this]; BuildContext get currentContext => _currentElement; Widget get currentWidget => _currentElement?.widget; - State get currentState { + T get currentState { Element element = _currentElement; - if (element is StatefulComponentElement) + if (element is StatefulComponentElement) return element.state; return null; } diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index d69d75a3a6..fa0bb19fc4 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -107,10 +107,6 @@ class NavigatorState extends State { void pop([dynamic result]) { setState(() { - while (currentRoute.ephemeral) { - currentRoute.didPop(null); - _currentPosition -= 1; - } assert(_currentPosition > 0); currentRoute.didPop(result); _currentPosition -= 1; diff --git a/packages/flutter/lib/src/widgets/placeholder.dart b/packages/flutter/lib/src/widgets/placeholder.dart new file mode 100644 index 0000000000..69c79184f7 --- /dev/null +++ b/packages/flutter/lib/src/widgets/placeholder.dart @@ -0,0 +1,30 @@ +// 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 'package:sky/src/widgets/basic.dart'; +import 'package:sky/src/widgets/framework.dart'; + +class Placeholder extends StatefulComponent { + Placeholder({ Key key }) : super(key: key); + + PlaceholderState createState() => new PlaceholderState(); +} + +class PlaceholderState extends State { + Widget get child => _child; + Widget _child; + void set child(Widget child) { + if (_child == child) + return; + setState(() { + _child = child; + }); + } + + Widget build(BuildContext context) { + if (_child != null) + return child; + return new SizedBox(width: 0.0, height: 0.0); + } +} diff --git a/packages/flutter/lib/src/widgets/popup_menu.dart b/packages/flutter/lib/src/widgets/popup_menu.dart index c6af624244..85295fd7c4 100644 --- a/packages/flutter/lib/src/widgets/popup_menu.dart +++ b/packages/flutter/lib/src/widgets/popup_menu.dart @@ -137,7 +137,7 @@ class MenuRoute extends Route { return result; } - bool get ephemeral => false; // we could make this true, but then we'd have to use popRoute(), not pop(), in menus + bool get ephemeral => true; bool get modal => true; bool get opaque => false; Duration get transitionDuration => _kMenuDuration; diff --git a/packages/flutter/lib/src/widgets/snack_bar.dart b/packages/flutter/lib/src/widgets/snack_bar.dart index 757d9a683a..9e72a0d8fb 100644 --- a/packages/flutter/lib/src/widgets/snack_bar.dart +++ b/packages/flutter/lib/src/widgets/snack_bar.dart @@ -6,21 +6,20 @@ import 'package:sky/animation.dart'; import 'package:sky/gestures.dart'; import 'package:sky/material.dart'; import 'package:sky/painting.dart'; -import 'package:sky/src/widgets/animated_component.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/framework.dart'; import 'package:sky/src/widgets/gesture_detector.dart'; import 'package:sky/src/widgets/material.dart'; +import 'package:sky/src/widgets/navigator.dart'; +import 'package:sky/src/widgets/placeholder.dart'; import 'package:sky/src/widgets/theme.dart'; import 'package:sky/src/widgets/transitions.dart'; -typedef void SnackBarDismissedCallback(); - const Duration _kSlideInDuration = const Duration(milliseconds: 200); -const double kSnackHeight = 52.0; -const double kSideMargins = 24.0; -const double kVerticalPadding = 14.0; -const Color kSnackBackground = const Color(0xFF323232); +const double _kSnackHeight = 52.0; +const double _kSideMargins = 24.0; +const double _kVerticalPadding = 14.0; +const Color _kSnackBackground = const Color(0xFF323232); class SnackBarAction extends StatelessComponent { SnackBarAction({Key key, this.label, this.onPressed }) : super(key: key) { @@ -34,70 +33,60 @@ class SnackBarAction extends StatelessComponent { return new GestureDetector( onTap: onPressed, child: new Container( - margin: const EdgeDims.only(left: kSideMargins), - padding: const EdgeDims.symmetric(vertical: kVerticalPadding), + margin: const EdgeDims.only(left: _kSideMargins), + padding: const EdgeDims.symmetric(vertical: _kVerticalPadding), child: new Text(label) ) ); } } -class SnackBar extends AnimatedComponent { +class SnackBar extends StatelessComponent { SnackBar({ Key key, this.content, this.actions, - bool showing, - this.onDismissed - }) : super(key: key, direction: showing ? AnimationDirection.forward : AnimationDirection.reverse, duration: _kSlideInDuration) { + this.performance + }) : super(key: key) { assert(content != null); } final Widget content; final List actions; - final SnackBarDismissedCallback onDismissed; - - SnackBarState createState() => new SnackBarState(); -} - -class SnackBarState extends AnimatedState { - void handleDismissed() { - if (config.onDismissed != null) - config.onDismissed(); - } + final PerformanceView performance; Widget build(BuildContext context) { List children = [ new Flexible( child: new Container( - margin: const EdgeDims.symmetric(vertical: kVerticalPadding), + margin: const EdgeDims.symmetric(vertical: _kVerticalPadding), child: new DefaultTextStyle( style: Typography.white.subhead, - child: config.content + child: content ) ) ) ]; - if (config.actions != null) - children.addAll(config.actions); + if (actions != null) + children.addAll(actions); return new SquashTransition( - performance: performance.view, + performance: performance, height: new AnimatedValue( 0.0, - end: kSnackHeight, + end: _kSnackHeight, curve: easeIn, reverseCurve: easeOut ), child: new ClipRect( child: new OverflowBox( - minHeight: kSnackHeight, - maxHeight: kSnackHeight, + minHeight: _kSnackHeight, + maxHeight: _kSnackHeight, child: new Material( level: 2, - color: kSnackBackground, + color: _kSnackBackground, type: MaterialType.canvas, child: new Container( - margin: const EdgeDims.symmetric(horizontal: kSideMargins), + margin: const EdgeDims.symmetric(horizontal: _kSideMargins), child: new DefaultTextStyle( style: new TextStyle(color: Theme.of(context).accentColor), child: new Row(children) @@ -109,3 +98,28 @@ class SnackBarState extends AnimatedState { ); } } + +class _SnackBarRoute extends Route { + _SnackBarRoute({ this.content, this.actions }); + + final Widget content; + final List actions; + + bool get hasContent => false; + bool get ephemeral => true; + bool get modal => false; + Duration get transitionDuration => _kSlideInDuration; + + Widget build(NavigatorState navigator, PerformanceView nextRoutePerformance) => null; +} + +void showSnackBar({ NavigatorState navigator, GlobalKey placeholderKey, Widget content, List actions }) { + Route route = new _SnackBarRoute(); + SnackBar snackBar = new SnackBar( + content: content, + actions: actions, + performance: route.performance + ); + placeholderKey.currentState.child = snackBar; + navigator.push(route); +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index df63f53c19..d43db17293 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -37,6 +37,7 @@ export 'src/widgets/material_button.dart'; export 'src/widgets/mimic.dart'; export 'src/widgets/mixed_viewport.dart'; export 'src/widgets/navigator.dart'; +export 'src/widgets/placeholder.dart'; export 'src/widgets/popup_menu.dart'; export 'src/widgets/popup_menu_item.dart'; export 'src/widgets/progress_indicator.dart';