Commit d05c7f62 authored by Hans Muller's avatar Hans Muller Committed by GitHub

UserAccountsDrawerHeader gallery demo, etc (#7297)

parent c9c577ae
...@@ -12,6 +12,7 @@ export 'colors_demo.dart'; ...@@ -12,6 +12,7 @@ export 'colors_demo.dart';
export 'data_table_demo.dart'; export 'data_table_demo.dart';
export 'date_and_time_picker_demo.dart'; export 'date_and_time_picker_demo.dart';
export 'dialog_demo.dart'; export 'dialog_demo.dart';
export 'drawer_demo.dart';
export 'expansion_panels_demo.dart'; export 'expansion_panels_demo.dart';
export 'grid_list_demo.dart'; export 'grid_list_demo.dart';
export 'icons_demo.dart'; export 'icons_demo.dart';
......
// 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<DrawerDemo> with TickerProviderStateMixin {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
static const List<String> _drawerContents = const <String>[
'A', 'B', 'C', 'D', 'E',
];
AnimationController _controller;
Animation<double> _drawerContentsOpacity;
Animation<FractionalOffset> _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<FractionalOffset>(
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: <Widget>[
new UserAccountsDrawerHeader(
accountName: new Text('Zach Widget'),
accountEmail: new Text('zach.widget@example.com'),
currentAccountPicture: new CircleAvatar(backgroundImage: new AssetImage(_kAsset0)),
otherAccountsPictures: <Widget>[
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: <Widget>[
// 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: <Widget>[
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: <Widget>[
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,
),
),
],
),
),
),
);
}
}
...@@ -102,6 +102,12 @@ final List<GalleryItem> kAllGalleryItems = <GalleryItem>[ ...@@ -102,6 +102,12 @@ final List<GalleryItem> kAllGalleryItems = <GalleryItem>[
routeName: DialogDemo.routeName, routeName: DialogDemo.routeName,
buildRoute: (BuildContext context) => new DialogDemo() 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( new GalleryItem(
title: 'Expand/collapse list control', title: 'Expand/collapse list control',
subtitle: 'List with one level of sublists', subtitle: 'List with one level of sublists',
......
...@@ -33,6 +33,7 @@ final List<String> demoTitles = <String>[ ...@@ -33,6 +33,7 @@ final List<String> demoTitles = <String>[
'Chips', 'Chips',
'Date and time pickers', 'Date and time pickers',
'Dialog', 'Dialog',
'Drawer',
'Expand/collapse list control', 'Expand/collapse list control',
'Expansion panels', 'Expansion panels',
'Floating action button', 'Floating action button',
......
...@@ -3,25 +3,128 @@ ...@@ -3,25 +3,128 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'debug.dart'; import 'debug.dart';
class _AccountPictures extends StatelessWidget {
_AccountPictures({
Key key,
this.currentAccountPicture,
this.otherAccountsPictures,
}) : super(key: key);
final Widget currentAccountPicture;
final List<Widget> otherAccountsPictures;
@override
Widget build(BuildContext context) {
return new Stack(
children: <Widget>[
new Positioned(
top: 0.0,
right: 0.0,
child: new Row(
children: (otherAccountsPictures ?? <Widget>[]).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 ? <Widget>[icon] : <Widget>[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)
? <Widget>[accountNameLine, accountEmailLine]
: <Widget>[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. /// 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. /// Requires one of its ancestors to be a [Material] widget.
/// ///
/// See also: /// See also:
/// ///
/// * [Drawer]
/// * [DrawerHeader], for a drawer header that doesn't show user acounts /// * [DrawerHeader], for a drawer header that doesn't show user acounts
/// * <https://material.google.com/patterns/navigation-drawer.html> /// * <https://material.google.com/patterns/navigation-drawer.html>
class UserAccountsDrawerHeader extends StatefulWidget { class UserAccountsDrawerHeader extends StatefulWidget {
...@@ -38,30 +141,31 @@ class UserAccountsDrawerHeader extends StatefulWidget { ...@@ -38,30 +141,31 @@ class UserAccountsDrawerHeader extends StatefulWidget {
this.onDetailsPressed this.onDetailsPressed
}) : super(key: key); }) : super(key: key);
/// A callback that gets called when the account name/email/dropdown /// The header's background. If decoration is null then a [BoxDecoration]
/// section is pressed. /// with its background color set to the current theme's primaryColor is used.
final VoidCallback onDetailsPressed;
/// The background to show in the drawer header.
final Decoration decoration; final Decoration decoration;
/// A widget placed in the upper-left corner representing the current /// A widget placed in the upper-left corner that represents the current
/// account picture. Normally a [CircleAvatar]. /// user's account. Normally a [CircleAvatar].
final Widget currentAccountPicture; final Widget currentAccountPicture;
/// A list of widgets that represent the user's accounts. Up to three of will /// A list of widgets that represent the current user's other accounts.
/// be arranged in a row in the header's upper-right corner. Normally a list /// Up to three of these widgets will be arranged in a row in the header's
/// of [CircleAvatar] widgets. /// upper-right corner. Normally a list of [CircleAvatar] widgets.
final List<Widget> otherAccountsPictures; final List<Widget> otherAccountsPictures;
/// A widget placed on the top row of the account details representing the /// A widget that represents the user's current account name. It is
/// account's name. /// displayed on the left, below the [currentAccountPicture].
final Widget accountName; final Widget accountName;
/// A widget placed on the bottom row of the account details representing the /// A widget that represents the email address of the user's current account.
/// account's e-mail address. /// It is displayed on the left, below the [accountName].
final Widget accountEmail; final Widget accountEmail;
/// A callback that is called when the horizontal area which contains the
/// [accountName] and [accountEmail] is tapped.
final VoidCallback onDetailsPressed;
@override @override
_UserAccountsDrawerHeaderState createState() => new _UserAccountsDrawerHeaderState(); _UserAccountsDrawerHeaderState createState() => new _UserAccountsDrawerHeaderState();
} }
...@@ -72,89 +176,32 @@ class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> { ...@@ -72,89 +176,32 @@ class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
final List<Widget> otherAccountsPictures = config.otherAccountsPictures ?? <Widget>[];
return new DrawerHeader( return new DrawerHeader(
decoration: config.decoration, decoration: config.decoration ?? new BoxDecoration(
backgroundColor: Theme.of(context).primaryColor,
),
child: new Column( child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Expanded( new Expanded(
child: new Stack( child: new _AccountPictures(
children: <Widget>[ currentAccountPicture: config.currentAccountPicture,
new Positioned( otherAccountsPictures: config.otherAccountsPictures,
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( new _AccountDetails(
height: 56.0, accountName: config.accountName,
child: new InkWell( accountEmail: config.accountEmail,
onTap: () { isOpen: _isOpen,
onTap: config.onDetailsPressed == null ? null : () {
setState(() { setState(() {
_isOpen = !_isOpen; _isOpen = !_isOpen;
}); });
if (config.onDetailsPressed != null)
config.onDetailsPressed(); config.onDetailsPressed();
}, },
child: new Container(
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new DefaultTextStyle(
style: const TextStyle(
fontWeight: FontWeight.w500,
color: Colors.white
),
child: config.accountName
), ),
new Row( ],
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
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
)
)
)
]
)
]
)
)
)
)
]
)
); );
} }
} }
...@@ -71,4 +71,93 @@ void main() { ...@@ -71,4 +71,93 @@ void main() {
expect(avatarDTopRight.y - topRight.y, equals(16.0)); expect(avatarDTopRight.y - topRight.y, equals(16.0));
expect(avatarDTopRight.x - avatarCTopRight.x, equals(40.0 + 16.0)); // size + space between 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<Widget> 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: <Widget>[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)
);
});
} }
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