Commit 1d195cb9 authored by Adam Barth's avatar Adam Barth

Fix a large number of Drawer bugs

This patch restructures how we handle drawer. The drawer is now a child of the
Scaffold, which wraps the Drawer in a DrawerController. The DrawerController
manages the interaction with the navigator as well as the edge swiping. The
DrawerController's state machine is driven almost entirely off its Performance,
which it now owns completely.

Fixes #90
Fixes #187
Fixes #192
Fixes #194
Fixes #604
parent 17533e1d
...@@ -55,6 +55,7 @@ class FeedFragment extends StatefulComponent { ...@@ -55,6 +55,7 @@ class FeedFragment extends StatefulComponent {
class FeedFragmentState extends State<FeedFragment> { class FeedFragmentState extends State<FeedFragment> {
FitnessMode _fitnessMode = FitnessMode.feed; FitnessMode _fitnessMode = FitnessMode.feed;
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
void _handleFitnessModeChange(FitnessMode value) { void _handleFitnessModeChange(FitnessMode value) {
setState(() { setState(() {
...@@ -63,9 +64,8 @@ class FeedFragmentState extends State<FeedFragment> { ...@@ -63,9 +64,8 @@ class FeedFragmentState extends State<FeedFragment> {
Navigator.pop(context); Navigator.pop(context);
} }
void _showDrawer() { Widget _buildDrawer() {
showDrawer( return new Drawer(
context: context,
child: new Block(<Widget>[ child: new Block(<Widget>[
new DrawerHeader(child: new Text('Fitness')), new DrawerHeader(child: new Text('Fitness')),
new DrawerItem( new DrawerItem(
...@@ -106,7 +106,7 @@ class FeedFragmentState extends State<FeedFragment> { ...@@ -106,7 +106,7 @@ class FeedFragmentState extends State<FeedFragment> {
return new ToolBar( return new ToolBar(
left: new IconButton( left: new IconButton(
icon: "navigation/menu", icon: "navigation/menu",
onPressed: _showDrawer), onPressed: () => _scaffoldKey.currentState?.openDrawer()),
center: new Text(fitnessModeTitle) center: new Text(fitnessModeTitle)
); );
} }
...@@ -207,9 +207,11 @@ class FeedFragmentState extends State<FeedFragment> { ...@@ -207,9 +207,11 @@ class FeedFragmentState extends State<FeedFragment> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
key: _scaffoldKey,
toolBar: buildToolBar(), toolBar: buildToolBar(),
body: buildBody(), body: buildBody(),
floatingActionButton: buildFloatingActionButton() floatingActionButton: buildFloatingActionButton(),
drawer: _buildDrawer()
); );
} }
} }
......
...@@ -6,19 +6,25 @@ import 'package:flutter/material.dart'; ...@@ -6,19 +6,25 @@ import 'package:flutter/material.dart';
import 'demo/widget_demo.dart'; import 'demo/widget_demo.dart';
class GalleryPage extends StatelessComponent { class GalleryPage extends StatefulComponent {
GalleryPage({ this.demos, this.active, this.onThemeChanged }); GalleryPage({ this.demos, this.active, this.onThemeChanged });
final List<WidgetDemo> demos; final List<WidgetDemo> demos;
final WidgetDemo active; final WidgetDemo active;
final ValueChanged<ThemeData> onThemeChanged; final ValueChanged<ThemeData> onThemeChanged;
void _showDrawer(BuildContext context) { _GalleryPageState createState() => new _GalleryPageState();
}
class _GalleryPageState extends State<GalleryPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildDrawer() {
List<Widget> items = <Widget>[ List<Widget> items = <Widget>[
new DrawerHeader(child: new Text('Material demos')), new DrawerHeader(child: new Text('Material demos')),
]; ];
for (WidgetDemo demo in demos) { for (WidgetDemo demo in config.demos) {
items.add(new DrawerItem( items.add(new DrawerItem(
onPressed: () { onPressed: () {
Navigator.pushNamed(context, demo.routeName); Navigator.pushNamed(context, demo.routeName);
...@@ -27,12 +33,12 @@ class GalleryPage extends StatelessComponent { ...@@ -27,12 +33,12 @@ class GalleryPage extends StatelessComponent {
)); ));
} }
showDrawer(context: context, child: new Block(items)); return new Drawer(child: new Block(items));
} }
Widget _body(BuildContext context) { Widget _buildBody() {
if (active != null) if (config.active != null)
return active.builder(context); return config.active.builder(context);
return new Material( return new Material(
child: new Center( child: new Center(
child: new Text('Select a demo from the drawer') child: new Text('Select a demo from the drawer')
...@@ -40,22 +46,24 @@ class GalleryPage extends StatelessComponent { ...@@ -40,22 +46,24 @@ class GalleryPage extends StatelessComponent {
); );
} }
Widget _tabBar(BuildContext context) { Widget _buildTabBar() {
final WidgetBuilder builder = active?.tabBarBuilder; final WidgetBuilder builder = config.active?.tabBarBuilder;
return builder != null ? builder(context) : null; return builder != null ? builder(context) : null;
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
key: _scaffoldKey,
toolBar: new ToolBar( toolBar: new ToolBar(
left: new IconButton( left: new IconButton(
icon: 'navigation/menu', icon: 'navigation/menu',
onPressed: () { _showDrawer(context); } onPressed: () { _scaffoldKey.currentState?.openDrawer(); }
), ),
center: new Text(active?.title ?? 'Material gallery'), center: new Text(config.active?.title ?? 'Material gallery'),
tabBar: _tabBar(context) tabBar: _buildTabBar()
), ),
body: _body(context) drawer: _buildDrawer(),
body: _buildBody()
); );
} }
} }
...@@ -21,7 +21,7 @@ class StockHome extends StatefulComponent { ...@@ -21,7 +21,7 @@ class StockHome extends StatefulComponent {
class StockHomeState extends State<StockHome> { class StockHomeState extends State<StockHome> {
final GlobalKey scaffoldKey = new GlobalKey(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final TabBarSelection _tabBarSelection = new TabBarSelection(); final TabBarSelection _tabBarSelection = new TabBarSelection();
bool _isSearching = false; bool _isSearching = false;
String _searchQuery; String _searchQuery;
...@@ -70,9 +70,8 @@ class StockHomeState extends State<StockHome> { ...@@ -70,9 +70,8 @@ class StockHomeState extends State<StockHome> {
); );
} }
void _showDrawer() { Widget _buildDrawer(BuildContext context) {
showDrawer( return new Drawer(
context: context,
child: new Block(<Widget>[ child: new Block(<Widget>[
new DrawerHeader(child: new Text('Stocks')), new DrawerHeader(child: new Text('Stocks')),
new DrawerItem( new DrawerItem(
...@@ -151,7 +150,7 @@ class StockHomeState extends State<StockHome> { ...@@ -151,7 +150,7 @@ class StockHomeState extends State<StockHome> {
elevation: 0, elevation: 0,
left: new IconButton( left: new IconButton(
icon: "navigation/menu", icon: "navigation/menu",
onPressed: _showDrawer onPressed: () => _scaffoldKey.currentState?.openDrawer()
), ),
center: new FadeTransition( center: new FadeTransition(
opacity: new AnimatedValue<double>( opacity: new AnimatedValue<double>(
...@@ -200,7 +199,7 @@ class StockHomeState extends State<StockHome> { ...@@ -200,7 +199,7 @@ class StockHomeState extends State<StockHome> {
stock.percentChange = 100.0 * (1.0 / stock.lastSale); stock.percentChange = 100.0 * (1.0 / stock.lastSale);
stock.lastSale += 1.0; stock.lastSale += 1.0;
}); });
scaffoldKey.currentState.showSnackBar(new SnackBar( _scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text("Purchased ${stock.symbol} for ${stock.lastSale}"), content: new Text("Purchased ${stock.symbol} for ${stock.lastSale}"),
actions: <SnackBarAction>[ actions: <SnackBarAction>[
new SnackBarAction(label: "BUY MORE", onPressed: () { _buyStock(stock, arrowKey); }) new SnackBarAction(label: "BUY MORE", onPressed: () { _buyStock(stock, arrowKey); })
...@@ -219,7 +218,7 @@ class StockHomeState extends State<StockHome> { ...@@ -219,7 +218,7 @@ class StockHomeState extends State<StockHome> {
Navigator.pushNamed(context, '/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys); Navigator.pushNamed(context, '/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys);
}, },
onShow: (Stock stock, Key arrowKey) { onShow: (Stock stock, Key arrowKey) {
scaffoldKey.currentState.showBottomSheet((BuildContext context) => new StockSymbolBottomSheet(stock: stock)); _scaffoldKey.currentState.showBottomSheet((BuildContext context) => new StockSymbolBottomSheet(stock: stock));
} }
); );
} }
...@@ -285,9 +284,10 @@ class StockHomeState extends State<StockHome> { ...@@ -285,9 +284,10 @@ class StockHomeState extends State<StockHome> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
key: scaffoldKey, key: _scaffoldKey,
toolBar: _isSearching ? buildSearchBar() : buildToolBar(), toolBar: _isSearching ? buildSearchBar() : buildToolBar(),
floatingActionButton: buildFloatingActionButton(), floatingActionButton: buildFloatingActionButton(),
drawer: _buildDrawer(context),
body: new SizeObserver( body: new SizeObserver(
onSizeChanged: _handleSizeChanged, onSizeChanged: _handleSizeChanged,
child: new TabBarView<StockHomeTab>( child: new TabBarView<StockHomeTab>(
......
...@@ -46,6 +46,8 @@ class CardCollectionState extends State<CardCollection> { ...@@ -46,6 +46,8 @@ class CardCollectionState extends State<CardCollection> {
InvalidatorCallback _invalidator; InvalidatorCallback _invalidator;
Size _cardCollectionSize = new Size(200.0, 200.0); Size _cardCollectionSize = new Size(200.0, 200.0);
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
void _initVariableSizedCardModels() { void _initVariableSizedCardModels() {
List<double> cardHeights = <double>[ List<double> cardHeights = <double>[
48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0, 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
...@@ -117,9 +119,8 @@ class CardCollectionState extends State<CardCollection> { ...@@ -117,9 +119,8 @@ class CardCollectionState extends State<CardCollection> {
} }
} }
void _showDrawer() { Widget _buildDrawer() {
showDrawer( return new Drawer(
context: context,
child: new IconTheme( child: new IconTheme(
data: const IconThemeData(color: IconThemeColor.black), data: const IconThemeData(color: IconThemeColor.black),
child: new Block(<Widget>[ child: new Block(<Widget>[
...@@ -265,9 +266,9 @@ class CardCollectionState extends State<CardCollection> { ...@@ -265,9 +266,9 @@ class CardCollectionState extends State<CardCollection> {
); );
} }
Widget buildToolBar() { Widget _buildToolBar() {
return new ToolBar( return new ToolBar(
left: new IconButton(icon: "navigation/menu", onPressed: _showDrawer), left: new IconButton(icon: "navigation/menu", onPressed: () => _scaffoldKey.currentState?.openDrawer()),
right: <Widget>[ right: <Widget>[
new Text(_dismissDirectionText(_dismissDirection)) new Text(_dismissDirectionText(_dismissDirection))
], ],
...@@ -281,7 +282,7 @@ class CardCollectionState extends State<CardCollection> { ...@@ -281,7 +282,7 @@ class CardCollectionState extends State<CardCollection> {
); );
} }
Widget buildCard(BuildContext context, int index) { Widget _buildCard(BuildContext context, int index) {
if (index >= _cardModels.length) if (index >= _cardModels.length)
return null; return null;
...@@ -393,19 +394,18 @@ class CardCollectionState extends State<CardCollection> { ...@@ -393,19 +394,18 @@ class CardCollectionState extends State<CardCollection> {
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget cardCollection; Widget cardCollection;
if (_fixedSizeCards) { if (_fixedSizeCards) {
cardCollection = new ScrollableList<CardModel> ( cardCollection = new ScrollableList<CardModel> (
snapOffsetCallback: _snapToCenter ? _toSnapOffset : null, snapOffsetCallback: _snapToCenter ? _toSnapOffset : null,
snapAlignmentOffset: _cardCollectionSize.height / 2.0, snapAlignmentOffset: _cardCollectionSize.height / 2.0,
items: _cardModels, items: _cardModels,
itemBuilder: (BuildContext context, CardModel card, int index) => buildCard(context, card.value), itemBuilder: (BuildContext context, CardModel card, int index) => _buildCard(context, card.value),
itemExtent: _cardModels[0].height itemExtent: _cardModels[0].height
); );
} else { } else {
cardCollection = new ScrollableMixedWidgetList( cardCollection = new ScrollableMixedWidgetList(
builder: buildCard, builder: _buildCard,
token: _cardModels.length, token: _cardModels.length,
snapOffsetCallback: _snapToCenter ? _toSnapOffset : null, snapOffsetCallback: _snapToCenter ? _toSnapOffset : null,
snapAlignmentOffset: _cardCollectionSize.height / 2.0, snapAlignmentOffset: _cardCollectionSize.height / 2.0,
...@@ -446,7 +446,8 @@ class CardCollectionState extends State<CardCollection> { ...@@ -446,7 +446,8 @@ class CardCollectionState extends State<CardCollection> {
primarySwatch: _primaryColor primarySwatch: _primaryColor
), ),
child: new Scaffold( child: new Scaffold(
toolBar: buildToolBar(), toolBar: _buildToolBar(),
drawer: _buildDrawer(),
body: body body: body
) )
); );
......
...@@ -41,6 +41,8 @@ class PageableListAppState extends State<PageableListApp> { ...@@ -41,6 +41,8 @@ class PageableListAppState extends State<PageableListApp> {
ScrollDirection scrollDirection = ScrollDirection.horizontal; ScrollDirection scrollDirection = ScrollDirection.horizontal;
bool itemsWrap = false; bool itemsWrap = false;
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
void updatePageSize(Size newSize) { void updatePageSize(Size newSize) {
setState(() { setState(() {
pageSize = newSize; pageSize = newSize;
...@@ -83,9 +85,8 @@ class PageableListAppState extends State<PageableListApp> { ...@@ -83,9 +85,8 @@ class PageableListAppState extends State<PageableListApp> {
}); });
} }
void _showDrawer() { Widget _buildDrawer() {
showDrawer( return new Drawer(
context: context,
child: new Block(<Widget>[ child: new Block(<Widget>[
new DrawerHeader(child: new Text('Options')), new DrawerHeader(child: new Text('Options')),
new DrawerItem( new DrawerItem(
...@@ -111,9 +112,9 @@ class PageableListAppState extends State<PageableListApp> { ...@@ -111,9 +112,9 @@ class PageableListAppState extends State<PageableListApp> {
); );
} }
Widget buildToolBar() { Widget _buildToolBar() {
return new ToolBar( return new ToolBar(
left: new IconButton(icon: "navigation/menu", onPressed: _showDrawer), left: new IconButton(icon: "navigation/menu", onPressed: () => _scaffoldKey.currentState?.openDrawer()),
center: new Text('PageableList'), center: new Text('PageableList'),
right: <Widget>[ right: <Widget>[
new Text(scrollDirection == ScrollDirection.horizontal ? "horizontal" : "vertical") new Text(scrollDirection == ScrollDirection.horizontal ? "horizontal" : "vertical")
...@@ -121,7 +122,7 @@ class PageableListAppState extends State<PageableListApp> { ...@@ -121,7 +122,7 @@ class PageableListAppState extends State<PageableListApp> {
); );
} }
Widget buildBody(BuildContext context) { Widget _buildBody(BuildContext context) {
Widget list = new PageableList<CardModel>( Widget list = new PageableList<CardModel>(
items: cardModels, items: cardModels,
itemsWrap: itemsWrap, itemsWrap: itemsWrap,
...@@ -141,8 +142,9 @@ class PageableListAppState extends State<PageableListApp> { ...@@ -141,8 +142,9 @@ class PageableListAppState extends State<PageableListApp> {
return new IconTheme( return new IconTheme(
data: const IconThemeData(color: IconThemeColor.white), data: const IconThemeData(color: IconThemeColor.white),
child: new Scaffold( child: new Scaffold(
toolBar: buildToolBar(), toolBar: _buildToolBar(),
body: buildBody(context) drawer: _buildDrawer(),
body: _buildBody(context)
) )
); );
} }
......
...@@ -22,111 +22,48 @@ import 'material.dart'; ...@@ -22,111 +22,48 @@ import 'material.dart';
// The right nav can vary depending on content. // The right nav can vary depending on content.
const double _kWidth = 304.0; const double _kWidth = 304.0;
const double _kEdgeDragWidth = 20.0;
const double _kMinFlingVelocity = 365.0; const double _kMinFlingVelocity = 365.0;
const Duration _kBaseSettleDuration = const Duration(milliseconds: 246); const Duration _kBaseSettleDuration = const Duration(milliseconds: 246);
class _Drawer extends StatelessComponent { class Drawer extends StatelessComponent {
_Drawer({ Key key, this.route }) : super(key: key); Drawer({
Key key,
final _DrawerRoute route; this.elevation: 16,
this.child
Widget build(BuildContext context) { }) : super(key: key);
return new Focus(
key: new GlobalObjectKey(route),
child: new ConstrainedBox(
constraints: const BoxConstraints.expand(width: _kWidth),
child: new Material(
elevation: route.elevation,
child: route.child
)
)
);
}
}
enum _DrawerState {
showing,
popped,
closed,
}
class _DrawerRoute extends OverlayRoute {
_DrawerRoute({ this.child, this.elevation });
final Widget child;
final int elevation; final int elevation;
final Widget child;
List<WidgetBuilder> get builders => <WidgetBuilder>[ _build ]; Widget build(BuildContext context) {
return new ConstrainedBox(
final GlobalKey<_DrawerControllerState> _drawerKey = new GlobalKey<_DrawerControllerState>(); constraints: const BoxConstraints.expand(width: _kWidth),
_DrawerState _state = _DrawerState.showing; child: new Material(
elevation: elevation,
Widget _build(BuildContext context) { child: child
return new RepaintBoundary(
child: new _DrawerController(
key: _drawerKey,
settleDuration: _kBaseSettleDuration,
onClosed: () {
_DrawerState previousState = _state;
_state = _DrawerState.closed;
switch (previousState) {
case _DrawerState.showing:
Navigator.pop(context);
break;
case _DrawerState.popped:
finished();
break;
case _DrawerState.closed:
assert(false);
break;
}
},
child: new _Drawer(route: this)
) )
); );
} }
bool didPop(dynamic result) {
// we don't call the superclass because we want to control the timing of the
// call to finished().
switch (_state) {
case _DrawerState.showing:
_drawerKey.currentState?._close();
_state = _DrawerState.popped;
break;
case _DrawerState.popped:
assert(false);
break;
case _DrawerState.closed:
finished();
break;
}
return true;
}
} }
class _DrawerController extends StatefulComponent { class DrawerController extends StatefulComponent {
_DrawerController({ DrawerController({
Key key, GlobalKey key,
this.settleDuration,
this.onClosed,
this.child this.child
}) : super(key: key); }) : super(key: key);
final Duration settleDuration;
final Widget child; final Widget child;
final VoidCallback onClosed;
_DrawerControllerState createState() => new _DrawerControllerState(); DrawerControllerState createState() => new DrawerControllerState();
} }
class _DrawerControllerState extends State<_DrawerController> { class DrawerControllerState extends State<DrawerController> {
void initState() { void initState() {
super.initState(); super.initState();
_performance = new Performance(duration: config.settleDuration) _performance = new Performance(duration: _kBaseSettleDuration)
..addListener(_performanceChanged) ..addListener(_performanceChanged)
..addStatusListener(_performanceStatusChanged) ..addStatusListener(_performanceStatusChanged);
..play();
} }
void dispose() { void dispose() {
...@@ -143,15 +80,41 @@ class _DrawerControllerState extends State<_DrawerController> { ...@@ -143,15 +80,41 @@ class _DrawerControllerState extends State<_DrawerController> {
}); });
} }
LocalHistoryEntry _historyEntry;
void _ensureHistoryEntry() {
if (_historyEntry == null) {
ModalRoute route = ModalRoute.of(context);
if (route != null) {
_historyEntry = new LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved);
route.addLocalHistoryEntry(_historyEntry);
}
}
}
void _performanceStatusChanged(PerformanceStatus status) { void _performanceStatusChanged(PerformanceStatus status) {
if (status == PerformanceStatus.dismissed && config.onClosed != null) switch (status) {
config.onClosed(); case PerformanceStatus.forward:
_ensureHistoryEntry();
break;
case PerformanceStatus.reverse:
_historyEntry?.remove();
_historyEntry = null;
break;
case PerformanceStatus.dismissed:
break;
case PerformanceStatus.completed:
break;
}
} }
Performance _performance; void _handleHistoryEntryRemoved() {
double _width; _historyEntry = null;
close();
}
final AnimatedColorValue _color = new AnimatedColorValue(Colors.transparent, end: Colors.black54); Performance _performance;
double _width = _kEdgeDragWidth;
void _handleSizeChanged(Size newSize) { void _handleSizeChanged(Size newSize) {
setState(() { setState(() {
...@@ -161,6 +124,7 @@ class _DrawerControllerState extends State<_DrawerController> { ...@@ -161,6 +124,7 @@ class _DrawerControllerState extends State<_DrawerController> {
void _handlePointerDown(_) { void _handlePointerDown(_) {
_performance.stop(); _performance.stop();
_ensureHistoryEntry();
} }
void _move(double delta) { void _move(double delta) {
...@@ -168,57 +132,77 @@ class _DrawerControllerState extends State<_DrawerController> { ...@@ -168,57 +132,77 @@ class _DrawerControllerState extends State<_DrawerController> {
} }
void _settle(Offset velocity) { void _settle(Offset velocity) {
if (_performance.isDismissed)
return;
if (velocity.dx.abs() >= _kMinFlingVelocity) { if (velocity.dx.abs() >= _kMinFlingVelocity) {
_performance.fling(velocity: velocity.dx / _width); _performance.fling(velocity: velocity.dx / _width);
} else if (_performance.progress < 0.5) { } else if (_performance.progress < 0.5) {
_close(); close();
} else { } else {
_performance.fling(velocity: 1.0); open();
} }
} }
void _close() { void open() {
_performance.fling(velocity: 1.0);
}
void close() {
_performance.fling(velocity: -1.0); _performance.fling(velocity: -1.0);
} }
final AnimatedColorValue _color = new AnimatedColorValue(Colors.transparent, end: Colors.black54);
Widget build(BuildContext context) { Widget build(BuildContext context) {
_performance.updateVariable(_color); HitTestBehavior behavior;
return new GestureDetector( Widget child;
onHorizontalDragUpdate: _move, if (_performance.status == PerformanceStatus.dismissed) {
onHorizontalDragEnd: _settle, behavior = HitTestBehavior.translucent;
child: new Stack(<Widget>[ child = new Align(
new GestureDetector( alignment: const FractionalOffset(0.0, 0.5),
onTap: _close, widthFactor: 1.0,
child: new DecoratedBox( child: new Container(width: _kEdgeDragWidth)
decoration: new BoxDecoration( );
backgroundColor: _color.value } else {
), _performance.updateVariable(_color);
child: new Container() child = new RepaintBoundary(
) child: new Stack(<Widget>[
), new GestureDetector(
new Positioned( onTap: close,
top: 0.0, child: new DecoratedBox(
left: 0.0, decoration: new BoxDecoration(
bottom: 0.0, backgroundColor: _color.value
child: new Listener( ),
onPointerDown: _handlePointerDown, child: new Container()
child: new Align( )
alignment: const FractionalOffset(1.0, 0.5), ),
widthFactor: _performance.progress, new Align(
child: new SizeObserver( alignment: const FractionalOffset(0.0, 0.5),
onSizeChanged: _handleSizeChanged, child: new Listener(
child: new RepaintBoundary( onPointerDown: _handlePointerDown,
child: config.child child: new Align(
alignment: const FractionalOffset(1.0, 0.5),
widthFactor: _performance.progress,
child: new SizeObserver(
onSizeChanged: _handleSizeChanged,
child: new RepaintBoundary(
child: new Focus(
key: new GlobalObjectKey(config.key),
child: config.child
)
)
) )
) )
) )
) )
) ])
]) );
}
return new GestureDetector(
onHorizontalDragUpdate: _move,
onHorizontalDragEnd: _settle,
behavior: behavior,
child: child
); );
} }
} }
void showDrawer({ BuildContext context, Widget child, int elevation: 16 }) {
Navigator.push(context, new _DrawerRoute(child: child, elevation: elevation));
}
...@@ -15,20 +15,30 @@ import 'bottom_sheet.dart'; ...@@ -15,20 +15,30 @@ import 'bottom_sheet.dart';
import 'material.dart'; import 'material.dart';
import 'snack_bar.dart'; import 'snack_bar.dart';
import 'tool_bar.dart'; import 'tool_bar.dart';
import 'drawer.dart';
const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent
enum _Child { body, toolBar, bottomSheet, snackBar, floatingActionButton } enum _Child {
body,
toolBar,
bottomSheet,
snackBar,
floatingActionButton,
drawer,
}
class _ScaffoldLayout extends MultiChildLayoutDelegate { class _ScaffoldLayout extends MultiChildLayoutDelegate {
void performLayout(Size size, BoxConstraints constraints) { void performLayout(Size size, BoxConstraints constraints) {
BoxConstraints looseConstraints = constraints.loosen();
// This part of the layout has the same effect as putting the toolbar and // This part of the layout has the same effect as putting the toolbar and
// body in a column and making the body flexible. What's different is that // body in a column and making the body flexible. What's different is that
// in this case the toolbar appears -after- the body in the stacking order, // in this case the toolbar appears -after- the body in the stacking order,
// so the toolbar's shadow is drawn on top of the body. // so the toolbar's shadow is drawn on top of the body.
final BoxConstraints toolBarConstraints = constraints.loosen().tightenWidth(size.width); final BoxConstraints toolBarConstraints = looseConstraints.tightenWidth(size.width);
Size toolBarSize = Size.zero; Size toolBarSize = Size.zero;
if (isChild(_Child.toolBar)) { if (isChild(_Child.toolBar)) {
...@@ -52,7 +62,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -52,7 +62,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
// non-zero height then it's inset from the parent's right and bottom edges // non-zero height then it's inset from the parent's right and bottom edges
// by _kFloatingActionButtonMargin. // by _kFloatingActionButtonMargin.
final BoxConstraints fullWidthConstraints = constraints.loosen().tightenWidth(size.width); final BoxConstraints fullWidthConstraints = looseConstraints.tightenWidth(size.width);
Size bottomSheetSize = Size.zero; Size bottomSheetSize = Size.zero;
Size snackBarSize = Size.zero; Size snackBarSize = Size.zero;
...@@ -67,7 +77,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -67,7 +77,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
} }
if (isChild(_Child.floatingActionButton)) { if (isChild(_Child.floatingActionButton)) {
final Size fabSize = layoutChild(_Child.floatingActionButton, constraints.loosen()); final Size fabSize = layoutChild(_Child.floatingActionButton, looseConstraints);
final double fabX = size.width - fabSize.width - _kFloatingActionButtonMargin; final double fabX = size.width - fabSize.width - _kFloatingActionButtonMargin;
double fabY = size.height - fabSize.height - _kFloatingActionButtonMargin; double fabY = size.height - fabSize.height - _kFloatingActionButtonMargin;
if (snackBarSize.height > 0.0) if (snackBarSize.height > 0.0)
...@@ -76,6 +86,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -76,6 +86,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
fabY = math.min(fabY, size.height - bottomSheetSize.height - fabSize.height / 2.0); fabY = math.min(fabY, size.height - bottomSheetSize.height - fabSize.height / 2.0);
positionChild(_Child.floatingActionButton, new Point(fabX, fabY)); positionChild(_Child.floatingActionButton, new Point(fabX, fabY));
} }
if (isChild(_Child.drawer)) {
layoutChild(_Child.drawer, looseConstraints);
positionChild(_Child.drawer, Point.origin);
}
} }
} }
...@@ -86,12 +101,14 @@ class Scaffold extends StatefulComponent { ...@@ -86,12 +101,14 @@ class Scaffold extends StatefulComponent {
Key key, Key key,
this.toolBar, this.toolBar,
this.body, this.body,
this.floatingActionButton this.floatingActionButton,
this.drawer
}) : super(key: key); }) : super(key: key);
final ToolBar toolBar; final ToolBar toolBar;
final Widget body; final Widget body;
final Widget floatingActionButton; final Widget floatingActionButton;
final Widget drawer;
static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(ScaffoldState); static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(ScaffoldState);
...@@ -100,6 +117,14 @@ class Scaffold extends StatefulComponent { ...@@ -100,6 +117,14 @@ class Scaffold extends StatefulComponent {
class ScaffoldState extends State<Scaffold> { class ScaffoldState extends State<Scaffold> {
// DRAWER API
final GlobalKey<DrawerControllerState> _drawerKey = new GlobalKey<DrawerControllerState>();
void openDrawer() {
_drawerKey.currentState.open();
}
// SNACKBAR API // SNACKBAR API
Queue<ScaffoldFeatureController<SnackBar>> _snackBars = new Queue<ScaffoldFeatureController<SnackBar>>(); Queue<ScaffoldFeatureController<SnackBar>> _snackBars = new Queue<ScaffoldFeatureController<SnackBar>>();
...@@ -270,6 +295,16 @@ class ScaffoldState extends State<Scaffold> { ...@@ -270,6 +295,16 @@ class ScaffoldState extends State<Scaffold> {
_addIfNonNull(children, config.floatingActionButton, _Child.floatingActionButton); _addIfNonNull(children, config.floatingActionButton, _Child.floatingActionButton);
if (config.drawer != null) {
children.add(new LayoutId(
id: _Child.drawer,
child: new DrawerController(
key: _drawerKey,
child: config.drawer
)
));
}
return new CustomMultiChildLayout(children, delegate: _scaffoldLayout); return new CustomMultiChildLayout(children, delegate: _scaffoldLayout);
} }
......
...@@ -146,7 +146,7 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -146,7 +146,7 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
super.dispose(); super.dispose();
} }
final ProxyPerformance forwardPerformance = new ProxyPerformance(); final ProxyPerformance forwardPerformance = new ProxyPerformance();
void didPushNext(Route nextRoute) { void didPushNext(Route nextRoute) {
...@@ -183,7 +183,7 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -183,7 +183,7 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
forwardPerformance, forwardPerformance,
buildTransition( buildTransition(
context, context,
performance, performance,
child child
) )
); );
...@@ -202,6 +202,7 @@ class LocalHistoryEntry { ...@@ -202,6 +202,7 @@ class LocalHistoryEntry {
LocalHistoryRoute _owner; LocalHistoryRoute _owner;
void remove() { void remove() {
_owner.removeLocalHistoryEntry(this); _owner.removeLocalHistoryEntry(this);
assert(_owner == null);
} }
void _notifyRemoved() { void _notifyRemoved() {
if (onRemove != null) if (onRemove != null)
...@@ -432,4 +433,4 @@ abstract class PageRoute<T> extends ModalRoute<T> { ...@@ -432,4 +433,4 @@ abstract class PageRoute<T> extends ModalRoute<T> {
NamedRouteSettings settings: const NamedRouteSettings() NamedRouteSettings settings: const NamedRouteSettings()
}) : super(completer: completer, settings: settings); }) : super(completer: completer, settings: settings);
bool get opaque => true; bool get opaque => true;
} }
\ No newline at end of file
...@@ -11,20 +11,25 @@ void main() { ...@@ -11,20 +11,25 @@ void main() {
test('Drawer control test', () { test('Drawer control test', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
BuildContext context; BuildContext context;
tester.pumpWidget( tester.pumpWidget(
new MaterialApp( new MaterialApp(
routes: <String, RouteBuilder>{ routes: <String, RouteBuilder>{
'/': (RouteArguments args) { '/': (RouteArguments args) {
context = args.context; context = args.context;
return new Container(); return new Scaffold(
key: scaffoldKey,
drawer: new Text('drawer'),
body: new Container()
);
} }
} }
) )
); );
tester.pump(); // no effect tester.pump(); // no effect
expect(tester.findText('drawer'), isNull); expect(tester.findText('drawer'), isNull);
showDrawer(context: context, child: new Text('drawer')); scaffoldKey.currentState.openDrawer();
tester.pump(); // drawer should be starting to animate in tester.pump(); // drawer should be starting to animate in
expect(tester.findText('drawer'), isNotNull); expect(tester.findText('drawer'), isNotNull);
tester.pump(new Duration(seconds: 1)); // animation done tester.pump(new Duration(seconds: 1)); // animation done
...@@ -39,21 +44,24 @@ void main() { ...@@ -39,21 +44,24 @@ void main() {
test('Drawer tap test', () { test('Drawer tap test', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
BuildContext context; GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
tester.pumpWidget(new Container()); // throw away the old App and its Navigator tester.pumpWidget(new Container()); // throw away the old App and its Navigator
tester.pumpWidget( tester.pumpWidget(
new MaterialApp( new MaterialApp(
routes: <String, RouteBuilder>{ routes: <String, RouteBuilder>{
'/': (RouteArguments args) { '/': (RouteArguments args) {
context = args.context; return new Scaffold(
return new Container(); key: scaffoldKey,
drawer: new Text('drawer'),
body: new Container()
);
} }
} }
) )
); );
tester.pump(); // no effect tester.pump(); // no effect
expect(tester.findText('drawer'), isNull); expect(tester.findText('drawer'), isNull);
showDrawer(context: context, child: new Text('drawer')); scaffoldKey.currentState.openDrawer();
tester.pump(); // drawer should be starting to animate in tester.pump(); // drawer should be starting to animate in
expect(tester.findText('drawer'), isNotNull); expect(tester.findText('drawer'), isNotNull);
tester.pump(new Duration(seconds: 1)); // animation done tester.pump(new Duration(seconds: 1)); // animation done
...@@ -64,7 +72,9 @@ void main() { ...@@ -64,7 +72,9 @@ void main() {
tester.pump(new Duration(seconds: 1)); // ditto tester.pump(new Duration(seconds: 1)); // ditto
expect(tester.findText('drawer'), isNotNull); expect(tester.findText('drawer'), isNotNull);
tester.tapAt(const Point(750.0, 100.0)); // on the mask tester.tapAt(const Point(750.0, 100.0)); // on the mask
tester.pump(); // drawer should be starting to animate away tester.pump();
tester.pump(new Duration(milliseconds: 10));
// drawer should be starting to animate away
expect(tester.findText('drawer'), isNotNull); expect(tester.findText('drawer'), isNotNull);
tester.pump(new Duration(seconds: 1)); // animation done tester.pump(new Duration(seconds: 1)); // animation done
expect(tester.findText('drawer'), isNull); expect(tester.findText('drawer'), isNull);
......
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