user_accounts_drawer_header.dart 12.1 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 75 76 77 78 79 80 81
    this.onTap,
    this.isOpen,
  }) : super(key: key);

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

82 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
  @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);
116 117 118 119 120 121
    // 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) {
122 123 124 125 126 127
      _controller.forward();
    } else {
      _controller.reverse();
    }
  }

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

134
    final ThemeData theme = Theme.of(context);
135 136
    final List<Widget> children = <Widget>[];

137
    if (widget.accountName != null) {
138
      final Widget accountNameLine = LayoutId(
139
        id: _AccountDetailsLayout.accountName,
140
        child: Padding(
141
          padding: const EdgeInsets.symmetric(vertical: 2.0),
142
          child: DefaultTextStyle(
143 144
            style: theme.primaryTextTheme.body2,
            overflow: TextOverflow.ellipsis,
145
            child: widget.accountName,
146 147 148 149
          ),
        ),
      );
      children.add(accountNameLine);
150
    }
151

152
    if (widget.accountEmail != null) {
153
      final Widget accountEmailLine = LayoutId(
154
        id: _AccountDetailsLayout.accountEmail,
155
        child: Padding(
156
          padding: const EdgeInsets.symmetric(vertical: 2.0),
157
          child: DefaultTextStyle(
158 159
            style: theme.primaryTextTheme.body1,
            overflow: TextOverflow.ellipsis,
160
            child: widget.accountEmail,
161
          ),
162 163
        ),
      );
164
      children.add(accountEmailLine);
165
    }
166
    if (widget.onTap != null) {
167
      final MaterialLocalizations localizations = MaterialLocalizations.of(context);
168
      final Widget dropDownIcon = LayoutId(
169
        id: _AccountDetailsLayout.dropdownIcon,
170
        child: Semantics(
171 172
          container: true,
          button: true,
173
          onTap: widget.onTap,
174
          child: SizedBox(
175 176
            height: _kAccountDetailsHeight,
            width: _kAccountDetailsHeight,
177
            child: Center(
178 179 180 181 182 183
              child: Transform.rotate(
                angle: _animation.value * math.pi,
                child: Icon(
                  Icons.arrow_drop_down,
                  color: Colors.white,
                  semanticLabel: widget.isOpen
184 185
                    ? localizations.hideAccountsLabel
                    : localizations.showAccountsLabel,
186
                ),
187 188 189 190 191 192 193 194
              ),
            ),
          ),
        ),
      );
      children.add(dropDownIcon);
    }

195 196
    Widget accountDetails = CustomMultiChildLayout(
      delegate: _AccountDetailsLayout(
197 198 199 200 201
        textDirection: Directionality.of(context),
      ),
      children: children,
    );

202
    if (widget.onTap != null) {
203
      accountDetails = InkWell(
204
        onTap: widget.onTap,
205 206 207 208
        child: accountDetails,
        excludeFromSemantics: true,
      );
    }
209

210
    return SizedBox(
211
      height: _kAccountDetailsHeight,
212
      child: accountDetails,
213 214 215 216
    );
  }
}

217 218 219 220 221 222
const double _kAccountDetailsHeight = 56.0;

class _AccountDetailsLayout extends MultiChildLayoutDelegate {

  _AccountDetailsLayout({ @required this.textDirection });

223 224 225
  static const String accountName = 'accountName';
  static const String accountEmail = 'accountEmail';
  static const String dropdownIcon = 'dropdownIcon';
226 227 228 229 230 231 232 233

  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)
234
      iconSize = layoutChild(dropdownIcon, BoxConstraints.loose(size));
235 236 237 238 239 240
      positionChild(dropdownIcon, _offsetForIcon(size, iconSize));
    }

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

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

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

      // place account name above account email
      if (bottomLine == accountEmail && hasChild(accountName)) {
251
        final Size nameSize = layoutChild(accountName, BoxConstraints.loose(constraintSize));
252 253 254 255 256 257 258 259 260 261 262
        positionChild(accountName, _offsetForName(size, nameSize, bottomLineOffset));
      }
    }
  }

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

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

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

319 320
  /// The header's background. If decoration is null then a [BoxDecoration]
  /// with its background color set to the current theme's primaryColor is used.
321
  final Decoration decoration;
322

323
  /// The margin around the drawer header.
324
  final EdgeInsetsGeometry margin;
325

326 327
  /// A widget placed in the upper-left corner that represents the current
  /// user's account. Normally a [CircleAvatar].
328 329
  final Widget currentAccountPicture;

330 331 332
  /// 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.
333 334
  final List<Widget> otherAccountsPictures;

335 336
  /// A widget that represents the user's current account name. It is
  /// displayed on the left, below the [currentAccountPicture].
337 338
  final Widget accountName;

339 340
  /// A widget that represents the email address of the user's current account.
  /// It is displayed on the left, below the [accountName].
341 342
  final Widget accountEmail;

343 344 345 346
  /// A callback that is called when the horizontal area which contains the
  /// [accountName] and [accountEmail] is tapped.
  final VoidCallback onDetailsPressed;

347
  @override
348
  _UserAccountsDrawerHeaderState createState() => _UserAccountsDrawerHeaderState();
349 350 351
}

class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
352
  bool _isOpen = false;
353

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

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