user_accounts_drawer_header.dart 12.3 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 6
import 'dart:math' as math;

7
import 'package:flutter/widgets.dart';
8
import 'package:flutter/foundation.dart';
9

10
import 'colors.dart';
11
import 'debug.dart';
12 13 14
import 'drawer_header.dart';
import 'icons.dart';
import 'ink_well.dart';
15
import 'material_localizations.dart';
16
import 'theme.dart';
17

18
class _AccountPictures extends StatelessWidget {
19
  const _AccountPictures({
20 21 22 23 24 25 26 27 28 29
    Key key,
    this.currentAccountPicture,
    this.otherAccountsPictures,
  }) : super(key: key);

  final Widget currentAccountPicture;
  final List<Widget> otherAccountsPictures;

  @override
  Widget build(BuildContext context) {
30
    return Stack(
31
      children: <Widget>[
32
        PositionedDirectional(
33
          top: 0.0,
34
          end: 0.0,
35
          child: Row(
36
            children: (otherAccountsPictures ?? <Widget>[]).take(3).map<Widget>((Widget picture) {
37
              return Padding(
38
                padding: const EdgeInsetsDirectional.only(start: 8.0),
39
                child: Semantics(
40
                  container: true,
41
                  child: Container(
42 43 44
                  padding: const EdgeInsets.only(left: 8.0, bottom: 8.0),
                    width: 48.0,
                    height: 48.0,
45
                    child: picture,
46
                 ),
47
                ),
48 49 50 51
              );
            }).toList(),
          ),
        ),
52
        Positioned(
53
          top: 0.0,
54
          child: Semantics(
55
            explicitChildNodes: true,
56
            child: SizedBox(
57 58
              width: 72.0,
              height: 72.0,
59
              child: currentAccountPicture,
60
            ),
61 62 63 64 65 66 67
          ),
        ),
      ],
    );
  }
}

68
class _AccountDetails extends StatefulWidget {
69
  const _AccountDetails({
70
    Key key,
71 72
    @required this.accountName,
    @required this.accountEmail,
73 74
    this.onTap,
    this.isOpen,
75
    this.arrowColor,
76 77 78 79 80 81
  }) : super(key: key);

  final Widget accountName;
  final Widget accountEmail;
  final VoidCallback onTap;
  final bool isOpen;
82
  final Color arrowColor;
83

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
  @override
  _AccountDetailsState createState() => _AccountDetailsState();
}

class _AccountDetailsState extends State<_AccountDetails> with SingleTickerProviderStateMixin {
  Animation<double> _animation;
  AnimationController _controller;
  @override
  void initState () {
    super.initState();
    _controller = AnimationController(
      value: widget.isOpen ? 1.0 : 0.0,
      duration: const Duration(milliseconds: 200),
      vsync: this,
    );
    _animation = CurvedAnimation(
      parent: _controller,
      curve: Curves.fastOutSlowIn,
      reverseCurve: Curves.fastOutSlowIn.flipped,
    )
      ..addListener(() => setState(() {
        // [animation]'s value has changed here.
      }));
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  void didUpdateWidget (_AccountDetails oldWidget) {
    super.didUpdateWidget(oldWidget);
118 119 120 121 122 123
    // If the state of the arrow did not change, there is no need to trigger the animation
    if (oldWidget.isOpen == widget.isOpen) {
      return;
    }

    if (widget.isOpen) {
124 125 126 127 128 129
      _controller.forward();
    } else {
      _controller.reverse();
    }
  }

130 131
  @override
  Widget build(BuildContext context) {
132
    assert(debugCheckHasDirectionality(context));
133
    assert(debugCheckHasMaterialLocalizations(context));
134
    assert(debugCheckHasMaterialLocalizations(context));
135

136
    final ThemeData theme = Theme.of(context);
137
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
138

139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
    Widget accountDetails = CustomMultiChildLayout(
      delegate: _AccountDetailsLayout(
        textDirection: Directionality.of(context),
      ),
      children: <Widget>[
        if (widget.accountName != null)
          LayoutId(
            id: _AccountDetailsLayout.accountName,
            child: Padding(
              padding: const EdgeInsets.symmetric(vertical: 2.0),
              child: DefaultTextStyle(
                style: theme.primaryTextTheme.body2,
                overflow: TextOverflow.ellipsis,
                child: widget.accountName,
              ),
            ),
155
          ),
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
        if (widget.accountEmail != null)
          LayoutId(
            id: _AccountDetailsLayout.accountEmail,
            child: Padding(
              padding: const EdgeInsets.symmetric(vertical: 2.0),
              child: DefaultTextStyle(
                style: theme.primaryTextTheme.body1,
                overflow: TextOverflow.ellipsis,
                child: widget.accountEmail,
              ),
            ),
          ),
        if (widget.onTap != null)
          LayoutId(
            id: _AccountDetailsLayout.dropdownIcon,
            child: Semantics(
              container: true,
              button: true,
              onTap: widget.onTap,
              child: SizedBox(
                height: _kAccountDetailsHeight,
                width: _kAccountDetailsHeight,
                child: Center(
                  child: Transform.rotate(
                    angle: _animation.value * math.pi,
                    child: Icon(
                      Icons.arrow_drop_down,
                      color: widget.arrowColor,
                      semanticLabel: widget.isOpen
                        ? localizations.hideAccountsLabel
                        : localizations.showAccountsLabel,
                    ),
                  ),
189
                ),
190 191 192
              ),
            ),
          ),
193
      ],
194 195
    );

196
    if (widget.onTap != null) {
197
      accountDetails = InkWell(
198
        onTap: widget.onTap,
199 200 201 202
        child: accountDetails,
        excludeFromSemantics: true,
      );
    }
203

204
    return SizedBox(
205
      height: _kAccountDetailsHeight,
206
      child: accountDetails,
207 208 209 210
    );
  }
}

211 212 213 214 215 216
const double _kAccountDetailsHeight = 56.0;

class _AccountDetailsLayout extends MultiChildLayoutDelegate {

  _AccountDetailsLayout({ @required this.textDirection });

217 218 219
  static const String accountName = 'accountName';
  static const String accountEmail = 'accountEmail';
  static const String dropdownIcon = 'dropdownIcon';
220 221 222 223 224 225 226 227

  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)
228
      iconSize = layoutChild(dropdownIcon, BoxConstraints.loose(size));
229 230 231 232 233 234
      positionChild(dropdownIcon, _offsetForIcon(size, iconSize));
    }

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

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

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

      // place account name above account email
      if (bottomLine == accountEmail && hasChild(accountName)) {
245
        final Size nameSize = layoutChild(accountName, BoxConstraints.loose(constraintSize));
246 247 248 249 250 251 252 253 254 255 256
        positionChild(accountName, _offsetForName(size, nameSize, bottomLineOffset));
      }
    }
  }

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

  Offset _offsetForIcon(Size size, Size iconSize) {
    switch (textDirection) {
      case TextDirection.ltr:
257
        return Offset(size.width - iconSize.width, size.height - iconSize.height);
258
      case TextDirection.rtl:
259
        return Offset(0.0, size.height - iconSize.height);
260 261 262 263 264 265 266 267 268
    }
    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:
269
        return Offset(0.0, y);
270
      case TextDirection.rtl:
271
        return Offset(size.width - bottomLineSize.width, y);
272 273 274 275 276 277 278 279 280
    }
    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:
281
        return Offset(0.0, y);
282
      case TextDirection.rtl:
283
        return Offset(size.width - nameSize.width, y);
284 285 286 287 288 289
    }
    assert(false, 'Unreachable');
    return null;
  }
}

290 291 292 293 294 295
/// A material design [Drawer] header that identifies the app's user.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
296
///  * [DrawerHeader], for a drawer header that doesn't show user accounts.
297
///  * <https://material.io/design/components/navigation-drawer.html#anatomy>
298 299 300 301
class UserAccountsDrawerHeader extends StatefulWidget {
  /// Creates a material design drawer header.
  ///
  /// Requires one of its ancestors to be a [Material] widget.
302
  const UserAccountsDrawerHeader({
303 304
    Key key,
    this.decoration,
305
    this.margin = const EdgeInsets.only(bottom: 8.0),
306 307
    this.currentAccountPicture,
    this.otherAccountsPictures,
308 309
    @required this.accountName,
    @required this.accountEmail,
310
    this.onDetailsPressed,
311
    this.arrowColor = Colors.white,
312 313
  }) : super(key: key);

314 315
  /// The header's background. If decoration is null then a [BoxDecoration]
  /// with its background color set to the current theme's primaryColor is used.
316
  final Decoration decoration;
317

318
  /// The margin around the drawer header.
319
  final EdgeInsetsGeometry margin;
320

321 322
  /// A widget placed in the upper-left corner that represents the current
  /// user's account. Normally a [CircleAvatar].
323 324
  final Widget currentAccountPicture;

325 326 327
  /// 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.
328 329
  final List<Widget> otherAccountsPictures;

330 331
  /// A widget that represents the user's current account name. It is
  /// displayed on the left, below the [currentAccountPicture].
332 333
  final Widget accountName;

334 335
  /// A widget that represents the email address of the user's current account.
  /// It is displayed on the left, below the [accountName].
336 337
  final Widget accountEmail;

338 339 340 341
  /// A callback that is called when the horizontal area which contains the
  /// [accountName] and [accountEmail] is tapped.
  final VoidCallback onDetailsPressed;

342 343 344
  /// The [Color] of the arrow icon.
  final Color arrowColor;

345
  @override
346
  _UserAccountsDrawerHeaderState createState() => _UserAccountsDrawerHeaderState();
347 348 349
}

class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
350
  bool _isOpen = false;
351

352 353 354 355 356 357 358
  void _handleDetailsPressed() {
    setState(() {
      _isOpen = !_isOpen;
    });
    widget.onDetailsPressed();
  }

359 360 361
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
362
    assert(debugCheckHasMaterialLocalizations(context));
363
    return Semantics(
364 365
      container: true,
      label: MaterialLocalizations.of(context).signedInLabel,
366 367
      child: DrawerHeader(
        decoration: widget.decoration ?? BoxDecoration(
368 369 370 371
          color: Theme.of(context).primaryColor,
        ),
        margin: widget.margin,
        padding: const EdgeInsetsDirectional.only(top: 16.0, start: 16.0),
372
        child: SafeArea(
373
          bottom: false,
374
          child: Column(
375 376
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
377 378
              Expanded(
                child: Padding(
379
                  padding: const EdgeInsetsDirectional.only(end: 16.0),
380
                  child: _AccountPictures(
381 382 383
                    currentAccountPicture: widget.currentAccountPicture,
                    otherAccountsPictures: widget.otherAccountsPictures,
                  ),
384
                ),
385
              ),
386
              _AccountDetails(
387 388 389 390
                accountName: widget.accountName,
                accountEmail: widget.accountEmail,
                isOpen: _isOpen,
                onTap: widget.onDetailsPressed == null ? null : _handleDetailsPressed,
391
                arrowColor: widget.arrowColor,
392 393 394
              ),
            ],
          ),
395
        ),
396
      ),
397 398 399
    );
  }
}