app_bar.dart 34.7 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/foundation.dart';
8
import 'package:flutter/rendering.dart';
9
import 'package:flutter/services.dart';
10
import 'package:flutter/widgets.dart';
11

12
import 'back_button.dart';
13
import 'constants.dart';
14 15 16
import 'flexible_space_bar.dart';
import 'icon_button.dart';
import 'icons.dart';
17
import 'material.dart';
18
import 'material_localizations.dart';
19
import 'scaffold.dart';
20
import 'tabs.dart';
21
import 'theme.dart';
22
import 'typography.dart';
23

24 25 26 27 28
// Examples can assume:
// void _airDress() { }
// void _restitchDress() { }
// void _repairDress() { }

29
const double _kLeadingWidth = kToolbarHeight; // So the leading button is square.
30

31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
// Bottom justify the kToolbarHeight child which may overflow the top.
class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
  const _ToolbarContainerLayout();

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return constraints.tighten(height: kToolbarHeight);
  }

  @override
  Size getSize(BoxConstraints constraints) {
    return new Size(constraints.maxWidth, kToolbarHeight);
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return new Offset(0.0, size.height - childSize.height);
  }

  @override
  bool shouldRelayout(_ToolbarContainerLayout oldDelegate) => false;
}

54
// TODO(eseidel) Toolbar needs to change size based on orientation:
55
// http://material.google.com/layout/structure.html#structure-app-bar
56 57 58 59
// Mobile Landscape: 48dp
// Mobile Portrait: 56dp
// Tablet/Desktop: 64dp

60 61
/// A material design app bar.
///
62
/// An app bar consists of a toolbar and potentially other widgets, such as a
63
/// [TabBar] and a [FlexibleSpaceBar]. App bars typically expose one or more
64 65 66
/// common [actions] with [IconButton]s which are optionally followed by a
/// [PopupMenuButton] for less common operations (sometimes called the "overflow
/// menu").
67
///
68 69 70 71
/// App bars are typically used in the [Scaffold.appBar] property, which places
/// the app bar as a fixed-height widget at the top of the screen. For a
/// scrollable app bar, see [SliverAppBar], which embeds an [AppBar] in a sliver
/// for use in a [CustomScrollView].
72
///
73 74 75 76 77 78 79 80 81 82 83 84 85 86
/// The AppBar displays the toolbar widgets, [leading], [title], and [actions],
/// above the [bottom] (if any). The [bottom] is usually used for a [TabBar]. If
/// a [flexibleSpace] widget is specified then it is stacked behind the toolbar
/// and the bottom widget. The following diagram shows where each of these slots
/// appears in the toolbar when the writing language is left-to-right (e.g.
/// English):
///
/// ![The leading widget is in the top left, the actions are in the top right,
/// the title is between them. The bottom is, naturally, at the bottom, and the
/// flexibleSpace is behind all of them.](https://flutter.github.io/assets-for-api-docs/material/app_bar.png)
///
/// If the [leading] widget is omitted, but the [AppBar] is in a [Scaffold] with
/// a [Drawer], then a button will be inserted to open the drawer. Otherwise, if
/// the nearest [Navigator] has any previous routes, a [BackButton] is inserted
87 88 89
/// instead. This behavior can be turned off by setting the [automaticallyImplyLeading]
/// to false. In that case a null leading widget will result in the middle/title widget
/// stretching to start.
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
///
/// ## Sample code
///
/// ```dart
/// new AppBar(
///   title: new Text('My Fancy Dress'),
///   actions: <Widget>[
///     new IconButton(
///       icon: new Icon(Icons.playlist_play),
///       tooltip: 'Air it',
///       onPressed: _airDress,
///     ),
///     new IconButton(
///       icon: new Icon(Icons.playlist_add),
///       tooltip: 'Restitch it',
///       onPressed: _restitchDress,
///     ),
///     new IconButton(
///       icon: new Icon(Icons.playlist_add_check),
///       tooltip: 'Repair it',
///       onPressed: _repairDress,
///     ),
///   ],
/// )
/// ```
115
///
116 117
/// See also:
///
118
///  * [Scaffold], which displays the [AppBar] in its [Scaffold.appBar] slot.
119 120
///  * [SliverAppBar], which uses [AppBar] to provide a flexible app bar that
///    can be used in a [CustomScrollView].
121 122 123 124 125 126
///  * [TabBar], which is typically placed in the [bottom] slot of the [AppBar]
///    if the screen has multiple pages arranged in tabs.
///  * [IconButton], which is used with [actions] to show buttons on the app bar.
///  * [PopupMenuButton], to show a popup menu on the app bar, via [actions].
///  * [FlexibleSpaceBar], which is used with [flexibleSpace] when the app bar
///    can expand and collapse.
127
///  * <https://material.google.com/layout/structure.html#structure-toolbars>
128
class AppBar extends StatefulWidget implements PreferredSizeWidget {
129 130
  /// Creates a material design app bar.
  ///
131 132 133
  /// The arguments [elevation], [primary], [toolbarOpacity], [bottomOpacity]
  /// and [automaticallyImplyLeading] must not be null.
  ///
134
  /// Typically used in the [Scaffold.appBar] property.
135
  AppBar({
136
    Key key,
137
    this.leading,
138
    this.automaticallyImplyLeading: true,
139 140
    this.title,
    this.actions,
141
    this.flexibleSpace,
142
    this.bottom,
143
    this.elevation: 4.0,
Adam Barth's avatar
Adam Barth committed
144
    this.backgroundColor,
145
    this.brightness,
146
    this.iconTheme,
147
    this.textTheme,
148
    this.primary: true,
149
    this.centerTitle,
150
    this.titleSpacing: NavigationToolbar.kMiddleSpacing,
151 152
    this.toolbarOpacity: 1.0,
    this.bottomOpacity: 1.0,
153 154
  }) : assert(automaticallyImplyLeading != null),
       assert(elevation != null),
155
       assert(primary != null),
156
       assert(titleSpacing != null),
157 158 159 160
       assert(toolbarOpacity != null),
       assert(bottomOpacity != null),
       preferredSize = new Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)),
       super(key: key);
161

162 163
  /// A widget to display before the [title].
  ///
164 165 166 167 168 169
  /// If this is null and [automaticallyImplyLeading] is set to true, the
  /// [AppBar] will imply an appropriate widget. For example, if the [AppBar] is
  /// in a [Scaffold] that also has a [Drawer], the [Scaffold] will fill this
  /// widget with an [IconButton] that opens the drawer (using [Icons.menu]). If
  /// there's no [Drawer] and the parent [Navigator] can go back, the [AppBar]
  /// will use a [BackButton] that calls [Navigator.maybePop].
170
  final Widget leading;
171

172 173 174 175 176 177 178
  /// Controls whether we should try to imply the leading widget if null.
  ///
  /// If true and [leading] is null, automatically try to deduce what the leading
  /// widget should be. If false and [leading] is null, leading space is given to [title].
  /// If leading widget is not null, this parameter has no effect.
  final bool automaticallyImplyLeading;

179
  /// The primary widget displayed in the appbar.
180 181 182
  ///
  /// Typically a [Text] widget containing a description of the current contents
  /// of the app.
183
  final Widget title;
184

185
  /// Widgets to display after the [title] widget.
186 187 188 189
  ///
  /// Typically these widgets are [IconButton]s representing common operations.
  /// For less common operations, consider using a [PopupMenuButton] as the
  /// last action.
190 191 192 193 194 195 196 197 198 199 200 201 202
  ///
  /// For example:
  ///
  /// ```dart
  /// return new Scaffold(
  ///   appBar: new AppBar(
  ///     title: new Text('Hello World'),
  ///     actions: <Widget>[
  ///       new IconButton(
  ///         icon: new Icon(Icons.shopping_cart),
  ///         tooltip: 'Open shopping cart',
  ///         onPressed: _openCart,
  ///       ),
203
  ///     ],
204 205 206 207
  ///   ),
  ///   body: _buildBody(),
  /// );
  /// ```
208
  final List<Widget> actions;
209

210
  /// This widget is stacked behind the toolbar and the tabbar. It's height will
211
  /// be the same as the app bar's overall height.
212 213 214
  ///
  /// A flexible space isn't actually flexible unless the [AppBar]'s container
  /// changes the [AppBar]'s size. A [SliverAppBar] in a [CustomScrollView]
215
  /// changes the [AppBar]'s height when scrolled.
216 217 218 219
  ///
  /// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details.
  final Widget flexibleSpace;

220
  /// This widget appears across the bottom of the app bar.
221
  ///
222
  /// Typically a [TabBar]. Only widgets that implement [PreferredSizeWidget] can
223
  /// be used at the bottom of an app bar.
224 225 226 227 228
  ///
  /// See also:
  ///
  ///  * [PreferredSize], which can be used to give an arbitrary widget a preferred size.
  final PreferredSizeWidget bottom;
229

230 231
  /// The z-coordinate at which to place this app bar. This controls the size of
  /// the shadow below the app bar.
232
  ///
233
  /// Defaults to 4, the appropriate elevation for app bars.
234
  final double elevation;
235

236 237
  /// The color to use for the app bar's material. Typically this should be set
  /// along with [brightness], [iconTheme], [textTheme].
238 239
  ///
  /// Defaults to [ThemeData.primaryColor].
240
  final Color backgroundColor;
241

242 243
  /// The brightness of the app bar's material. Typically this is set along
  /// with [backgroundColor], [iconTheme], [textTheme].
Ian Hickson's avatar
Ian Hickson committed
244
  ///
245
  /// Defaults to [ThemeData.primaryColorBrightness].
246 247
  final Brightness brightness;

248 249 250 251 252 253 254 255
  /// The color, opacity, and size to use for app bar icons. Typically this
  /// is set along with [backgroundColor], [brightness], [textTheme].
  ///
  /// Defaults to [ThemeData.primaryIconTheme].
  final IconThemeData iconTheme;

  /// The typographic styles to use for text in the app bar. Typically this is
  /// set along with [brightness] [backgroundColor], [iconTheme].
256 257
  ///
  /// Defaults to [ThemeData.primaryTextTheme].
Adam Barth's avatar
Adam Barth committed
258
  final TextTheme textTheme;
259

260 261
  /// Whether this app bar is being displayed at the top of the screen.
  ///
262 263 264
  /// If true, the appbar's toolbar elements and [bottom] widget will be
  /// padded on top by the height of the system status bar. The layout
  /// of the [flexibleSpace] is not affected by the [primary] property.
265
  final bool primary;
266

267 268 269 270 271
  /// Whether the title should be centered.
  ///
  /// Defaults to being adapted to the current [TargetPlatform].
  final bool centerTitle;

272 273 274 275 276 277 278
  /// The spacing around [title] content on the horizontal axis. This spacing is
  /// applied even if there is no [leading] content or [actions]. If you want
  /// [title] to take all the space available, set this value to 0.0.
  ///
  /// Defaults to [NavigationToolbar.kMiddleSpacing].
  final double titleSpacing;

279 280 281 282 283 284 285
  /// How opaque the toolbar part of the app bar is.
  ///
  /// A value of 1.0 is fully opaque, and a value of 0.0 is fully transparent.
  ///
  /// Typically, this value is not changed from its default value (1.0). It is
  /// used by [SliverAppBar] to animate the opacity of the toolbar when the app
  /// bar is scrolled.
286
  final double toolbarOpacity;
287

288 289 290 291 292 293 294
  /// How opaque the bottom part of the app bar is.
  ///
  /// A value of 1.0 is fully opaque, and a value of 0.0 is fully transparent.
  ///
  /// Typically, this value is not changed from its default value (1.0). It is
  /// used by [SliverAppBar] to animate the opacity of the toolbar when the app
  /// bar is scrolled.
295
  final double bottomOpacity;
296

297 298
  /// A size whose height is the sum of [kToolbarHeight] and the [bottom] widget's
  /// preferred height.
299
  ///
300 301 302
  /// [Scaffold] uses this this size to set its app bar's height.
  @override
  final Size preferredSize;
303

304 305 306 307 308 309
  bool _getEffectiveCenterTitle(ThemeData themeData) {
    if (centerTitle != null)
      return centerTitle;
    assert(themeData.platform != null);
    switch (themeData.platform) {
      case TargetPlatform.android:
310
      case TargetPlatform.fuchsia:
311 312
        return false;
      case TargetPlatform.iOS:
313
        return actions == null || actions.length < 2;
314 315 316 317
    }
    return null;
  }

318
  @override
319
  _AppBarState createState() => new _AppBarState();
320 321
}

322
class _AppBarState extends State<AppBar> {
323 324 325 326
  void _handleDrawerButton() {
    Scaffold.of(context).openDrawer();
  }

327 328 329 330
  void _handleDrawerButtonEnd() {
    Scaffold.of(context).openEndDrawer();
  }

331 332
  @override
  Widget build(BuildContext context) {
333
    assert(!widget.primary || debugCheckHasMediaQuery(context));
334
    final ThemeData themeData = Theme.of(context);
335 336 337 338
    final ScaffoldState scaffold = Scaffold.of(context, nullOk: true);
    final ModalRoute<dynamic> parentRoute = ModalRoute.of(context);

    final bool hasDrawer = scaffold?.hasDrawer ?? false;
339
    final bool hasEndDrawer = scaffold?.hasEndDrawer ?? false;
340
    final bool canPop = parentRoute?.canPop ?? false;
341
    final bool useCloseButton = parentRoute is PageRoute<dynamic> && parentRoute.fullscreenDialog;
342

343 344 345
    IconThemeData appBarIconTheme = widget.iconTheme ?? themeData.primaryIconTheme;
    TextStyle centerStyle = widget.textTheme?.title ?? themeData.primaryTextTheme.title;
    TextStyle sideStyle = widget.textTheme?.body1 ?? themeData.primaryTextTheme.body1;
346

347
    final Brightness brightness = widget.brightness ?? themeData.primaryColorBrightness;
348
    SystemChrome.setSystemUIOverlayStyle(brightness == Brightness.dark
349 350
      ? SystemUiOverlayStyle.light
      : SystemUiOverlayStyle.dark);
351

352 353
    if (widget.toolbarOpacity != 1.0) {
      final double opacity = const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.toolbarOpacity);
354
      if (centerStyle?.color != null)
355
        centerStyle = centerStyle.copyWith(color: centerStyle.color.withOpacity(opacity));
356
      if (sideStyle?.color != null)
357
        sideStyle = sideStyle.copyWith(color: sideStyle.color.withOpacity(opacity));
358 359
      appBarIconTheme = appBarIconTheme.copyWith(
        opacity: opacity * (appBarIconTheme.opacity ?? 1.0)
Ian Hickson's avatar
Ian Hickson committed
360
      );
361 362
    }

363
    Widget leading = widget.leading;
364
    if (leading == null && widget.automaticallyImplyLeading) {
365
      if (hasDrawer) {
366
        leading = new IconButton(
367
          icon: const Icon(Icons.menu),
368
          onPressed: _handleDrawerButton,
369
          tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
370 371
        );
      } else {
372 373
        if (canPop)
          leading = useCloseButton ? const CloseButton() : const BackButton();
374 375
      }
    }
376
    if (leading != null) {
377 378 379
      leading = new ConstrainedBox(
        constraints: const BoxConstraints.tightFor(width: _kLeadingWidth),
        child: leading,
380 381
      );
    }
382

383 384 385 386 387 388 389
    Widget title = widget.title;
    if (title != null) {
      title = new DefaultTextStyle(
        style: centerStyle,
        softWrap: false,
        overflow: TextOverflow.ellipsis,
        child: title,
390
      );
391
    }
392 393

    Widget actions;
394
    if (widget.actions != null && widget.actions.isNotEmpty) {
395 396 397 398
      actions = new Row(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: widget.actions,
399
      );
400 401 402 403 404
    } else if (hasEndDrawer) {
      actions = new IconButton(
        icon: const Icon(Icons.menu),
        onPressed: _handleDrawerButtonEnd,
        tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
405
      );
406
    }
407

408
    final Widget toolbar = new Padding(
409
      padding: const EdgeInsetsDirectional.only(end: 4.0),
410 411 412 413 414
      child: new NavigationToolbar(
        leading: leading,
        middle: title,
        trailing: actions,
        centerMiddle: widget._getEffectiveCenterTitle(themeData),
415
        middleSpacing: widget.titleSpacing,
416
      ),
417
    );
418

419 420 421 422 423
    // If the toolbar is allocated less than kToolbarHeight make it
    // appear to scroll upwards within its shrinking container.
    Widget appBar = new ClipRect(
      child: new CustomSingleChildLayout(
        delegate: const _ToolbarContainerLayout(),
424
        child: IconTheme.merge(
425 426 427 428 429
          data: appBarIconTheme,
          child: new DefaultTextStyle(
            style: sideStyle,
            child: toolbar,
          ),
430 431
        ),
      ),
432 433
    );

434
    if (widget.bottom != null) {
435
      appBar = new Column(
436
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
437
        children: <Widget>[
438 439
          new Flexible(
            child: new ConstrainedBox(
440
              constraints: const BoxConstraints(maxHeight: kToolbarHeight),
441 442 443
              child: appBar,
            ),
          ),
444 445 446
          widget.bottomOpacity == 1.0 ? widget.bottom : new Opacity(
            opacity: const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.bottomOpacity),
            child: widget.bottom,
447 448
          ),
        ],
449 450 451
      );
    }

452
    // The padding applies to the toolbar and tabbar, not the flexible space.
453
    if (widget.primary) {
Ian Hickson's avatar
Ian Hickson committed
454 455
      appBar = new SafeArea(
        top: true,
456
        child: appBar,
457 458
      );
    }
459

460
    appBar = new Align(
461
      alignment: Alignment.topCenter,
462 463 464
      child: appBar,
    );

465
    if (widget.flexibleSpace != null) {
466
      appBar = new Stack(
467
        fit: StackFit.passthrough,
468
        children: <Widget>[
469
          widget.flexibleSpace,
470
          appBar,
471
        ],
472 473 474
      );
    }

475
    return new Material(
476 477
      color: widget.backgroundColor ?? themeData.primaryColor,
      elevation: widget.elevation,
478
      child: appBar,
479 480
    );
  }
481 482
}

483
class _FloatingAppBar extends StatefulWidget {
484
  const _FloatingAppBar({ Key key, this.child }) : super(key: key);
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534

  final Widget child;

  @override
  _FloatingAppBarState createState() => new _FloatingAppBarState();
}

// A wrapper for the widget created by _SliverAppBarDelegate that starts and
/// stops the floating appbar's snap-into-view or snap-out-of-view animation.
class _FloatingAppBarState extends State<_FloatingAppBar> {
  ScrollPosition _position;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (_position != null)
      _position.isScrollingNotifier.removeListener(_isScrollingListener);
    _position = Scrollable.of(context)?.position;
    if (_position != null)
      _position.isScrollingNotifier.addListener(_isScrollingListener);
  }

  @override
  void dispose() {
    if (_position != null)
      _position.isScrollingNotifier.removeListener(_isScrollingListener);
    super.dispose();
  }

  RenderSliverFloatingPersistentHeader _headerRenderer() {
    return context.ancestorRenderObjectOfType(const TypeMatcher<RenderSliverFloatingPersistentHeader>());
  }

  void _isScrollingListener() {
    if (_position == null)
      return;

    // When a scroll stops, then maybe snap the appbar into view.
    // Similarly, when a scroll starts, then maybe stop the snap animation.
    final RenderSliverFloatingPersistentHeader header = _headerRenderer();
    if (_position.isScrollingNotifier.value)
      header?.maybeStopSnapAnimation(_position.userScrollDirection);
    else
      header?.maybeStartSnapAnimation(_position.userScrollDirection);
  }

  @override
  Widget build(BuildContext context) => widget.child;
}

535 536 537
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate({
    @required this.leading,
538
    @required this.automaticallyImplyLeading,
539 540 541
    @required this.title,
    @required this.actions,
    @required this.flexibleSpace,
542
    @required this.bottom,
543
    @required this.elevation,
544
    @required this.forceElevated,
545 546 547 548 549 550
    @required this.backgroundColor,
    @required this.brightness,
    @required this.iconTheme,
    @required this.textTheme,
    @required this.primary,
    @required this.centerTitle,
551
    @required this.titleSpacing,
552
    @required this.expandedHeight,
553
    @required this.collapsedHeight,
554
    @required this.topPadding,
555
    @required this.floating,
556
    @required this.pinned,
557
    @required this.snapConfiguration,
558 559
  }) : assert(primary || topPadding == 0.0),
       _bottomHeight = bottom?.preferredSize?.height ?? 0.0;
560 561

  final Widget leading;
562
  final bool automaticallyImplyLeading;
563 564 565
  final Widget title;
  final List<Widget> actions;
  final Widget flexibleSpace;
566
  final PreferredSizeWidget bottom;
567
  final double elevation;
568
  final bool forceElevated;
569 570 571 572 573 574
  final Color backgroundColor;
  final Brightness brightness;
  final IconThemeData iconTheme;
  final TextTheme textTheme;
  final bool primary;
  final bool centerTitle;
575
  final double titleSpacing;
576
  final double expandedHeight;
577
  final double collapsedHeight;
578
  final double topPadding;
579
  final bool floating;
580 581 582 583 584
  final bool pinned;

  final double _bottomHeight;

  @override
585
  double get minExtent => collapsedHeight ?? (topPadding + kToolbarHeight + _bottomHeight);
586

587
  @override
588
  double get maxExtent => math.max(topPadding + (expandedHeight ?? kToolbarHeight + _bottomHeight), minExtent);
589

590 591 592
  @override
  final FloatingHeaderSnapConfiguration snapConfiguration;

593 594
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
595 596 597
    final double visibleMainHeight = maxExtent - shrinkOffset - topPadding;
    final double toolbarOpacity = pinned && !floating ? 1.0
      : ((visibleMainHeight - _bottomHeight) / kToolbarHeight).clamp(0.0, 1.0);
598
    final Widget appBar = FlexibleSpaceBar.createSettings(
599 600 601 602 603 604
      minExtent: minExtent,
      maxExtent: maxExtent,
      currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
      toolbarOpacity: toolbarOpacity,
      child: new AppBar(
        leading: leading,
605
        automaticallyImplyLeading: automaticallyImplyLeading,
606 607 608 609
        title: title,
        actions: actions,
        flexibleSpace: flexibleSpace,
        bottom: bottom,
610
        elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4.0 : 0.0,
611 612 613 614 615 616
        backgroundColor: backgroundColor,
        brightness: brightness,
        iconTheme: iconTheme,
        textTheme: textTheme,
        primary: primary,
        centerTitle: centerTitle,
617
        titleSpacing: titleSpacing,
618 619 620 621
        toolbarOpacity: toolbarOpacity,
        bottomOpacity: pinned ? 1.0 : (visibleMainHeight / _bottomHeight).clamp(0.0, 1.0),
      ),
    );
622
    return floating ? new _FloatingAppBar(child: appBar) : appBar;
623 624 625
  }

  @override
626
  bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) {
627
    return leading != oldDelegate.leading
628
        || automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading
629 630 631 632 633 634 635 636 637 638 639 640
        || title != oldDelegate.title
        || actions != oldDelegate.actions
        || flexibleSpace != oldDelegate.flexibleSpace
        || bottom != oldDelegate.bottom
        || _bottomHeight != oldDelegate._bottomHeight
        || elevation != oldDelegate.elevation
        || backgroundColor != oldDelegate.backgroundColor
        || brightness != oldDelegate.brightness
        || iconTheme != oldDelegate.iconTheme
        || textTheme != oldDelegate.textTheme
        || primary != oldDelegate.primary
        || centerTitle != oldDelegate.centerTitle
641
        || titleSpacing != oldDelegate.titleSpacing
642
        || expandedHeight != oldDelegate.expandedHeight
643 644
        || topPadding != oldDelegate.topPadding
        || pinned != oldDelegate.pinned
645 646
        || floating != oldDelegate.floating
        || snapConfiguration != oldDelegate.snapConfiguration;
647
  }
648 649 650

  @override
  String toString() {
651
    return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)';
652
  }
653 654
}

655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671
/// A material design app bar that integrates with a [CustomScrollView].
///
/// An app bar consists of a toolbar and potentially other widgets, such as a
/// [TabBar] and a [FlexibleSpaceBar]. App bars typically expose one or more
/// common actions with [IconButton]s which are optionally followed by a
/// [PopupMenuButton] for less common operations.
///
/// Sliver app bars are typically used as the first child of a
/// [CustomScrollView], which lets the app bar integrate with the scroll view so
/// that it can vary in height according to the scroll offset or float above the
/// other content in the scroll view. For a fixed-height app bar at the top of
/// the screen see [AppBar], which is used in the [Scaffold.appBar] slot.
///
/// The AppBar displays the toolbar widgets, [leading], [title], and
/// [actions], above the [bottom] (if any). If a [flexibleSpace] widget is
/// specified then it is stacked behind the toolbar and the bottom widget.
///
672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692
/// ## Sample code
///
/// This is an example that could be included in a [CustomScrollView]'s
/// [CustomScrollView.slivers] list:
///
/// ```dart
/// new SliverAppBar(
///   expandedHeight: 150.0,
///   flexibleSpace: const FlexibleSpaceBar(
///     title: const Text('Available seats'),
///   ),
///   actions: <Widget>[
///     new IconButton(
///       icon: const Icon(Icons.add_circle),
///       tooltip: 'Add new entry',
///       onPressed: () { /* ... */ },
///     ),
///   ]
/// )
/// ```
///
693 694 695 696 697 698 699 700 701 702 703 704
/// See also:
///
///  * [CustomScrollView], which integrates the [SliverAppBar] into its
///    scrolling.
///  * [AppBar], which is a fixed-height app bar for use in [Scaffold.appBar].
///  * [TabBar], which is typically placed in the [bottom] slot of the [AppBar]
///    if the screen has multiple pages arranged in tabs.
///  * [IconButton], which is used with [actions] to show buttons on the app bar.
///  * [PopupMenuButton], to show a popup menu on the app bar, via [actions].
///  * [FlexibleSpaceBar], which is used with [flexibleSpace] when the app bar
///    can expand and collapse.
///  * <https://material.google.com/layout/structure.html#structure-toolbars>
705
class SliverAppBar extends StatefulWidget {
706
  /// Creates a material design app bar that can be placed in a [CustomScrollView].
707 708 709
  ///
  /// The arguments [forceElevated], [primary], [floating], [pinned], [snap]
  /// and [automaticallyImplyLeading] must not be null.
710
  const SliverAppBar({
711 712
    Key key,
    this.leading,
713
    this.automaticallyImplyLeading: true,
714 715 716 717 718
    this.title,
    this.actions,
    this.flexibleSpace,
    this.bottom,
    this.elevation,
719
    this.forceElevated: false,
720 721 722 723 724 725
    this.backgroundColor,
    this.brightness,
    this.iconTheme,
    this.textTheme,
    this.primary: true,
    this.centerTitle,
726
    this.titleSpacing: NavigationToolbar.kMiddleSpacing,
727 728 729
    this.expandedHeight,
    this.floating: false,
    this.pinned: false,
730
    this.snap: false,
731 732
  }) : assert(automaticallyImplyLeading != null),
       assert(forceElevated != null),
733
       assert(primary != null),
734
       assert(titleSpacing != null),
735 736 737
       assert(floating != null),
       assert(pinned != null),
       assert(snap != null),
738
       assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
739
       super(key: key);
740 741 742

  /// A widget to display before the [title].
  ///
743 744 745 746 747 748
  /// If this is null and [automaticallyImplyLeading] is set to true, the [AppBar] will
  /// imply an appropriate widget. For example, if the [AppBar] is in a [Scaffold]
  /// that also has a [Drawer], the [Scaffold] will fill this widget with an
  /// [IconButton] that opens the drawer. If there's no [Drawer] and the parent
  /// [Navigator] can go back, the [AppBar] will use a [BackButton] that calls
  /// [Navigator.maybePop].
749 750
  final Widget leading;

751 752 753 754 755 756 757
  /// Controls whether we should try to imply the leading widget if null.
  ///
  /// If true and [leading] is null, automatically try to deduce what the leading
  /// widget should be. If false and [leading] is null, leading space is given to [title].
  /// If leading widget is not null, this parameter has no effect.
  final bool automaticallyImplyLeading;

758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782
  /// The primary widget displayed in the appbar.
  ///
  /// Typically a [Text] widget containing a description of the current contents
  /// of the app.
  final Widget title;

  /// Widgets to display after the [title] widget.
  ///
  /// Typically these widgets are [IconButton]s representing common operations.
  /// For less common operations, consider using a [PopupMenuButton] as the
  /// last action.
  ///
  /// For example:
  ///
  /// ```dart
  /// return new Scaffold(
  ///   body: new CustomView(
  ///     primary: true,
  ///     slivers: <Widget>[
  ///       new SliverAppBar(
  ///         title: new Text('Hello World'),
  ///         actions: <Widget>[
  ///           new IconButton(
  ///             icon: new Icon(Icons.shopping_cart),
  ///             tooltip: 'Open shopping cart',
783 784 785
  ///             onPressed: () {
  ///               // handle the press
  ///             },
786 787 788 789 790 791 792 793 794 795 796
  ///           ),
  ///         ],
  ///       ),
  ///       // ...rest of body...
  ///     ],
  ///   ),
  /// );
  /// ```
  final List<Widget> actions;

  /// This widget is stacked behind the toolbar and the tabbar. It's height will
797
  /// be the same as the app bar's overall height.
798 799 800 801 802 803
  ///
  /// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details.
  final Widget flexibleSpace;

  /// This widget appears across the bottom of the appbar.
  ///
804 805 806 807 808 809 810
  /// Typically a [TabBar]. Only widgets that implement [PreferredSizeWidget] can
  /// be used at the bottom of an app bar.
  ///
  /// See also:
  ///
  ///  * [PreferredSize], which can be used to give an arbitrary widget a preferred size.
  final PreferredSizeWidget bottom;
811

812
  /// The z-coordinate at which to place this app bar when it is above other
813
  /// content. This controls the size of the shadow below the app bar.
814 815 816
  ///
  /// Defaults to 4, the appropriate elevation for app bars.
  ///
817 818 819 820
  /// If [forceElevated] is false, the elevation is ignored when the app bar has
  /// no content underneath it. For example, if the app bar is [pinned] but no
  /// content is scrolled under it, or if it scrolls with the content, then no
  /// shadow is drawn, regardless of the value of [elevation].
821
  final double elevation;
822

823 824 825 826 827 828 829 830 831 832 833
  /// Whether to show the shadow appropriate for the [elevation] even if the
  /// content is not scrolled under the [AppBar].
  ///
  /// Defaults to false, meaning that the [elevation] is only applied when the
  /// [AppBar] is being displayed over content that is scrolled under it.
  ///
  /// When set to true, the [elevation] is applied regardless.
  ///
  /// Ignored when [elevation] is zero.
  final bool forceElevated;

834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868
  /// The color to use for the app bar's material. Typically this should be set
  /// along with [brightness], [iconTheme], [textTheme].
  ///
  /// Defaults to [ThemeData.primaryColor].
  final Color backgroundColor;

  /// The brightness of the app bar's material. Typically this is set along
  /// with [backgroundColor], [iconTheme], [textTheme].
  ///
  /// Defaults to [ThemeData.primaryColorBrightness].
  final Brightness brightness;

  /// The color, opacity, and size to use for app bar icons. Typically this
  /// is set along with [backgroundColor], [brightness], [textTheme].
  ///
  /// Defaults to [ThemeData.primaryIconTheme].
  final IconThemeData iconTheme;

  /// The typographic styles to use for text in the app bar. Typically this is
  /// set along with [brightness] [backgroundColor], [iconTheme].
  ///
  /// Defaults to [ThemeData.primaryTextTheme].
  final TextTheme textTheme;

  /// Whether this app bar is being displayed at the top of the screen.
  ///
  /// If this is true, the top padding specified by the [MediaQuery] will be
  /// added to the top of the toolbar.
  final bool primary;

  /// Whether the title should be centered.
  ///
  /// Defaults to being adapted to the current [TargetPlatform].
  final bool centerTitle;

869 870 871 872 873 874 875
  /// The spacing around [title] content on the horizontal axis. This spacing is
  /// applied even if there is no [leading] content or [actions]. If you want
  /// [title] to take all the space available, set this value to 0.0.
  ///
  /// Defaults to [NavigationToolbar.kMiddleSpacing].
  final double titleSpacing;

876 877 878 879 880 881 882 883 884 885
  /// The size of the app bar when it is fully expanded.
  ///
  /// By default, the total height of the toolbar and the bottom widget (if
  /// any). If a [flexibleSpace] widget is specified this height should be big
  /// enough to accommodate whatever that widget contains.
  ///
  /// This does not include the status bar height (which will be automatically
  /// included if [primary] is true).
  final double expandedHeight;

886 887 888 889 890
  /// Whether the app bar should become visible as soon as the user scrolls
  /// towards the app bar.
  ///
  /// Otherwise, the user will need to scroll near the top of the scroll view to
  /// reveal the app bar.
891
  ///
892 893 894
  /// If [snap] is true then a scroll that exposes the app bar will trigger an
  /// animation that slides the entire app bar into view. Similarly if a scroll
  /// dismisses the app bar, the animation will slide it completely out of view.
895 896
  final bool floating;

897 898 899 900
  /// Whether the app bar should remain visible at the start of the scroll view.
  ///
  /// The app bar can still expand an contract as the user scrolls, but it will
  /// remain visible rather than being scrolled out of view.
901 902
  final bool pinned;

903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948
  /// If [snap] and [floating] are true then the floating app bar will "snap"
  /// into view.
  ///
  /// If [snap] is true then a scroll that exposes the floating app bar will
  /// trigger an animation that slides the entire app bar into view. Similarly if
  /// a scroll dismisses the app bar, the animation will slide the app bar
  /// completely out of view.
  ///
  /// Snapping only applies when the app bar is floating, not when the appbar
  /// appears at the top of its scroll view.
  final bool snap;

  @override
  _SliverAppBarState createState() => new _SliverAppBarState();
}

// This class is only Stateful because it owns the TickerProvider used
// by the floating appbar snap animation (via FloatingHeaderSnapConfiguration).
class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMixin {
  FloatingHeaderSnapConfiguration _snapConfiguration;

  void _updateSnapConfiguration() {
    if (widget.snap && widget.floating) {
      _snapConfiguration = new FloatingHeaderSnapConfiguration(
        vsync: this,
        curve: Curves.easeOut,
        duration: const Duration(milliseconds: 200),
      );
    } else {
      _snapConfiguration = null;
    }
  }

  @override
  void initState() {
    super.initState();
    _updateSnapConfiguration();
  }

  @override
  void didUpdateWidget(SliverAppBar oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.snap != oldWidget.snap || widget.floating != oldWidget.floating)
      _updateSnapConfiguration();
  }

949 950
  @override
  Widget build(BuildContext context) {
951
    assert(!widget.primary || debugCheckHasMediaQuery(context));
952 953
    final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0;
    final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
954
      ? widget.bottom.preferredSize.height + topPadding : null;
955

956 957 958 959
    return new MediaQuery.removePadding(
      context: context,
      removeBottom: true,
      child: new SliverPersistentHeader(
960 961
        floating: widget.floating,
        pinned: widget.pinned,
962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984
        delegate: new _SliverAppBarDelegate(
          leading: widget.leading,
          automaticallyImplyLeading: widget.automaticallyImplyLeading,
          title: widget.title,
          actions: widget.actions,
          flexibleSpace: widget.flexibleSpace,
          bottom: widget.bottom,
          elevation: widget.elevation,
          forceElevated: widget.forceElevated,
          backgroundColor: widget.backgroundColor,
          brightness: widget.brightness,
          iconTheme: widget.iconTheme,
          textTheme: widget.textTheme,
          primary: widget.primary,
          centerTitle: widget.centerTitle,
          titleSpacing: widget.titleSpacing,
          expandedHeight: widget.expandedHeight,
          collapsedHeight: collapsedHeight,
          topPadding: topPadding,
          floating: widget.floating,
          pinned: widget.pinned,
          snapConfiguration: _snapConfiguration,
        ),
985 986 987
      ),
    );
  }
988
}