app_bar.dart 33.3 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:math' as math;

7
import 'package:flutter/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 'page.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 151
    this.toolbarOpacity: 1.0,
    this.bottomOpacity: 1.0,
152 153
  }) : assert(automaticallyImplyLeading != null),
       assert(elevation != null),
154 155 156 157 158
       assert(primary != null),
       assert(toolbarOpacity != null),
       assert(bottomOpacity != null),
       preferredSize = new Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)),
       super(key: key);
159

160 161
  /// A widget to display before the [title].
  ///
162 163 164 165 166 167
  /// 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].
168
  final Widget leading;
169

170 171 172 173 174 175 176
  /// 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;

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

183
  /// Widgets to display after the [title] widget.
184 185 186 187
  ///
  /// Typically these widgets are [IconButton]s representing common operations.
  /// For less common operations, consider using a [PopupMenuButton] as the
  /// last action.
188 189 190 191 192 193 194 195 196 197 198 199 200
  ///
  /// 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,
  ///       ),
201
  ///     ],
202 203 204 205
  ///   ),
  ///   body: _buildBody(),
  /// );
  /// ```
206
  final List<Widget> actions;
207

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

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

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

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

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

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

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

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

270 271 272 273 274 275 276
  /// 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.
277
  final double toolbarOpacity;
278

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

288 289
  /// A size whose height is the sum of [kToolbarHeight] and the [bottom] widget's
  /// preferred height.
290
  ///
291 292 293
  /// [Scaffold] uses this this size to set its app bar's height.
  @override
  final Size preferredSize;
294

295 296 297 298 299 300
  bool _getEffectiveCenterTitle(ThemeData themeData) {
    if (centerTitle != null)
      return centerTitle;
    assert(themeData.platform != null);
    switch (themeData.platform) {
      case TargetPlatform.android:
301
      case TargetPlatform.fuchsia:
302 303
        return false;
      case TargetPlatform.iOS:
304
        return actions == null || actions.length < 2;
305 306 307 308
    }
    return null;
  }

309
  @override
310
  _AppBarState createState() => new _AppBarState();
311 312
}

313
class _AppBarState extends State<AppBar> {
314 315 316 317 318 319
  void _handleDrawerButton() {
    Scaffold.of(context).openDrawer();
  }

  @override
  Widget build(BuildContext context) {
320
    assert(!widget.primary || debugCheckHasMediaQuery(context));
321
    final ThemeData themeData = Theme.of(context);
322 323 324 325 326 327
    final ScaffoldState scaffold = Scaffold.of(context, nullOk: true);
    final ModalRoute<dynamic> parentRoute = ModalRoute.of(context);

    final bool hasDrawer = scaffold?.hasDrawer ?? false;
    final bool canPop = parentRoute?.canPop ?? false;
    final bool useCloseButton = parentRoute is MaterialPageRoute<dynamic> && parentRoute.fullscreenDialog;
328

329 330 331
    IconThemeData appBarIconTheme = widget.iconTheme ?? themeData.primaryIconTheme;
    TextStyle centerStyle = widget.textTheme?.title ?? themeData.primaryTextTheme.title;
    TextStyle sideStyle = widget.textTheme?.body1 ?? themeData.primaryTextTheme.body1;
332

333
    final Brightness brightness = widget.brightness ?? themeData.primaryColorBrightness;
334
    SystemChrome.setSystemUIOverlayStyle(brightness == Brightness.dark
335 336
      ? SystemUiOverlayStyle.light
      : SystemUiOverlayStyle.dark);
337

338 339
    if (widget.toolbarOpacity != 1.0) {
      final double opacity = const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.toolbarOpacity);
340
      if (centerStyle?.color != null)
341
        centerStyle = centerStyle.copyWith(color: centerStyle.color.withOpacity(opacity));
342
      if (sideStyle?.color != null)
343
        sideStyle = sideStyle.copyWith(color: sideStyle.color.withOpacity(opacity));
344 345
      appBarIconTheme = appBarIconTheme.copyWith(
        opacity: opacity * (appBarIconTheme.opacity ?? 1.0)
Ian Hickson's avatar
Ian Hickson committed
346
      );
347 348
    }

349
    Widget leading = widget.leading;
350
    if (leading == null && widget.automaticallyImplyLeading) {
351
      if (hasDrawer) {
352
        leading = new IconButton(
353
          icon: const Icon(Icons.menu),
354 355 356 357
          onPressed: _handleDrawerButton,
          tooltip: 'Open navigation menu' // TODO(ianh): Figure out how to localize this string
        );
      } else {
358 359
        if (canPop)
          leading = useCloseButton ? const CloseButton() : const BackButton();
360 361
      }
    }
362
    if (leading != null) {
363 364 365
      leading = new ConstrainedBox(
        constraints: const BoxConstraints.tightFor(width: _kLeadingWidth),
        child: leading,
366 367
      );
    }
368

369 370 371 372 373 374 375
    Widget title = widget.title;
    if (title != null) {
      title = new DefaultTextStyle(
        style: centerStyle,
        softWrap: false,
        overflow: TextOverflow.ellipsis,
        child: title,
376
      );
377
    }
378 379

    Widget actions;
380
    if (widget.actions != null && widget.actions.isNotEmpty) {
381 382 383 384
      actions = new Row(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: widget.actions,
385 386
      );
    }
387

388
    final Widget toolbar = new Padding(
389
      padding: const EdgeInsets.only(right: 4.0),
390 391 392 393 394
      child: new NavigationToolbar(
        leading: leading,
        middle: title,
        trailing: actions,
        centerMiddle: widget._getEffectiveCenterTitle(themeData),
395
      ),
396
    );
397

398 399 400 401 402
    // 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(),
403
        child: IconTheme.merge(
404 405 406 407 408
          data: appBarIconTheme,
          child: new DefaultTextStyle(
            style: sideStyle,
            child: toolbar,
          ),
409 410
        ),
      ),
411 412
    );

413
    if (widget.bottom != null) {
414
      appBar = new Column(
415
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
416
        children: <Widget>[
417 418
          new Flexible(
            child: new ConstrainedBox(
419
              constraints: const BoxConstraints(maxHeight: kToolbarHeight),
420 421 422
              child: appBar,
            ),
          ),
423 424 425
          widget.bottomOpacity == 1.0 ? widget.bottom : new Opacity(
            opacity: const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.bottomOpacity),
            child: widget.bottom,
426 427
          ),
        ],
428 429 430
      );
    }

431
    // The padding applies to the toolbar and tabbar, not the flexible space.
432
    if (widget.primary) {
433 434 435
      appBar = new Padding(
        padding: new EdgeInsets.only(top: MediaQuery.of(context).padding.top),
        child: appBar,
436 437
      );
    }
438

439 440 441 442 443
    appBar = new Align(
      alignment: FractionalOffset.topCenter,
      child: appBar,
    );

444
    if (widget.flexibleSpace != null) {
445
      appBar = new Stack(
446
        fit: StackFit.passthrough,
447
        children: <Widget>[
448
          widget.flexibleSpace,
449
          appBar,
450
        ],
451 452 453
      );
    }

454
    return new Material(
455 456
      color: widget.backgroundColor ?? themeData.primaryColor,
      elevation: widget.elevation,
457
      child: appBar,
458 459
    );
  }
460 461
}

462
class _FloatingAppBar extends StatefulWidget {
463
  const _FloatingAppBar({ Key key, this.child }) : super(key: key);
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 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

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

514 515 516
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate({
    @required this.leading,
517
    @required this.automaticallyImplyLeading,
518 519 520
    @required this.title,
    @required this.actions,
    @required this.flexibleSpace,
521
    @required this.bottom,
522
    @required this.elevation,
523
    @required this.forceElevated,
524 525 526 527 528 529 530
    @required this.backgroundColor,
    @required this.brightness,
    @required this.iconTheme,
    @required this.textTheme,
    @required this.primary,
    @required this.centerTitle,
    @required this.expandedHeight,
531
    @required this.collapsedHeight,
532
    @required this.topPadding,
533
    @required this.floating,
534
    @required this.pinned,
535
    @required this.snapConfiguration,
536 537
  }) : assert(primary || topPadding == 0.0),
       _bottomHeight = bottom?.preferredSize?.height ?? 0.0;
538 539

  final Widget leading;
540
  final bool automaticallyImplyLeading;
541 542 543
  final Widget title;
  final List<Widget> actions;
  final Widget flexibleSpace;
544
  final PreferredSizeWidget bottom;
545
  final double elevation;
546
  final bool forceElevated;
547 548 549 550 551 552 553
  final Color backgroundColor;
  final Brightness brightness;
  final IconThemeData iconTheme;
  final TextTheme textTheme;
  final bool primary;
  final bool centerTitle;
  final double expandedHeight;
554
  final double collapsedHeight;
555
  final double topPadding;
556
  final bool floating;
557 558 559 560 561
  final bool pinned;

  final double _bottomHeight;

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

564
  @override
565
  double get maxExtent => math.max(topPadding + (expandedHeight ?? kToolbarHeight + _bottomHeight), minExtent);
566

567 568 569
  @override
  final FloatingHeaderSnapConfiguration snapConfiguration;

570 571
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
572 573 574
    final double visibleMainHeight = maxExtent - shrinkOffset - topPadding;
    final double toolbarOpacity = pinned && !floating ? 1.0
      : ((visibleMainHeight - _bottomHeight) / kToolbarHeight).clamp(0.0, 1.0);
575
    final Widget appBar = FlexibleSpaceBar.createSettings(
576 577 578 579 580 581
      minExtent: minExtent,
      maxExtent: maxExtent,
      currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
      toolbarOpacity: toolbarOpacity,
      child: new AppBar(
        leading: leading,
582
        automaticallyImplyLeading: automaticallyImplyLeading,
583 584 585 586
        title: title,
        actions: actions,
        flexibleSpace: flexibleSpace,
        bottom: bottom,
587
        elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4.0 : 0.0,
588 589 590 591 592 593 594 595 596 597
        backgroundColor: backgroundColor,
        brightness: brightness,
        iconTheme: iconTheme,
        textTheme: textTheme,
        primary: primary,
        centerTitle: centerTitle,
        toolbarOpacity: toolbarOpacity,
        bottomOpacity: pinned ? 1.0 : (visibleMainHeight / _bottomHeight).clamp(0.0, 1.0),
      ),
    );
598
    return floating ? new _FloatingAppBar(child: appBar) : appBar;
599 600 601
  }

  @override
602
  bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) {
603
    return leading != oldDelegate.leading
604
        || automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading
605 606 607 608 609 610 611 612 613 614 615 616 617
        || 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
        || expandedHeight != oldDelegate.expandedHeight
618 619
        || topPadding != oldDelegate.topPadding
        || pinned != oldDelegate.pinned
620 621
        || floating != oldDelegate.floating
        || snapConfiguration != oldDelegate.snapConfiguration;
622
  }
623 624 625

  @override
  String toString() {
626
    return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)';
627
  }
628 629
}

630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646
/// 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.
///
647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667
/// ## 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: () { /* ... */ },
///     ),
///   ]
/// )
/// ```
///
668 669 670 671 672 673 674 675 676 677 678 679
/// 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>
680
class SliverAppBar extends StatefulWidget {
681
  /// Creates a material design app bar that can be placed in a [CustomScrollView].
682 683 684
  ///
  /// The arguments [forceElevated], [primary], [floating], [pinned], [snap]
  /// and [automaticallyImplyLeading] must not be null.
685
  const SliverAppBar({
686 687
    Key key,
    this.leading,
688
    this.automaticallyImplyLeading: true,
689 690 691 692 693
    this.title,
    this.actions,
    this.flexibleSpace,
    this.bottom,
    this.elevation,
694
    this.forceElevated: false,
695 696 697 698 699 700 701 702 703
    this.backgroundColor,
    this.brightness,
    this.iconTheme,
    this.textTheme,
    this.primary: true,
    this.centerTitle,
    this.expandedHeight,
    this.floating: false,
    this.pinned: false,
704
    this.snap: false,
705 706
  }) : assert(automaticallyImplyLeading != null),
       assert(forceElevated != null),
707
       assert(primary != null),
708 709
       assert(floating != null),
       assert(pinned != null),
710
       assert(!pinned || !floating || bottom != null, 'A pinned and floating app bar must have a bottom widget.'),
711
       assert(snap != null),
712
       assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
713
       super(key: key);
714 715 716

  /// A widget to display before the [title].
  ///
717 718 719 720 721 722
  /// 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].
723 724
  final Widget leading;

725 726 727 728 729 730 731
  /// 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;

732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756
  /// 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',
757 758 759
  ///             onPressed: () {
  ///               // handle the press
  ///             },
760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777
  ///           ),
  ///         ],
  ///       ),
  ///       // ...rest of body...
  ///     ],
  ///   ),
  /// );
  /// ```
  final List<Widget> actions;

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

  /// This widget appears across the bottom of the appbar.
  ///
778 779 780 781 782 783 784
  /// 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;
785

786
  /// The z-coordinate at which to place this app bar when it is above other
787
  /// content. This controls the size of the shadow below the app bar.
788 789 790
  ///
  /// Defaults to 4, the appropriate elevation for app bars.
  ///
791 792 793 794
  /// 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].
795
  final double elevation;
796

797 798 799 800 801 802 803 804 805 806 807
  /// 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;

808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852
  /// 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;

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

853 854 855 856 857
  /// 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.
858
  ///
859 860 861
  /// 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.
862 863
  final bool floating;

864 865 866 867
  /// 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.
868 869
  final bool pinned;

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 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915
  /// 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();
  }

916 917
  @override
  Widget build(BuildContext context) {
918
    assert(!widget.primary || debugCheckHasMediaQuery(context));
919 920
    final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0;
    final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
921
      ? widget.bottom.preferredSize.height + topPadding : null;
922

923
    return new SliverPersistentHeader(
924 925
      floating: widget.floating,
      pinned: widget.pinned,
926
      delegate: new _SliverAppBarDelegate(
927
        leading: widget.leading,
928
        automaticallyImplyLeading: widget.automaticallyImplyLeading,
929 930 931 932 933
        title: widget.title,
        actions: widget.actions,
        flexibleSpace: widget.flexibleSpace,
        bottom: widget.bottom,
        elevation: widget.elevation,
934
        forceElevated: widget.forceElevated,
935 936 937 938 939 940 941
        backgroundColor: widget.backgroundColor,
        brightness: widget.brightness,
        iconTheme: widget.iconTheme,
        textTheme: widget.textTheme,
        primary: widget.primary,
        centerTitle: widget.centerTitle,
        expandedHeight: widget.expandedHeight,
942 943
        collapsedHeight: collapsedHeight,
        topPadding: topPadding,
944 945 946
        floating: widget.floating,
        pinned: widget.pinned,
        snapConfiguration: _snapConfiguration,
947 948 949
      ),
    );
  }
950
}