diff --git a/examples/flutter_gallery/lib/demo/all.dart b/examples/flutter_gallery/lib/demo/all.dart index d199ab73de5..7b8f91d35df 100644 --- a/examples/flutter_gallery/lib/demo/all.dart +++ b/examples/flutter_gallery/lib/demo/all.dart @@ -12,6 +12,7 @@ export 'colors_demo.dart'; export 'data_table_demo.dart'; export 'date_and_time_picker_demo.dart'; export 'dialog_demo.dart'; +export 'drawer_demo.dart'; export 'expansion_panels_demo.dart'; export 'grid_list_demo.dart'; export 'icons_demo.dart'; diff --git a/examples/flutter_gallery/lib/demo/drawer_demo.dart b/examples/flutter_gallery/lib/demo/drawer_demo.dart new file mode 100644 index 00000000000..caae2615b8a --- /dev/null +++ b/examples/flutter_gallery/lib/demo/drawer_demo.dart @@ -0,0 +1,186 @@ +// Copyright 2016 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:flutter/material.dart'; + +const String _kAsset0 = 'packages/flutter_gallery_assets/shrine/vendors/zach.jpg'; +const String _kAsset1 = 'packages/flutter_gallery_assets/shrine/vendors/16c477b.jpg'; +const String _kAsset2 = 'packages/flutter_gallery_assets/shrine/vendors/sandra-adams.jpg'; + +class DrawerDemo extends StatefulWidget { + static const String routeName = '/drawer'; + + @override + _DrawerDemoState createState() => new _DrawerDemoState(); +} + +class _DrawerDemoState extends State with TickerProviderStateMixin { + final GlobalKey _scaffoldKey = new GlobalKey(); + + static const List _drawerContents = const [ + 'A', 'B', 'C', 'D', 'E', + ]; + + AnimationController _controller; + Animation _drawerContentsOpacity; + Animation _drawerDetailsPosition; + bool _showDrawerContents = true; + + @override + void initState() { + super.initState(); + _controller = new AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + _drawerContentsOpacity = new CurvedAnimation( + parent: new ReverseAnimation(_controller), + curve: Curves.fastOutSlowIn, + ); + _drawerDetailsPosition = new Tween( + begin: const FractionalOffset(0.0, -1.0), + end: const FractionalOffset(0.0, 0.0), + ).animate(new CurvedAnimation( + parent: _controller, + curve: Curves.fastOutSlowIn, + )); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + IconData _backIcon() { + switch (Theme.of(context).platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + return Icons.arrow_back; + case TargetPlatform.iOS: + return Icons.arrow_back_ios; + } + assert(false); + return null; + } + + void _showNotImplementedMessage() { + Navigator.of(context).pop(); // Dismiss the drawer. + _scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text("The drawer's items don't do anything") + )); + } + + @override + Widget build(BuildContext context) { + return new Scaffold( + key: _scaffoldKey, + appBar: new AppBar( + leading: new IconButton( + icon: new Icon(_backIcon()), + alignment: FractionalOffset.centerLeft, + tooltip: 'Back', + onPressed: () { + Navigator.pop(context); + }, + ), + title: new Text('Navigation drawer'), + ), + drawer: new Drawer( + child: new Block( + children: [ + new UserAccountsDrawerHeader( + accountName: new Text('Zach Widget'), + accountEmail: new Text('zach.widget@example.com'), + currentAccountPicture: new CircleAvatar(backgroundImage: new AssetImage(_kAsset0)), + otherAccountsPictures: [ + new CircleAvatar(backgroundImage: new AssetImage(_kAsset1)), + new CircleAvatar(backgroundImage: new AssetImage(_kAsset2)), + ], + onDetailsPressed: () { + _showDrawerContents = !_showDrawerContents; + if (_showDrawerContents) + _controller.reverse(); + else + _controller.forward(); + }, + ), + new ClipRect( + child: new Stack( + children: [ + // The initial contents of the drawer. + new FadeTransition( + opacity: _drawerContentsOpacity, + child: new Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _drawerContents.map((String id) { + return new DrawerItem( + icon: new CircleAvatar(child: new Text(id)), + child: new Text('Drawer item $id'), + onPressed: _showNotImplementedMessage, + ); + }).toList(), + ), + ), + // The drawer's "details" view. + new SlideTransition( + position: _drawerDetailsPosition, + child: new FadeTransition( + opacity: new ReverseAnimation(_drawerContentsOpacity), + child: new Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new DrawerItem( + icon: new Icon(Icons.add), + child: new Text('Add account'), + onPressed: _showNotImplementedMessage, + ), + new DrawerItem( + icon: new Icon(Icons.settings), + child: new Text('Manage accounts'), + onPressed: _showNotImplementedMessage, + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + body: new Center( + child: new InkWell( + onTap: () { + _scaffoldKey.currentState.openDrawer(); + }, + child: new Column( + mainAxisSize: MainAxisSize.min, + children: [ + new Container( + width: 100.0, + height: 100.0, + decoration: new BoxDecoration( + shape: BoxShape.circle, + backgroundImage: new BackgroundImage( + image: new AssetImage(_kAsset0), + ), + ), + ), + new Padding( + padding: const EdgeInsets.only(top: 8.0), + child: new Text('Tap here to open the drawer', + style: Theme.of(context).textTheme.subhead, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/examples/flutter_gallery/lib/gallery/item.dart b/examples/flutter_gallery/lib/gallery/item.dart index a29607e6ae1..518d5cbdc93 100644 --- a/examples/flutter_gallery/lib/gallery/item.dart +++ b/examples/flutter_gallery/lib/gallery/item.dart @@ -102,6 +102,12 @@ final List kAllGalleryItems = [ routeName: DialogDemo.routeName, buildRoute: (BuildContext context) => new DialogDemo() ), + new GalleryItem( + title: 'Drawer', + subtitle: 'Navigation drawer with a standard header', + routeName: DrawerDemo.routeName, + buildRoute: (BuildContext context) => new DrawerDemo() + ), new GalleryItem( title: 'Expand/collapse list control', subtitle: 'List with one level of sublists', diff --git a/examples/flutter_gallery/test_driver/transitions_perf_test.dart b/examples/flutter_gallery/test_driver/transitions_perf_test.dart index fe54fc667d8..2fafa7d89ae 100644 --- a/examples/flutter_gallery/test_driver/transitions_perf_test.dart +++ b/examples/flutter_gallery/test_driver/transitions_perf_test.dart @@ -33,6 +33,7 @@ final List demoTitles = [ 'Chips', 'Date and time pickers', 'Dialog', + 'Drawer', 'Expand/collapse list control', 'Expansion panels', 'Floating action button', diff --git a/packages/flutter/lib/src/material/user_accounts_drawer_header.dart b/packages/flutter/lib/src/material/user_accounts_drawer_header.dart index dc0c75d16b7..ee24c88f9ba 100644 --- a/packages/flutter/lib/src/material/user_accounts_drawer_header.dart +++ b/packages/flutter/lib/src/material/user_accounts_drawer_header.dart @@ -3,25 +3,128 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'debug.dart'; +class _AccountPictures extends StatelessWidget { + _AccountPictures({ + Key key, + this.currentAccountPicture, + this.otherAccountsPictures, + }) : super(key: key); + + final Widget currentAccountPicture; + final List otherAccountsPictures; + + @override + Widget build(BuildContext context) { + return new Stack( + children: [ + new Positioned( + top: 0.0, + right: 0.0, + child: new Row( + children: (otherAccountsPictures ?? []).take(3).map((Widget picture) { + return new Container( + margin: const EdgeInsets.only(left: 16.0), + width: 40.0, + height: 40.0, + child: picture + ); + }).toList(), + ), + ), + new Positioned( + top: 0.0, + child: new SizedBox( + width: 72.0, + height: 72.0, + child: currentAccountPicture + ), + ), + ], + ); + } +} + +class _AccountDetails extends StatelessWidget { + _AccountDetails({ + Key key, + this.accountName, + this.accountEmail, + this.onTap, + this.isOpen, + }) : super(key: key); + + final Widget accountName; + final Widget accountEmail; + final VoidCallback onTap; + final bool isOpen; + + Widget addDropdownIcon(Widget line) { + final Widget icon = new Expanded( + child: new Align( + alignment: FractionalOffset.centerRight, + child: new Icon( + isOpen ? Icons.arrow_drop_up : Icons.arrow_drop_down, + color: Colors.white + ), + ), + ); + return new Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: line == null ? [icon] : [line, icon], + ); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + Widget accountNameLine = accountName == null ? null : new DefaultTextStyle( + style: theme.primaryTextTheme.body2, + child: accountName, + ); + Widget accountEmailLine = accountEmail == null ? null : new DefaultTextStyle( + style: theme.primaryTextTheme.body1, + child: accountEmail, + ); + if (onTap != null) { + if (accountEmailLine != null) + accountEmailLine = addDropdownIcon(accountEmailLine); + else + accountNameLine = addDropdownIcon(accountNameLine); + } + + Widget accountDetails; + if (accountEmailLine != null || accountNameLine != null) { + accountDetails = new Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: (accountEmailLine != null && accountNameLine != null) + ? [accountNameLine, accountEmailLine] + : [accountNameLine ?? accountEmailLine] + ), + ); + } + + if (onTap != null) + accountDetails = new InkWell(onTap: onTap, child: accountDetails); + + return new SizedBox( + height: 56.0, + child: accountDetails, + ); + } +} + /// A material design [Drawer] header that identifies the app's user. /// -/// The top-most region of a material design drawer with user accounts. The -/// header's [decoration] is used to provide a background. -/// [currentAccountPicture] is the main account picture on the left, while -/// [otherAccountsPictures] are the smaller account pictures on the right. -/// [accountName] and [accountEmail] provide access to the top and bottom rows -/// of the account details in the lower part of the header. When touched, this -/// area triggers [onDetailsPressed] and toggles the dropdown icon on the right. -/// /// Requires one of its ancestors to be a [Material] widget. /// /// See also: /// -/// * [Drawer] /// * [DrawerHeader], for a drawer header that doesn't show user acounts /// * class UserAccountsDrawerHeader extends StatefulWidget { @@ -38,30 +141,31 @@ class UserAccountsDrawerHeader extends StatefulWidget { this.onDetailsPressed }) : super(key: key); - /// A callback that gets called when the account name/email/dropdown - /// section is pressed. - final VoidCallback onDetailsPressed; - - /// The background to show in the drawer header. + /// The header's background. If decoration is null then a [BoxDecoration] + /// with its background color set to the current theme's primaryColor is used. final Decoration decoration; - /// A widget placed in the upper-left corner representing the current - /// account picture. Normally a [CircleAvatar]. + /// A widget placed in the upper-left corner that represents the current + /// user's account. Normally a [CircleAvatar]. final Widget currentAccountPicture; - /// A list of widgets that represent the user's accounts. Up to three of will - /// be arranged in a row in the header's upper-right corner. Normally a list - /// of [CircleAvatar] widgets. + /// A list of widgets that represent the current user's other accounts. + /// Up to three of these widgets will be arranged in a row in the header's + /// upper-right corner. Normally a list of [CircleAvatar] widgets. final List otherAccountsPictures; - /// A widget placed on the top row of the account details representing the - /// account's name. + /// A widget that represents the user's current account name. It is + /// displayed on the left, below the [currentAccountPicture]. final Widget accountName; - /// A widget placed on the bottom row of the account details representing the - /// account's e-mail address. + /// A widget that represents the email address of the user's current account. + /// It is displayed on the left, below the [accountName]. final Widget accountEmail; + /// A callback that is called when the horizontal area which contains the + /// [accountName] and [accountEmail] is tapped. + final VoidCallback onDetailsPressed; + @override _UserAccountsDrawerHeaderState createState() => new _UserAccountsDrawerHeaderState(); } @@ -72,89 +176,32 @@ class _UserAccountsDrawerHeaderState extends State { @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); - final List otherAccountsPictures = config.otherAccountsPictures ?? []; return new DrawerHeader( - decoration: config.decoration, + decoration: config.decoration ?? new BoxDecoration( + backgroundColor: Theme.of(context).primaryColor, + ), child: new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ new Expanded( - child: new Stack( - children: [ - new Positioned( - top: 0.0, - right: 0.0, - child: new Row( - children: otherAccountsPictures.take(3).map( - (Widget picture) { - return new Container( - margin: const EdgeInsets.only(left: 16.0), - width: 40.0, - height: 40.0, - child: picture - ); - } - ).toList() - ) - ), - new Positioned( - top: 0.0, - child: new Container( - width: 72.0, - height: 72.0, - child: config.currentAccountPicture - ) - ) - ] + child: new _AccountPictures( + currentAccountPicture: config.currentAccountPicture, + otherAccountsPictures: config.otherAccountsPictures, ) ), - new Container( - height: 56.0, - child: new InkWell( - onTap: () { - setState(() { - _isOpen = !_isOpen; - }); - if (config.onDetailsPressed != null) - config.onDetailsPressed(); - }, - child: new Container( - margin: const EdgeInsets.symmetric(vertical: 8.0), - child: new Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - new DefaultTextStyle( - style: const TextStyle( - fontWeight: FontWeight.w500, - color: Colors.white - ), - child: config.accountName - ), - new Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - new DefaultTextStyle( - style: const TextStyle(color: Colors.white), - child: config.accountEmail - ), - new Expanded( - child: new Align( - alignment: FractionalOffset.centerRight, - child: new Icon( - _isOpen ? Icons.arrow_drop_up : Icons.arrow_drop_down, - color: Colors.white - ) - ) - ) - ] - ) - ] - ) - ) - ) - ) - ] - ) + new _AccountDetails( + accountName: config.accountName, + accountEmail: config.accountEmail, + isOpen: _isOpen, + onTap: config.onDetailsPressed == null ? null : () { + setState(() { + _isOpen = !_isOpen; + }); + config.onDetailsPressed(); + }, + ), + ], + ), ); } } diff --git a/packages/flutter/test/material/user_accounts_drawer_header_test.dart b/packages/flutter/test/material/user_accounts_drawer_header_test.dart index 66009e85729..5003fb3b4b6 100644 --- a/packages/flutter/test/material/user_accounts_drawer_header_test.dart +++ b/packages/flutter/test/material/user_accounts_drawer_header_test.dart @@ -71,4 +71,93 @@ void main() { expect(avatarDTopRight.y - topRight.y, equals(16.0)); expect(avatarDTopRight.x - avatarCTopRight.x, equals(40.0 + 16.0)); // size + space between }); + + + testWidgets('UserAccountsDrawerHeader null parameters', (WidgetTester tester) async { + Widget buildFrame({ + Widget currentAccountPicture, + List otherAccountsPictures, + Widget accountName, + Widget accountEmail, + VoidCallback onDetailsPressed, + }) { + return new Material( + child: new UserAccountsDrawerHeader( + currentAccountPicture: currentAccountPicture, + otherAccountsPictures: otherAccountsPictures, + accountName: accountName, + accountEmail: accountEmail, + onDetailsPressed: onDetailsPressed, + ), + ); + } + + await tester.pumpWidget(buildFrame()); + expect(find.byType(Icon), findsNothing); + + await tester.pumpWidget(buildFrame( + onDetailsPressed: () { }, + )); + expect(find.byType(Icon), findsOneWidget); + + await tester.pumpWidget(buildFrame( + accountName: new Text('accountName'), + onDetailsPressed: () { }, + )); + expect( + tester.getCenter(find.text('accountName')).y, + tester.getCenter(find.byType(Icon)).y + ); + + await tester.pumpWidget(buildFrame( + accountEmail: new Text('accountEmail'), + onDetailsPressed: () { }, + )); + expect( + tester.getCenter(find.text('accountEmail')).y, + tester.getCenter(find.byType(Icon)).y + ); + + await tester.pumpWidget(buildFrame( + accountName: new Text('accountName'), + accountEmail: new Text('accountEmail'), + onDetailsPressed: () { }, + )); + expect( + tester.getCenter(find.text('accountEmail')).y, + tester.getCenter(find.byType(Icon)).y + ); + expect( + tester.getBottomLeft(find.text('accountEmail')).y, + greaterThan(tester.getBottomLeft(find.text('accountName')).y) + ); + expect( + tester.getBottomLeft(find.text('accountEmail')).x, + tester.getBottomLeft(find.text('accountName')).x + ); + + await tester.pumpWidget(buildFrame( + currentAccountPicture: new CircleAvatar(child: new Text('A')), + )); + expect(find.text('A'), findsOneWidget); + + await tester.pumpWidget(buildFrame( + otherAccountsPictures: [new CircleAvatar(child: new Text('A'))], + )); + expect(find.text('A'), findsOneWidget); + + final Key avatarA = new Key('A'); + await tester.pumpWidget(buildFrame( + currentAccountPicture: new CircleAvatar(key: avatarA, child: new Text('A')), + accountName: new Text('accountName'), + )); + expect( + tester.getBottomLeft(find.byKey(avatarA)).x, + tester.getBottomLeft(find.text('accountName')).x + ); + expect( + tester.getBottomLeft(find.text('accountName')).y, + greaterThan(tester.getBottomLeft(find.byKey(avatarA)).y) + ); + }); }