app_bar.dart 35.5 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
/// 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
82
/// flexibleSpace is behind all of them.](https://flutter.github.io/assets-for-api-docs/assets/material/app_bar.png)
83 84 85 86
///
/// 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 151 152
    this.titleSpacing = NavigationToolbar.kMiddleSpacing,
    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
  /// ## Sample code
192 193
  ///
  /// ```dart
194
  /// new Scaffold(
195 196 197 198 199 200
  ///   appBar: new AppBar(
  ///     title: new Text('Hello World'),
  ///     actions: <Widget>[
  ///       new IconButton(
  ///         icon: new Icon(Icons.shopping_cart),
  ///         tooltip: 'Open shopping cart',
201 202 203
  ///         onPressed: () {
  ///           // ...
  ///         },
204
  ///       ),
205
  ///     ],
206
  ///   ),
207
  /// )
208
  /// ```
209
  final List<Widget> actions;
210

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

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

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

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

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

249 250 251 252 253 254 255 256
  /// 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].
257 258
  ///
  /// Defaults to [ThemeData.primaryTextTheme].
Adam Barth's avatar
Adam Barth committed
259
  final TextTheme textTheme;
260

261 262
  /// Whether this app bar is being displayed at the top of the screen.
  ///
263 264 265
  /// 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.
266
  final bool primary;
267

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

273 274 275 276 277 278 279
  /// 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;

280 281 282 283 284 285 286
  /// 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.
287
  final double toolbarOpacity;
288

289 290 291 292 293 294 295
  /// 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.
296
  final double bottomOpacity;
297

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

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

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

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

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

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

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

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

Michael Goderbauer's avatar
Michael Goderbauer committed
348 349 350 351 352 353 354 355 356 357 358 359 360
    if (parentRoute?.isCurrent ?? true) {
      final Brightness brightness = widget.brightness ?? themeData.primaryColorBrightness;
      // TODO(jonahwilliams): remove once we have platform themes.
      switch (defaultTargetPlatform) {
        case TargetPlatform.iOS:
          SystemChrome.setSystemUIOverlayStyle(brightness == Brightness.dark
              ? SystemUiOverlayStyle.light
              : SystemUiOverlayStyle.dark);
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
      }
361
    }
362

363 364
    if (widget.toolbarOpacity != 1.0) {
      final double opacity = const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.toolbarOpacity);
365
      if (centerStyle?.color != null)
366
        centerStyle = centerStyle.copyWith(color: centerStyle.color.withOpacity(opacity));
367
      if (sideStyle?.color != null)
368
        sideStyle = sideStyle.copyWith(color: sideStyle.color.withOpacity(opacity));
369 370
      appBarIconTheme = appBarIconTheme.copyWith(
        opacity: opacity * (appBarIconTheme.opacity ?? 1.0)
Ian Hickson's avatar
Ian Hickson committed
371
      );
372 373
    }

374
    Widget leading = widget.leading;
375
    if (leading == null && widget.automaticallyImplyLeading) {
376
      if (hasDrawer) {
377
        leading = new IconButton(
378
          icon: const Icon(Icons.menu),
379
          onPressed: _handleDrawerButton,
380
          tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
381 382
        );
      } else {
383 384
        if (canPop)
          leading = useCloseButton ? const CloseButton() : const BackButton();
385 386
      }
    }
387
    if (leading != null) {
388 389 390
      leading = new ConstrainedBox(
        constraints: const BoxConstraints.tightFor(width: _kLeadingWidth),
        child: leading,
391 392
      );
    }
393

394 395
    Widget title = widget.title;
    if (title != null) {
396
      bool namesRoute;
397 398 399
      switch (defaultTargetPlatform) {
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
400
           namesRoute = true;
401 402 403 404
           break;
        case TargetPlatform.iOS:
          break;
      }
405 406 407 408
      title = new DefaultTextStyle(
        style: centerStyle,
        softWrap: false,
        overflow: TextOverflow.ellipsis,
409 410 411 412 413
        child: new Semantics(
          namesRoute: namesRoute,
          child: title,
          header: true,
        ),
414
      );
415
    }
416 417

    Widget actions;
418
    if (widget.actions != null && widget.actions.isNotEmpty) {
419 420 421 422
      actions = new Row(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: widget.actions,
423
      );
424 425 426 427 428
    } else if (hasEndDrawer) {
      actions = new IconButton(
        icon: const Icon(Icons.menu),
        onPressed: _handleDrawerButtonEnd,
        tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
429
      );
430
    }
431

432 433 434 435 436 437
    final Widget toolbar = new NavigationToolbar(
      leading: leading,
      middle: title,
      trailing: actions,
      centerMiddle: widget._getEffectiveCenterTitle(themeData),
      middleSpacing: widget.titleSpacing,
438
    );
439

440 441 442 443 444
    // 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(),
445
        child: IconTheme.merge(
446 447 448 449 450
          data: appBarIconTheme,
          child: new DefaultTextStyle(
            style: sideStyle,
            child: toolbar,
          ),
451 452
        ),
      ),
453 454
    );

455
    if (widget.bottom != null) {
456
      appBar = new Column(
457
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
458
        children: <Widget>[
459 460
          new Flexible(
            child: new ConstrainedBox(
461
              constraints: const BoxConstraints(maxHeight: kToolbarHeight),
462 463 464
              child: appBar,
            ),
          ),
465 466 467
          widget.bottomOpacity == 1.0 ? widget.bottom : new Opacity(
            opacity: const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.bottomOpacity),
            child: widget.bottom,
468 469
          ),
        ],
470 471 472
      );
    }

473
    // The padding applies to the toolbar and tabbar, not the flexible space.
474
    if (widget.primary) {
Ian Hickson's avatar
Ian Hickson committed
475 476
      appBar = new SafeArea(
        top: true,
477
        child: appBar,
478 479
      );
    }
480

481
    appBar = new Align(
482
      alignment: Alignment.topCenter,
483 484 485
      child: appBar,
    );

486
    if (widget.flexibleSpace != null) {
487
      appBar = new Stack(
488
        fit: StackFit.passthrough,
489
        children: <Widget>[
490
          widget.flexibleSpace,
491
          appBar,
492
        ],
493 494 495
      );
    }

496 497 498 499 500 501 502 503
    return new Semantics(
      container: true,
      explicitChildNodes: true,
      child: new Material(
        color: widget.backgroundColor ?? themeData.primaryColor,
        elevation: widget.elevation,
        child: appBar,
      ),
504 505
    );
  }
506 507
}

508
class _FloatingAppBar extends StatefulWidget {
509
  const _FloatingAppBar({ Key key, this.child }) : super(key: key);
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 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559

  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;
}

560 561 562
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate({
    @required this.leading,
563
    @required this.automaticallyImplyLeading,
564 565 566
    @required this.title,
    @required this.actions,
    @required this.flexibleSpace,
567
    @required this.bottom,
568
    @required this.elevation,
569
    @required this.forceElevated,
570 571 572 573 574 575
    @required this.backgroundColor,
    @required this.brightness,
    @required this.iconTheme,
    @required this.textTheme,
    @required this.primary,
    @required this.centerTitle,
576
    @required this.titleSpacing,
577
    @required this.expandedHeight,
578
    @required this.collapsedHeight,
579
    @required this.topPadding,
580
    @required this.floating,
581
    @required this.pinned,
582
    @required this.snapConfiguration,
583 584
  }) : assert(primary || topPadding == 0.0),
       _bottomHeight = bottom?.preferredSize?.height ?? 0.0;
585 586

  final Widget leading;
587
  final bool automaticallyImplyLeading;
588 589 590
  final Widget title;
  final List<Widget> actions;
  final Widget flexibleSpace;
591
  final PreferredSizeWidget bottom;
592
  final double elevation;
593
  final bool forceElevated;
594 595 596 597 598 599
  final Color backgroundColor;
  final Brightness brightness;
  final IconThemeData iconTheme;
  final TextTheme textTheme;
  final bool primary;
  final bool centerTitle;
600
  final double titleSpacing;
601
  final double expandedHeight;
602
  final double collapsedHeight;
603
  final double topPadding;
604
  final bool floating;
605 606 607 608 609
  final bool pinned;

  final double _bottomHeight;

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

612
  @override
613
  double get maxExtent => math.max(topPadding + (expandedHeight ?? kToolbarHeight + _bottomHeight), minExtent);
614

615 616 617
  @override
  final FloatingHeaderSnapConfiguration snapConfiguration;

618 619
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
620 621 622
    final double visibleMainHeight = maxExtent - shrinkOffset - topPadding;
    final double toolbarOpacity = pinned && !floating ? 1.0
      : ((visibleMainHeight - _bottomHeight) / kToolbarHeight).clamp(0.0, 1.0);
623
    final Widget appBar = FlexibleSpaceBar.createSettings(
624 625 626 627 628 629
      minExtent: minExtent,
      maxExtent: maxExtent,
      currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
      toolbarOpacity: toolbarOpacity,
      child: new AppBar(
        leading: leading,
630
        automaticallyImplyLeading: automaticallyImplyLeading,
631 632
        title: title,
        actions: actions,
633 634 635
        flexibleSpace: (title == null && flexibleSpace != null)
          ? new Semantics(child: flexibleSpace, header: true)
          : flexibleSpace,
636
        bottom: bottom,
637
        elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4.0 : 0.0,
638 639 640 641 642 643
        backgroundColor: backgroundColor,
        brightness: brightness,
        iconTheme: iconTheme,
        textTheme: textTheme,
        primary: primary,
        centerTitle: centerTitle,
644
        titleSpacing: titleSpacing,
645 646 647 648
        toolbarOpacity: toolbarOpacity,
        bottomOpacity: pinned ? 1.0 : (visibleMainHeight / _bottomHeight).clamp(0.0, 1.0),
      ),
    );
649
    return floating ? new _FloatingAppBar(child: appBar) : appBar;
650 651 652
  }

  @override
653
  bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) {
654
    return leading != oldDelegate.leading
655
        || automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading
656 657 658 659 660 661 662 663 664 665 666 667
        || 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
668
        || titleSpacing != oldDelegate.titleSpacing
669
        || expandedHeight != oldDelegate.expandedHeight
670 671
        || topPadding != oldDelegate.topPadding
        || pinned != oldDelegate.pinned
672 673
        || floating != oldDelegate.floating
        || snapConfiguration != oldDelegate.snapConfiguration;
674
  }
675 676 677

  @override
  String toString() {
678
    return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)';
679
  }
680 681
}

682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
/// 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.
///
699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
/// ## 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: () { /* ... */ },
///     ),
///   ]
/// )
/// ```
///
720 721 722 723 724 725 726 727 728 729 730 731
/// 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>
732
class SliverAppBar extends StatefulWidget {
733
  /// Creates a material design app bar that can be placed in a [CustomScrollView].
734 735 736
  ///
  /// The arguments [forceElevated], [primary], [floating], [pinned], [snap]
  /// and [automaticallyImplyLeading] must not be null.
737
  const SliverAppBar({
738 739
    Key key,
    this.leading,
740
    this.automaticallyImplyLeading = true,
741 742 743 744 745
    this.title,
    this.actions,
    this.flexibleSpace,
    this.bottom,
    this.elevation,
746
    this.forceElevated = false,
747 748 749 750
    this.backgroundColor,
    this.brightness,
    this.iconTheme,
    this.textTheme,
751
    this.primary = true,
752
    this.centerTitle,
753
    this.titleSpacing = NavigationToolbar.kMiddleSpacing,
754
    this.expandedHeight,
755 756 757
    this.floating = false,
    this.pinned = false,
    this.snap = false,
758 759
  }) : assert(automaticallyImplyLeading != null),
       assert(forceElevated != null),
760
       assert(primary != null),
761
       assert(titleSpacing != null),
762 763 764
       assert(floating != null),
       assert(pinned != null),
       assert(snap != null),
765
       assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
766
       super(key: key);
767 768 769

  /// A widget to display before the [title].
  ///
770 771 772 773 774 775
  /// 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].
776 777
  final Widget leading;

778 779 780 781 782 783 784
  /// 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;

785 786 787 788 789 790 791 792 793 794 795 796
  /// 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.
  ///
797
  /// ## Sample code
798 799
  ///
  /// ```dart
800 801
  /// new Scaffold(
  ///   body: new CustomScrollView(
802 803 804 805 806 807 808 809
  ///     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',
810 811 812
  ///             onPressed: () {
  ///               // handle the press
  ///             },
813 814 815 816 817 818
  ///           ),
  ///         ],
  ///       ),
  ///       // ...rest of body...
  ///     ],
  ///   ),
819
  /// )
820 821 822 823
  /// ```
  final List<Widget> actions;

  /// This widget is stacked behind the toolbar and the tabbar. It's height will
824
  /// be the same as the app bar's overall height.
825 826 827 828 829 830
  ///
  /// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details.
  final Widget flexibleSpace;

  /// This widget appears across the bottom of the appbar.
  ///
831 832 833 834 835 836 837
  /// 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;
838

839
  /// The z-coordinate at which to place this app bar when it is above other
840
  /// content. This controls the size of the shadow below the app bar.
841 842 843
  ///
  /// Defaults to 4, the appropriate elevation for app bars.
  ///
844 845 846 847
  /// 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].
848
  final double elevation;
849

850 851 852 853 854 855 856 857 858 859 860
  /// 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;

861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895
  /// 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;

896 897 898 899 900 901 902
  /// 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;

903 904 905 906 907 908 909 910 911 912
  /// 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;

913 914 915 916 917
  /// 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.
918
  ///
919 920 921
  /// 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.
922 923
  final bool floating;

924 925
  /// Whether the app bar should remain visible at the start of the scroll view.
  ///
926
  /// The app bar can still expand and contract as the user scrolls, but it will
927
  /// remain visible rather than being scrolled out of view.
928 929
  final bool pinned;

930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975
  /// 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();
  }

976 977
  @override
  Widget build(BuildContext context) {
978
    assert(!widget.primary || debugCheckHasMediaQuery(context));
979 980
    final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0;
    final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
981
      ? widget.bottom.preferredSize.height + topPadding : null;
982

983 984 985 986
    return new MediaQuery.removePadding(
      context: context,
      removeBottom: true,
      child: new SliverPersistentHeader(
987 988
        floating: widget.floating,
        pinned: widget.pinned,
989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011
        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,
        ),
1012 1013 1014
      ),
    );
  }
1015
}