diff --git a/dev/manual_tests/card_collection.dart b/dev/manual_tests/card_collection.dart index ba057c6d71..8946fdc503 100644 --- a/dev/manual_tests/card_collection.dart +++ b/dev/manual_tests/card_collection.dart @@ -126,7 +126,7 @@ class CardCollectionState extends State { child: new IconTheme( data: const IconThemeData(color: Colors.black), child: new Block(children: [ - new DrawerHeader(content: new Center(child: new Text('Options'))), + new DrawerHeader(child: new Center(child: new Text('Options'))), buildDrawerCheckbox("Make card labels editable", _editable, _toggleEditable), buildDrawerCheckbox("Snap fling scrolls to center", _snapToCenter, _toggleSnapToCenter), buildDrawerCheckbox("Fixed size cards", _fixedSizeCards, _toggleFixedSizeCards), diff --git a/dev/manual_tests/pageable_list.dart b/dev/manual_tests/pageable_list.dart index 2a436fbb40..f6fba19c6e 100644 --- a/dev/manual_tests/pageable_list.dart +++ b/dev/manual_tests/pageable_list.dart @@ -85,7 +85,7 @@ class PageableListAppState extends State { Widget _buildDrawer() { return new Drawer( child: new Block(children: [ - new DrawerHeader(content: new Center(child: new Text('Options'))), + new DrawerHeader(child: new Center(child: new Text('Options'))), new DrawerItem( icon: new Icon(Icons.more_horiz), selected: scrollDirection == Axis.horizontal, diff --git a/examples/flutter_gallery/lib/demo/pesto_demo.dart b/examples/flutter_gallery/lib/demo/pesto_demo.dart index 14717e03c0..b024c4d5fc 100644 --- a/examples/flutter_gallery/lib/demo/pesto_demo.dart +++ b/examples/flutter_gallery/lib/demo/pesto_demo.dart @@ -121,7 +121,8 @@ class _PestoDemoState extends State { child: new Block( children: [ new DrawerHeader( - content: new Column( + child: new Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ new Container( decoration: new BoxDecoration( @@ -131,7 +132,7 @@ class _PestoDemoState extends State { width: 72.0, height: 72.0, padding: const EdgeInsets.all(2.0), - margin: const EdgeInsets.only(bottom: 8.0), + margin: const EdgeInsets.only(bottom: 16.0), child: new ClipOval( child: new Image( image: new AssetImage(_kUserImage), diff --git a/examples/flutter_gallery/lib/gallery/drawer.dart b/examples/flutter_gallery/lib/gallery/drawer.dart index e157b4ef03..d0e628f1aa 100644 --- a/examples/flutter_gallery/lib/gallery/drawer.dart +++ b/examples/flutter_gallery/lib/gallery/drawer.dart @@ -49,7 +49,7 @@ class GalleryDrawer extends StatelessWidget { child: new Block( children: [ new DrawerHeader( - content: new Center(child: new Text('Flutter gallery')) + child: new Center(child: new Text('Flutter gallery')) ), new DrawerItem( icon: new Icon(Icons.brightness_5), diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index ea5c9c1025..6844259d64 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -124,7 +124,7 @@ class StockHomeState extends State { Widget _buildDrawer(BuildContext context) { return new Drawer( child: new Block(children: [ - new DrawerHeader(content: new Center(child: new Text('Stocks'))), + new DrawerHeader(child: new Center(child: new Text('Stocks'))), new DrawerItem( icon: new Icon(Icons.assessment), selected: true, diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index f69ffd00a1..b5b98050e2 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -73,5 +73,6 @@ export 'src/material/toggleable.dart'; export 'src/material/tooltip.dart'; export 'src/material/two_level_list.dart'; export 'src/material/typography.dart'; +export 'src/material/user_accounts_drawer_header.dart'; export 'widgets.dart'; diff --git a/packages/flutter/lib/src/material/drawer_header.dart b/packages/flutter/lib/src/material/drawer_header.dart index dba108736e..24fab981ee 100644 --- a/packages/flutter/lib/src/material/drawer_header.dart +++ b/packages/flutter/lib/src/material/drawer_header.dart @@ -7,11 +7,11 @@ import 'package:flutter/widgets.dart'; import 'debug.dart'; import 'theme.dart'; -const double _kDrawerHeaderHeight = 140.0; +const double _kDrawerHeaderHeight = 160.0 + 1.0; // bottom edge -/// The top-most region of a material design drawer. The header's [background] -/// widget extends behind the system status bar and its [content] widget is -/// stacked on top of the background and below the status bar. +/// The top-most region of a material design drawer. The header's [child] +/// widget is placed inside of a [Container] whose [decoration] can be passed as +/// an argument. /// /// Part of the material design [Drawer]. /// @@ -22,19 +22,24 @@ const double _kDrawerHeaderHeight = 140.0; /// * [Drawer] /// * [DrawerItem] /// * + class DrawerHeader extends StatelessWidget { /// Creates a material design drawer header. /// /// Requires one of its ancestors to be a [Material] widget. - const DrawerHeader({ Key key, this.background, this.content }) : super(key: key); + const DrawerHeader({ + Key key, + this.decoration, + this.child + }) : super(key: key); - /// A widget that extends behind the system status bar and is stacked - /// behind the [content] widget. - final Widget background; + /// Decoration for the main drawer header [Container]; useful for applying + /// backgrounds. + final BoxDecoration decoration; - /// A widget that's positioned below the status bar and stacked on top of the - /// [background] widget. Typically a view of the user's id. - final Widget content; + /// A widget that extends behind the system status bar and is placed inside a + /// [Container]. + final Widget child; @override Widget build(BuildContext context) { @@ -42,7 +47,7 @@ class DrawerHeader extends StatelessWidget { final double statusBarHeight = MediaQuery.of(context).padding.top; return new Container( height: statusBarHeight + _kDrawerHeaderHeight, - margin: const EdgeInsets.only(bottom: 7.0), // 8 less 1 for the bottom border. + margin: const EdgeInsets.only(bottom: 8.0), decoration: new BoxDecoration( border: const Border( bottom: const BorderSide( @@ -51,20 +56,18 @@ class DrawerHeader extends StatelessWidget { ) ) ), - child: new Stack( - children: [ - background ?? new Container(), - new Positioned( - top: statusBarHeight + 16.0, - left: 16.0, - right: 16.0, - bottom: 8.0, - child: new DefaultTextStyle( - style: Theme.of(context).textTheme.body2, - child: content - ) - ) - ] + child: new Container( + padding: new EdgeInsets.only( + top: 16.0 + statusBarHeight, + left: 16.0, + right: 16.0, + bottom: 8.0 + ), + decoration: decoration, + child: new DefaultTextStyle( + style: Theme.of(context).textTheme.body2, + child: child + ) ) ); } diff --git a/packages/flutter/lib/src/material/user_accounts_drawer_header.dart b/packages/flutter/lib/src/material/user_accounts_drawer_header.dart new file mode 100644 index 0000000000..f2ab14b525 --- /dev/null +++ b/packages/flutter/lib/src/material/user_accounts_drawer_header.dart @@ -0,0 +1,166 @@ +// 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:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import 'debug.dart'; + +/// 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] +/// * [DrawerItem] +/// * + +class UserAccountsDrawerHeader extends StatefulWidget { + /// Creates a material design drawer header. + /// + /// Requires one of its ancestors to be a [Material] widget. + UserAccountsDrawerHeader({ + Key key, + this.decoration, + this.currentAccountPicture, + this.otherAccountsPictures, + this.accountName, + this.accountEmail, + this.onDetailsPressed + }) : super(key: key); + + /// A callback that gets called when the account name/email/dropdown + /// section is pressed. + final VoidCallback onDetailsPressed; + + /// Decoration for the main drawer header container useful for applying + /// backgrounds. + final BoxDecoration decoration; + + /// A widget placed in the upper-left corner representing the current + /// account picture. Normally a [CircleAvatar]. + final Widget currentAccountPicture; + + /// A list of widgets that represent the user's accounts. Up to three of them + /// are 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 + /// account name. + final Widget accountName; + + /// A widget placed on the bottom row of the account details representing + /// account email. + final Widget accountEmail; + + @override + _UserAccountsDrawerHeaderState createState() => + new _UserAccountsDrawerHeaderState(); +} + +class _UserAccountsDrawerHeaderState extends State { + /// Saves whether the account dropdown is open or not. + bool isOpen = false; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + final List otherAccountsPictures = config.otherAccountsPictures ?? + []; + return new DrawerHeader( + decoration: config.decoration, + child: new Column( + children: [ + new Flexible( + 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 + ) + ) + ] + ) + ), + 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 Flexible( + child: new Align( + alignment: FractionalOffset.centerRight, + child: new Icon( + isOpen ? Icons.arrow_drop_up : + Icons.arrow_drop_down, + color: Colors.white + ) + ) + ) + ] + ) + ] + ) + ) + ) + ) + ] + ) + ); + } +} diff --git a/packages/flutter/test/material/drawer_test.dart b/packages/flutter/test/material/drawer_test.dart index c3625f1e05..4b4cd6f09d 100644 --- a/packages/flutter/test/material/drawer_test.dart +++ b/packages/flutter/test/material/drawer_test.dart @@ -7,13 +7,18 @@ import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Drawer control test', (WidgetTester tester) async { + final Key containerKey = new Key('container'); + await tester.pumpWidget( new Scaffold( drawer: new Drawer( child: new Block( children: [ new DrawerHeader( - content: new Text('header') + child: new Container( + key: containerKey, + child: new Text('header') + ) ), new DrawerItem( icon: new Icon(Icons.archive), @@ -28,8 +33,21 @@ void main() { expect(find.text('Archive'), findsNothing); ScaffoldState state = tester.firstState(find.byType(Scaffold)); state.openDrawer(); + await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('Archive'), findsOneWidget); + + RenderBox box = tester.renderObject(find.byType(DrawerHeader)); + expect(box.size.height, equals(160.0 + 8.0 + 1.0)); // height + bottom margin + bottom edge + + final double drawerWidth = box.size.width; + final double drawerHeight = box.size.height; + + box = tester.renderObject(find.byKey(containerKey)); + expect(box.size.width, equals(drawerWidth - 2 * 16.0)); + expect(box.size.height, equals(drawerHeight - 2 * 16.0 - 1.0)); // bottom edge + + expect(find.text('header'), findsOneWidget); }); } diff --git a/packages/flutter/test/material/user_accounts_drawer_header_test.dart b/packages/flutter/test/material/user_accounts_drawer_header_test.dart new file mode 100644 index 0000000000..0c58560e55 --- /dev/null +++ b/packages/flutter/test/material/user_accounts_drawer_header_test.dart @@ -0,0 +1,74 @@ +// 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'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('UserAccuntsDrawerHeader test', (WidgetTester tester) async { + final Key avatarA = new Key('A'); + final Key avatarC = new Key('C'); + final Key avatarD = new Key('D'); + + await tester.pumpWidget( + new Material( + child: new UserAccountsDrawerHeader( + currentAccountPicture: new CircleAvatar( + key: avatarA, + child: new Text('A') + ), + otherAccountsPictures: [ + new CircleAvatar( + child: new Text('B') + ), + new CircleAvatar( + key: avatarC, + child: new Text('C') + ), + new CircleAvatar( + key: avatarD, + child: new Text('D') + ), + new CircleAvatar( + child: new Text('E') + ) + ], + accountName: new Text("name"), + accountEmail: new Text("email") + ) + ) + ); + + expect(find.text('A'), findsOneWidget); + expect(find.text('B'), findsOneWidget); + expect(find.text('C'), findsOneWidget); + expect(find.text('D'), findsOneWidget); + expect(find.text('E'), findsNothing); + + expect(find.text('name'), findsOneWidget); + expect(find.text('email'), findsOneWidget); + + RenderBox box = tester.renderObject(find.byKey(avatarA)); + expect(box.size.width, equals(72.0)); + expect(box.size.height, equals(72.0)); + + box = tester.renderObject(find.byKey(avatarC)); + expect(box.size.width, equals(40.0)); + expect(box.size.height, equals(40.0)); + + Point topLeft = tester.getTopLeft(find.byType(UserAccountsDrawerHeader)); + Point topRight = tester.getTopRight(find.byType(UserAccountsDrawerHeader)); + + Point avatarATopLeft = tester.getTopLeft(find.byKey(avatarA)); + Point avatarDTopRight = tester.getTopRight(find.byKey(avatarD)); + Point avatarCTopRight = tester.getTopRight(find.byKey(avatarC)); + + expect(avatarATopLeft.x - topLeft.x, equals(16.0)); + expect(avatarATopLeft.y - topLeft.y, equals(16.0)); + expect(topRight.x - avatarDTopRight.x, equals(16.0)); + expect(avatarDTopRight.y - topRight.y, equals(16.0)); + expect(avatarDTopRight.x - avatarCTopRight.x, equals(40.0 + 16.0)); // size + space between + }); +}