user_accounts_drawer_header.dart 10.6 KB
Newer Older
1 2 3 4
// 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.

5
import 'package:flutter/widgets.dart';
6

7
import 'colors.dart';
8
import 'debug.dart';
9 10 11
import 'drawer_header.dart';
import 'icons.dart';
import 'ink_well.dart';
12
import 'material_localizations.dart';
13
import 'theme.dart';
14

15
class _AccountPictures extends StatelessWidget {
16
  const _AccountPictures({
17 18 19 20 21 22 23 24 25 26 27 28
    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>[
29
        new PositionedDirectional(
30
          top: 0.0,
31
          end: 0.0,
32 33
          child: new Row(
            children: (otherAccountsPictures ?? <Widget>[]).take(3).map((Widget picture) {
34 35 36 37 38 39 40 41
              return new Semantics(
                explicitChildNodes: true,
                child: new Container(
                  margin: const EdgeInsetsDirectional.only(start: 16.0),
                  width: 40.0,
                  height: 40.0,
                  child: picture
                ),
42 43 44 45 46 47
              );
            }).toList(),
          ),
        ),
        new Positioned(
          top: 0.0,
48 49 50 51 52 53 54
          child: new Semantics(
            explicitChildNodes: true,
            child: new SizedBox(
              width: 72.0,
              height: 72.0,
              child: currentAccountPicture
            ),
55 56 57 58 59 60 61 62
          ),
        ),
      ],
    );
  }
}

class _AccountDetails extends StatelessWidget {
63
  const _AccountDetails({
64
    Key key,
65 66
    @required this.accountName,
    @required this.accountEmail,
67 68 69 70 71 72 73 74 75 76 77
    this.onTap,
    this.isOpen,
  }) : super(key: key);

  final Widget accountName;
  final Widget accountEmail;
  final VoidCallback onTap;
  final bool isOpen;

  @override
  Widget build(BuildContext context) {
78 79
    assert(debugCheckHasDirectionality(context));

80
    final ThemeData theme = Theme.of(context);
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
    final List<Widget> children = <Widget>[];

    if (accountName != null) {
      final Widget accountNameLine = new LayoutId(
        id: _AccountDetailsLayout.accountName,
        child: new Padding(
          padding: const EdgeInsets.symmetric(vertical: 2.0),
          child: new DefaultTextStyle(
            style: theme.primaryTextTheme.body2,
            overflow: TextOverflow.ellipsis,
            child: accountName,
          ),
        ),
      );
      children.add(accountNameLine);
96
    }
97

98 99 100 101 102 103 104 105 106 107
    if (accountEmail != null) {
      final Widget accountEmailLine = new LayoutId(
        id: _AccountDetailsLayout.accountEmail,
        child: new Padding(
          padding: const EdgeInsets.symmetric(vertical: 2.0),
          child: new DefaultTextStyle(
            style: theme.primaryTextTheme.body1,
            overflow: TextOverflow.ellipsis,
            child: accountEmail,
          ),
108 109
        ),
      );
110
      children.add(accountEmailLine);
111 112
    }

113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
    if (onTap != null) {
      final MaterialLocalizations localizations = MaterialLocalizations.of(context);
      final Widget dropDownIcon = new LayoutId(
        id: _AccountDetailsLayout.dropdownIcon,
        child: new Semantics(
          container: true,
          button: true,
          onTap: onTap,
          child: new SizedBox(
            height: _kAccountDetailsHeight,
            width: _kAccountDetailsHeight,
            child: new Center(
              child: new Icon(
                isOpen ? Icons.arrow_drop_up : Icons.arrow_drop_down,
                color: Colors.white,
                semanticLabel: isOpen
                    ? localizations.hideAccountsLabel
                    : localizations.showAccountsLabel,
              ),
            ),
          ),
        ),
      );
      children.add(dropDownIcon);
    }

    Widget accountDetails = new CustomMultiChildLayout(
      delegate: new _AccountDetailsLayout(
        textDirection: Directionality.of(context),
      ),
      children: children,
    );

    if (onTap != null) {
      accountDetails = new InkWell(
        onTap: onTap,
        child: accountDetails,
        excludeFromSemantics: true,
      );
    }
153 154

    return new SizedBox(
155
      height: _kAccountDetailsHeight,
156
      child: accountDetails,
157 158 159 160
    );
  }
}

161 162 163 164 165 166
const double _kAccountDetailsHeight = 56.0;

class _AccountDetailsLayout extends MultiChildLayoutDelegate {

  _AccountDetailsLayout({ @required this.textDirection });

167 168 169
  static const String accountName = 'accountName';
  static const String accountEmail = 'accountEmail';
  static const String dropdownIcon = 'dropdownIcon';
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239

  final TextDirection textDirection;

  @override
  void performLayout(Size size) {
    Size iconSize;
    if (hasChild(dropdownIcon)) {
      // place the dropdown icon in bottom right (LTR) or bottom left (RTL)
      iconSize = layoutChild(dropdownIcon, new BoxConstraints.loose(size));
      positionChild(dropdownIcon, _offsetForIcon(size, iconSize));
    }

    final String bottomLine = hasChild(accountEmail) ? accountEmail : (hasChild(accountName) ? accountName : null);

    if (bottomLine != null) {
      final Size constraintSize = iconSize == null ? size : size - new Offset(iconSize.width, 0.0);
      iconSize ??= const Size(_kAccountDetailsHeight, _kAccountDetailsHeight);

      // place bottom line center at same height as icon center
      final Size bottomLineSize = layoutChild(bottomLine, new BoxConstraints.loose(constraintSize));
      final Offset bottomLineOffset = _offsetForBottomLine(size, iconSize, bottomLineSize);
      positionChild(bottomLine, bottomLineOffset);

      // place account name above account email
      if (bottomLine == accountEmail && hasChild(accountName)) {
        final Size nameSize = layoutChild(accountName, new BoxConstraints.loose(constraintSize));
        positionChild(accountName, _offsetForName(size, nameSize, bottomLineOffset));
      }
    }
  }

  @override
  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true;

  Offset _offsetForIcon(Size size, Size iconSize) {
    switch (textDirection) {
      case TextDirection.ltr:
        return new Offset(size.width - iconSize.width, size.height - iconSize.height);
      case TextDirection.rtl:
        return new Offset(0.0, size.height - iconSize.height);
    }
    assert(false, 'Unreachable');
    return null;
  }

  Offset _offsetForBottomLine(Size size, Size iconSize, Size bottomLineSize) {
    final double y = size.height - 0.5 * iconSize.height - 0.5 * bottomLineSize.height;
    switch (textDirection) {
      case TextDirection.ltr:
        return new Offset(0.0, y);
      case TextDirection.rtl:
        return new Offset(size.width - bottomLineSize.width, y);
    }
    assert(false, 'Unreachable');
    return null;
  }

  Offset _offsetForName(Size size, Size nameSize, Offset bottomLineOffset) {
    final double y = bottomLineOffset.dy - nameSize.height;
    switch (textDirection) {
      case TextDirection.ltr:
        return new Offset(0.0, y);
      case TextDirection.rtl:
        return new Offset(size.width - nameSize.width, y);
    }
    assert(false, 'Unreachable');
    return null;
  }
}

240 241 242 243 244 245
/// A material design [Drawer] header that identifies the app's user.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
246
///  * [DrawerHeader], for a drawer header that doesn't show user accounts
247
///  * <https://material.google.com/patterns/navigation-drawer.html>
248 249 250 251
class UserAccountsDrawerHeader extends StatefulWidget {
  /// Creates a material design drawer header.
  ///
  /// Requires one of its ancestors to be a [Material] widget.
252
  const UserAccountsDrawerHeader({
253 254
    Key key,
    this.decoration,
255
    this.margin = const EdgeInsets.only(bottom: 8.0),
256 257
    this.currentAccountPicture,
    this.otherAccountsPictures,
258 259
    @required this.accountName,
    @required this.accountEmail,
260
    this.onDetailsPressed
261 262
  }) : super(key: key);

263 264
  /// The header's background. If decoration is null then a [BoxDecoration]
  /// with its background color set to the current theme's primaryColor is used.
265
  final Decoration decoration;
266

267
  /// The margin around the drawer header.
268
  final EdgeInsetsGeometry margin;
269

270 271
  /// A widget placed in the upper-left corner that represents the current
  /// user's account. Normally a [CircleAvatar].
272 273
  final Widget currentAccountPicture;

274 275 276
  /// 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.
277 278
  final List<Widget> otherAccountsPictures;

279 280
  /// A widget that represents the user's current account name. It is
  /// displayed on the left, below the [currentAccountPicture].
281 282
  final Widget accountName;

283 284
  /// A widget that represents the email address of the user's current account.
  /// It is displayed on the left, below the [accountName].
285 286
  final Widget accountEmail;

287 288 289 290
  /// A callback that is called when the horizontal area which contains the
  /// [accountName] and [accountEmail] is tapped.
  final VoidCallback onDetailsPressed;

291
  @override
292
  _UserAccountsDrawerHeaderState createState() => new _UserAccountsDrawerHeaderState();
293 294 295
}

class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
296
  bool _isOpen = false;
297

298 299 300 301 302 303 304
  void _handleDetailsPressed() {
    setState(() {
      _isOpen = !_isOpen;
    });
    widget.onDetailsPressed();
  }

305 306 307
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
    return new Semantics(
      container: true,
      label: MaterialLocalizations.of(context).signedInLabel,
      child: new DrawerHeader(
        decoration: widget.decoration ?? new BoxDecoration(
          color: Theme.of(context).primaryColor,
        ),
        margin: widget.margin,
        padding: const EdgeInsetsDirectional.only(top: 16.0, start: 16.0),
        child: new SafeArea(
          bottom: false,
          child: new Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              new Expanded(
                child: new Padding(
                  padding: const EdgeInsetsDirectional.only(end: 16.0),
                  child: new _AccountPictures(
                    currentAccountPicture: widget.currentAccountPicture,
                    otherAccountsPictures: widget.otherAccountsPictures,
                  ),
                )
              ),
              new _AccountDetails(
                accountName: widget.accountName,
                accountEmail: widget.accountEmail,
                isOpen: _isOpen,
                onTap: widget.onDetailsPressed == null ? null : _handleDetailsPressed,
              ),
            ],
          ),
339
        ),
340
      ),
341 342 343
    );
  }
}