Convert Drawer to using navigator This patch converts drawer to using the "openDialog" pattern for managing its state. Currently, the drawer entrance and exit animation aren't integrated with the navigator's animation system because the drawer's animations can be stopped and reversed, which the navigator can't yet understand. That means dismissing the drawer via the system back button causes the drawer to be removed instanteously. Fixes #715 Fixes #1187
diff --git a/examples/fitness/lib/feed.dart b/examples/fitness/lib/feed.dart index a3c0275..221b705 100644 --- a/examples/fitness/lib/feed.dart +++ b/examples/fitness/lib/feed.dart
@@ -66,19 +66,14 @@ void _handleFitnessModeChange(FitnessMode value) { setState(() { _fitnessMode = value; - _drawerShowing = false; }); + config.navigator.pop(); } - Drawer buildDrawer() { - if (_drawerStatus == AnimationStatus.dismissed) - return null; - return new Drawer( - showing: _drawerShowing, - level: 3, - onDismissed: _handleDrawerDismissed, + void _showDrawer() { + showDrawer( navigator: config.navigator, - children: [ + child: new Block([ new DrawerHeader(child: new Text('Fitness')), new DrawerItem( icon: 'action/view_list', @@ -98,26 +93,10 @@ new DrawerItem( icon: 'action/help', child: new Text('Help & Feedback')) - ] + ]) ); } - bool _drawerShowing = false; - AnimationStatus _drawerStatus = AnimationStatus.dismissed; - - void _handleOpenDrawer() { - setState(() { - _drawerShowing = true; - _drawerStatus = AnimationStatus.forward; - }); - } - - void _handleDrawerDismissed() { - setState(() { - _drawerStatus = AnimationStatus.dismissed; - }); - } - void _handleShowSettings() { config.navigator.pop(); config.navigator.pushNamed('/settings'); @@ -135,7 +114,7 @@ return new ToolBar( left: new IconButton( icon: "navigation/menu", - onPressed: _handleOpenDrawer), + onPressed: _showDrawer), center: new Text(fitnessModeTitle) ); } @@ -262,8 +241,7 @@ toolbar: buildToolBar(), body: buildBody(), snackBar: buildSnackBar(), - floatingActionButton: buildFloatingActionButton(), - drawer: buildDrawer() + floatingActionButton: buildFloatingActionButton() ); } }
diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 523b668..80e399c 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart
@@ -56,22 +56,6 @@ }); } - bool _drawerShowing = false; - AnimationStatus _drawerStatus = AnimationStatus.dismissed; - - void _handleOpenDrawer() { - setState(() { - _drawerShowing = true; - _drawerStatus = AnimationStatus.forward; - }); - } - - void _handleDrawerDismissed() { - setState(() { - _drawerStatus = AnimationStatus.dismissed; - }); - } - bool _autorefresh = false; void _handleAutorefreshChanged(bool value) { setState(() { @@ -91,16 +75,10 @@ ); } - Drawer buildDrawer() { - if (_drawerStatus == AnimationStatus.dismissed) - return null; - assert(_drawerShowing); // TODO(mpcomplete): this is always true - return new Drawer( - level: 3, - showing: _drawerShowing, - onDismissed: _handleDrawerDismissed, + void _showDrawer() { + showDrawer( navigator: config.navigator, - children: [ + child: new Block([ new DrawerHeader(child: new Text('Stocks')), new DrawerItem( icon: 'action/assessment', @@ -141,7 +119,7 @@ new DrawerItem( icon: 'action/help', child: new Text('Help & Feedback')) - ] + ]) ); } @@ -154,7 +132,7 @@ return new ToolBar( left: new IconButton( icon: "navigation/menu", - onPressed: _handleOpenDrawer + onPressed: _showDrawer ), center: new Text('Stocks'), right: [ @@ -276,8 +254,7 @@ toolbar: _isSearching ? buildSearchBar() : buildToolBar(), body: buildTabNavigator(), snackBar: buildSnackBar(), - floatingActionButton: buildFloatingActionButton(), - drawer: buildDrawer() + floatingActionButton: buildFloatingActionButton() ); } }
diff --git a/examples/widgets/card_collection.dart b/examples/widgets/card_collection.dart index ba5a53c..0e7a06c 100644 --- a/examples/widgets/card_collection.dart +++ b/examples/widgets/card_collection.dart
@@ -4,7 +4,6 @@ import 'dart:sky' as sky; -import 'package:sky/animation.dart'; import 'package:sky/material.dart'; import 'package:sky/painting.dart'; import 'package:sky/widgets.dart'; @@ -18,11 +17,15 @@ Key get key => new ObjectKey(this); } -class CardCollectionApp extends StatefulComponent { - CardCollectionAppState createState() => new CardCollectionAppState(); +class CardCollection extends StatefulComponent { + CardCollection({ this.navigator }); + + final NavigatorState navigator; + + CardCollectionState createState() => new CardCollectionState(); } -class CardCollectionAppState extends State<CardCollectionApp> { +class CardCollectionState extends State<CardCollection> { static const TextStyle cardLabelStyle = const TextStyle(color: Colors.white, fontSize: 18.0, fontWeight: bold); @@ -37,9 +40,7 @@ DismissDirection _dismissDirection = DismissDirection.horizontal; bool _snapToCenter = false; bool _fixedSizeCards = false; - bool _drawerShowing = false; bool _sunshine = false; - AnimationStatus _drawerStatus = AnimationStatus.dismissed; InvalidatorCallback _invalidator; Size _cardCollectionSize = new Size(200.0, 200.0); @@ -114,17 +115,23 @@ } } - void _handleOpenDrawer() { - setState(() { - _drawerShowing = true; - _drawerStatus = AnimationStatus.forward; - }); - } - - void _handleDrawerDismissed() { - setState(() { - _drawerStatus = AnimationStatus.dismissed; - }); + void _showDrawer() { + showDrawer( + navigator: config.navigator, + child: new IconTheme( + data: const IconThemeData(color: IconThemeColor.black), + child: new Block([ + new DrawerHeader(child: new Text('Options')), + buildDrawerCheckbox("Snap fling scrolls to center", _snapToCenter, _toggleSnapToCenter), + buildDrawerCheckbox("Fixed size cards", _fixedSizeCards, _toggleFixedSizeCards), + buildDrawerCheckbox("Let the sun shine", _sunshine, _toggleSunshine), + new DrawerDivider(), + buildDrawerRadioItem(DismissDirection.horizontal, 'action/code'), + buildDrawerRadioItem(DismissDirection.left, 'navigation/arrow_back'), + buildDrawerRadioItem(DismissDirection.right, 'navigation/arrow_forward'), + ]) + ) + ); } String _dismissDirectionText(DismissDirection direction) { @@ -151,65 +158,41 @@ }); } - _changeDismissDirection(DismissDirection newDismissDirection) { + void _changeDismissDirection(DismissDirection newDismissDirection) { setState(() { _dismissDirection = newDismissDirection; - _drawerStatus = AnimationStatus.dismissed; }); + config.navigator.pop(); } - Widget buildDrawer() { - if (_drawerStatus == AnimationStatus.dismissed) - return null; + Widget buildDrawerCheckbox(String label, bool value, Function callback) { + return new DrawerItem( + onPressed: callback, + child: new Row([ + new Flexible(child: new Text(label)), + new Checkbox(value: value, onChanged: (_) { callback(); }) + ]) + ); + } - Widget buildDrawerCheckbox(String label, bool value, Function callback) { - return new DrawerItem( - onPressed: callback, - child: new Row([ - new Flexible(child: new Text(label)), - new Checkbox(value: value, onChanged: (_) { callback(); }) - ]) - ); - } - - Widget buildDrawerRadioItem(DismissDirection direction, String icon) { - return new DrawerItem( - icon: icon, - onPressed: () { _changeDismissDirection(direction); }, - child: new Row([ - new Flexible(child: new Text(_dismissDirectionText(direction))), - new Radio( - value: direction, - onChanged: _changeDismissDirection, - groupValue: _dismissDirection - ) - ]) - ); - } - - return new IconTheme( - data: const IconThemeData(color: IconThemeColor.black), - child: new Drawer( - level: 3, - showing: _drawerShowing, - onDismissed: _handleDrawerDismissed, - children: [ - new DrawerHeader(child: new Text('Options')), - buildDrawerCheckbox("Snap fling scrolls to center", _snapToCenter, _toggleSnapToCenter), - buildDrawerCheckbox("Fixed size cards", _fixedSizeCards, _toggleFixedSizeCards), - buildDrawerCheckbox("Let the sun shine", _sunshine, _toggleSunshine), - new DrawerDivider(), - buildDrawerRadioItem(DismissDirection.horizontal, 'action/code'), - buildDrawerRadioItem(DismissDirection.left, 'navigation/arrow_back'), - buildDrawerRadioItem(DismissDirection.right, 'navigation/arrow_forward'), - ] - ) + Widget buildDrawerRadioItem(DismissDirection direction, String icon) { + return new DrawerItem( + icon: icon, + onPressed: () { _changeDismissDirection(direction); }, + child: new Row([ + new Flexible(child: new Text(_dismissDirectionText(direction))), + new Radio( + value: direction, + onChanged: _changeDismissDirection, + groupValue: _dismissDirection + ) + ]) ); } Widget buildToolBar() { return new ToolBar( - left: new IconButton(icon: "navigation/menu", onPressed: _handleOpenDrawer), + left: new IconButton(icon: "navigation/menu", onPressed: _showDrawer), center: new Text('Swipe Away'), right: [ new Text(_dismissDirectionText(_dismissDirection)) @@ -358,24 +341,23 @@ body = new Stack([body, indicator]); } - return new Theme( - data: new ThemeData( - brightness: ThemeBrightness.light, - primarySwatch: Colors.blue, - accentColor: Colors.redAccent[200] - ), - child: new Title( - title: 'Cards', - child: new Scaffold( - toolbar: buildToolBar(), - drawer: buildDrawer(), - body: body - ) - ) + return new Scaffold( + toolbar: buildToolBar(), + body: body ); } } void main() { - runApp(new CardCollectionApp()); + runApp(new App( + title: 'Cards', + theme: new ThemeData( + brightness: ThemeBrightness.light, + primarySwatch: Colors.blue, + accentColor: Colors.redAccent[200] + ), + routes: { + '/': (NavigatorState navigator, Route route) => new CardCollection(navigator: navigator), + } + )); }
diff --git a/examples/widgets/pageable_list.dart b/examples/widgets/pageable_list.dart index e9c079c..c00da4b 100644 --- a/examples/widgets/pageable_list.dart +++ b/examples/widgets/pageable_list.dart
@@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:sky/animation.dart'; import 'package:sky/material.dart'; import 'package:sky/painting.dart'; import 'package:sky/widgets.dart'; @@ -17,6 +16,10 @@ } class PageableListApp extends StatefulComponent { + PageableListApp({ this.navigator }); + + final NavigatorState navigator; + PageableListAppState createState() => new PageableListAppState(); } @@ -85,31 +88,10 @@ }); } - bool _drawerShowing = false; - AnimationStatus _drawerStatus = AnimationStatus.dismissed; - - void _handleOpenDrawer() { - setState(() { - _drawerShowing = true; - _drawerStatus = AnimationStatus.forward; - }); - } - - void _handleDrawerDismissed() { - setState(() { - _drawerStatus = AnimationStatus.dismissed; - }); - } - - Drawer buildDrawer() { - if (_drawerStatus == AnimationStatus.dismissed) - return null; - - return new Drawer( - level: 3, - showing: _drawerShowing, - onDismissed: _handleDrawerDismissed, - children: [ + void _showDrawer() { + showDrawer( + navigator: config.navigator, + child: new Block([ new DrawerHeader(child: new Text('Options')), new DrawerItem( icon: 'navigation/more_horiz', @@ -130,14 +112,13 @@ new Checkbox(value: itemsWrap) ]) ) - ] + ]) ); - } Widget buildToolBar() { return new ToolBar( - left: new IconButton(icon: "navigation/menu", onPressed: _handleOpenDrawer), + left: new IconButton(icon: "navigation/menu", onPressed: _showDrawer), center: new Text('PageableList'), right: [ new Text(scrollDirection == ScrollDirection.horizontal ? "horizontal" : "vertical") @@ -167,25 +148,24 @@ Widget build(BuildContext context) { return new IconTheme( data: const IconThemeData(color: IconThemeColor.white), - child: new Theme( - data: new ThemeData( - brightness: ThemeBrightness.light, - primarySwatch: Colors.blue, - accentColor: Colors.redAccent[200] - ), - child: new Title( - title: 'PageableList', - child: new Scaffold( - drawer: buildDrawer(), - toolbar: buildToolBar(), - body: buildBody(context) - ) - ) + child: new Scaffold( + toolbar: buildToolBar(), + body: buildBody(context) ) ); } } void main() { - runApp(new PageableListApp()); + runApp(new App( + title: 'PageableList', + theme: new ThemeData( + brightness: ThemeBrightness.light, + primarySwatch: Colors.blue, + accentColor: Colors.redAccent[200] + ), + routes: { + '/': (NavigatorState navigator, Route route) => new PageableListApp(navigator: navigator), + } + )); }
diff --git a/packages/flutter/lib/src/animation/animation_performance.dart b/packages/flutter/lib/src/animation/animation_performance.dart index 8111754..6eb2ee8 100644 --- a/packages/flutter/lib/src/animation/animation_performance.dart +++ b/packages/flutter/lib/src/animation/animation_performance.dart
@@ -79,9 +79,6 @@ /// If non-null, animate with this timing instead of a linear timing AnimationTiming timing; - /// If non-null, animate with this force instead of a zero-to-one timeline. - Force attachedForce; - /// The progress of this performance along the timeline /// /// Note: Setting this value stops the current animation. @@ -136,12 +133,6 @@ /// Start running this animation in the most recently direction Future resume() { - if (attachedForce != null) { - return fling( - velocity: _direction == Direction.forward ? 1.0 : -1.0, - force: attachedForce - ); - } return _animateTo(_direction == Direction.forward ? 1.0 : 0.0); }
diff --git a/packages/flutter/lib/src/widgets/drawer.dart b/packages/flutter/lib/src/widgets/drawer.dart index edee253..5d980b3 100644 --- a/packages/flutter/lib/src/widgets/drawer.dart +++ b/packages/flutter/lib/src/widgets/drawer.dart
@@ -14,6 +14,7 @@ import 'package:sky/src/widgets/scrollable.dart'; import 'package:sky/src/widgets/theme.dart'; import 'package:sky/src/widgets/transitions.dart'; +import 'package:sky/src/widgets/focus.dart'; // TODO(eseidel): Draw width should vary based on device size: // http://www.google.com/design/spec/layout/structure.html#structure-side-nav @@ -36,22 +37,16 @@ const Point _kOpenPosition = Point.origin; const Point _kClosedPosition = const Point(-_kWidth, 0.0); -typedef void DrawerDismissedCallback(); - class Drawer extends StatefulComponent { Drawer({ Key key, - this.children, - this.showing: false, - this.level: 0, - this.onDismissed, + this.child, + this.level: 3, this.navigator }) : super(key: key); - final List<Widget> children; - final bool showing; + final Widget child; final int level; - final DrawerDismissedCallback onDismissed; final NavigatorState navigator; DrawerState createState() => new DrawerState(); @@ -60,44 +55,24 @@ class DrawerState extends State<Drawer> { void initState() { super.initState(); - _performance = new AnimationPerformance(duration: _kBaseSettleDuration); - _performance.addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.dismissed) - _handleDismissed(); - }); - // Use a spring force for animating the drawer. We can't use curves for - // this because we need a linear curve in order to track the user's finger - // while dragging. - _performance.attachedForce = kDefaultSpringForce; - if (config.navigator != null) { - // TODO(ianh): This is crazy. We should convert drawer to use a pattern like openDialog(). - // https://github.com/domokit/sky_engine/pull/1186 - scheduleMicrotask(() { - config.navigator.pushState(this, (_) => _performance.reverse()); + _performance = new AnimationPerformance(duration: _kBaseSettleDuration) + ..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.dismissed) + config.navigator.pop(); }); - } - _performance.play(_direction); + _open(); } AnimationPerformance _performance; - Direction get _direction => config.showing ? Direction.forward : Direction.reverse; - - void didUpdateConfig(Drawer oldConfig) { - if (config.showing != oldConfig.showing) - _performance.play(_direction); - } - Widget build(BuildContext context) { - var mask = new GestureDetector( + Widget mask = new GestureDetector( + onTap: _close, child: new ColorTransition( performance: _performance.view, - color: new AnimatedColorValue(Colors.transparent, end: const Color(0x7F000000)), + color: new AnimatedColorValue(Colors.transparent, end: Colors.black54), child: new Container() - ), - onTap: () { - _performance.reverse(); - } + ) ); Widget content = new SlideTransition( @@ -105,12 +80,12 @@ position: new AnimatedValue<Point>(_kClosedPosition, end: _kOpenPosition), child: new AnimatedContainer( curve: ease, - duration: const Duration(milliseconds: 200), + duration: _kThemeChangeDuration, decoration: new BoxDecoration( backgroundColor: Theme.of(context).canvasColor, boxShadow: shadows[config.level]), width: _kWidth, - child: new Block(config.children) + child: config.child ) ); @@ -118,33 +93,64 @@ onHorizontalDragStart: _performance.stop, onHorizontalDragUpdate: _handleDragUpdate, onHorizontalDragEnd: _handleDragEnd, - child: new Stack([ mask, content ]) + child: new Stack([ + mask, + new Positioned( + top: 0.0, + left: 0.0, + bottom: 0.0, + child: content + ) + ]) ); } - void _handleDismissed() { - if (config.navigator != null && - config.navigator.currentRoute is StateRoute && - (config.navigator.currentRoute as StateRoute).owner == this) // TODO(ianh): remove cast once analyzer is cleverer - config.navigator.pop(); - if (config.onDismissed != null) - config.onDismissed(); - } - bool get _isMostlyClosed => _performance.progress < 0.5; - void _settle() { _isMostlyClosed ? _performance.reverse() : _performance.play(); } - void _handleDragUpdate(double delta) { _performance.progress += delta / _kWidth; } + void _open() { + _performance.fling(velocity: 1.0); + } + + void _close() { + _performance.fling(velocity: -1.0); + } + void _handleDragEnd(Offset velocity) { if (velocity.dx.abs() >= _kMinFlingVelocity) { _performance.fling(velocity: velocity.dx * _kFlingVelocityScale); + } else if (_isMostlyClosed) { + _close(); } else { - _settle(); + _open(); } } +} +class DrawerRoute extends Route { + DrawerRoute({ this.child, this.level }); + + final Widget child; + final int level; + + bool get opaque => false; + + Widget build(NavigatorState navigator, WatchableAnimationPerformance nextRoutePerformance) { + return new Focus( + key: new GlobalObjectKey(this), + autofocus: true, + child: new Drawer( + child: child, + level: level, + navigator: navigator + ) + ); + } +} + +void showDrawer({ NavigatorState navigator, Widget child, int level: 3 }) { + navigator.push(new DrawerRoute(child: child, level: level)); }