app_bar.dart 35.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 '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

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

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

379 380
    Widget title = widget.title;
    if (title != null) {
381
      bool namesRoute;
382 383 384
      switch (defaultTargetPlatform) {
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
385
           namesRoute = true;
386 387 388 389
           break;
        case TargetPlatform.iOS:
          break;
      }
390 391 392 393
      title = new DefaultTextStyle(
        style: centerStyle,
        softWrap: false,
        overflow: TextOverflow.ellipsis,
394 395 396 397 398
        child: new Semantics(
          namesRoute: namesRoute,
          child: title,
          header: true,
        ),
399
      );
400
    }
401 402

    Widget actions;
403
    if (widget.actions != null && widget.actions.isNotEmpty) {
404 405 406 407
      actions = new Row(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: widget.actions,
408
      );
409 410 411 412 413
    } else if (hasEndDrawer) {
      actions = new IconButton(
        icon: const Icon(Icons.menu),
        onPressed: _handleDrawerButtonEnd,
        tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
414
      );
415
    }
416

417 418 419 420 421 422
    final Widget toolbar = new NavigationToolbar(
      leading: leading,
      middle: title,
      trailing: actions,
      centerMiddle: widget._getEffectiveCenterTitle(themeData),
      middleSpacing: widget.titleSpacing,
423
    );
424

425 426 427 428 429
    // 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(),
430
        child: IconTheme.merge(
431 432 433 434 435
          data: appBarIconTheme,
          child: new DefaultTextStyle(
            style: sideStyle,
            child: toolbar,
          ),
436 437
        ),
      ),
438
    );
439
    if (widget.bottom != null) {
440
      appBar = new Column(
441
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
442
        children: <Widget>[
443 444
          new Flexible(
            child: new ConstrainedBox(
445
              constraints: const BoxConstraints(maxHeight: kToolbarHeight),
446 447 448
              child: appBar,
            ),
          ),
449 450 451
          widget.bottomOpacity == 1.0 ? widget.bottom : new Opacity(
            opacity: const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.bottomOpacity),
            child: widget.bottom,
452 453
          ),
        ],
454 455 456
      );
    }

457
    // The padding applies to the toolbar and tabbar, not the flexible space.
458
    if (widget.primary) {
Ian Hickson's avatar
Ian Hickson committed
459 460
      appBar = new SafeArea(
        top: true,
461
        child: appBar,
462 463
      );
    }
464

465
    appBar = new Align(
466
      alignment: Alignment.topCenter,
467 468 469
      child: appBar,
    );

470
    if (widget.flexibleSpace != null) {
471
      appBar = new Stack(
472
        fit: StackFit.passthrough,
473
        children: <Widget>[
474
          widget.flexibleSpace,
475
          appBar,
476
        ],
477 478
      );
    }
479 480
    final Brightness brightness = widget.brightness ?? themeData.primaryColorBrightness;
    final SystemUiOverlayStyle overlayStyle = brightness == Brightness.dark
481 482
        ? SystemUiOverlayStyle.light
        : SystemUiOverlayStyle.dark;
483

484 485 486
    return new Semantics(
      container: true,
      explicitChildNodes: true,
487 488 489 490 491 492 493
      child: new AnnotatedRegion<SystemUiOverlayStyle>(
        value: overlayStyle,
        child: new Material(
          color: widget.backgroundColor ?? themeData.primaryColor,
          elevation: widget.elevation,
          child: appBar,
        ),
494
      ),
495 496
    );
  }
497 498
}

499
class _FloatingAppBar extends StatefulWidget {
500
  const _FloatingAppBar({ Key key, this.child }) : super(key: key);
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 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550

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

551 552 553
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate({
    @required this.leading,
554
    @required this.automaticallyImplyLeading,
555 556 557
    @required this.title,
    @required this.actions,
    @required this.flexibleSpace,
558
    @required this.bottom,
559
    @required this.elevation,
560
    @required this.forceElevated,
561 562 563 564 565 566
    @required this.backgroundColor,
    @required this.brightness,
    @required this.iconTheme,
    @required this.textTheme,
    @required this.primary,
    @required this.centerTitle,
567
    @required this.titleSpacing,
568
    @required this.expandedHeight,
569
    @required this.collapsedHeight,
570
    @required this.topPadding,
571
    @required this.floating,
572
    @required this.pinned,
573
    @required this.snapConfiguration,
574 575
  }) : assert(primary || topPadding == 0.0),
       _bottomHeight = bottom?.preferredSize?.height ?? 0.0;
576 577

  final Widget leading;
578
  final bool automaticallyImplyLeading;
579 580 581
  final Widget title;
  final List<Widget> actions;
  final Widget flexibleSpace;
582
  final PreferredSizeWidget bottom;
583
  final double elevation;
584
  final bool forceElevated;
585 586 587 588 589 590
  final Color backgroundColor;
  final Brightness brightness;
  final IconThemeData iconTheme;
  final TextTheme textTheme;
  final bool primary;
  final bool centerTitle;
591
  final double titleSpacing;
592
  final double expandedHeight;
593
  final double collapsedHeight;
594
  final double topPadding;
595
  final bool floating;
596 597 598 599 600
  final bool pinned;

  final double _bottomHeight;

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

603
  @override
604
  double get maxExtent => math.max(topPadding + (expandedHeight ?? kToolbarHeight + _bottomHeight), minExtent);
605

606 607 608
  @override
  final FloatingHeaderSnapConfiguration snapConfiguration;

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

  @override
644
  bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) {
645
    return leading != oldDelegate.leading
646
        || automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading
647 648 649 650 651 652 653 654 655 656 657 658
        || 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
659
        || titleSpacing != oldDelegate.titleSpacing
660
        || expandedHeight != oldDelegate.expandedHeight
661 662
        || topPadding != oldDelegate.topPadding
        || pinned != oldDelegate.pinned
663 664
        || floating != oldDelegate.floating
        || snapConfiguration != oldDelegate.snapConfiguration;
665
  }
666 667 668

  @override
  String toString() {
669
    return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)';
670
  }
671 672
}

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

  /// A widget to display before the [title].
  ///
761 762 763 764 765 766
  /// 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].
767 768
  final Widget leading;

769 770 771 772 773 774 775
  /// 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;

776 777 778 779 780 781 782 783 784 785 786 787
  /// 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.
  ///
788
  /// ## Sample code
789 790
  ///
  /// ```dart
791 792
  /// new Scaffold(
  ///   body: new CustomScrollView(
793 794 795 796 797 798 799 800
  ///     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',
801 802 803
  ///             onPressed: () {
  ///               // handle the press
  ///             },
804 805 806 807 808 809
  ///           ),
  ///         ],
  ///       ),
  ///       // ...rest of body...
  ///     ],
  ///   ),
810
  /// )
811 812 813 814
  /// ```
  final List<Widget> actions;

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

  /// This widget appears across the bottom of the appbar.
  ///
822 823 824 825 826 827 828
  /// 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;
829

830
  /// The z-coordinate at which to place this app bar when it is above other
831
  /// content. This controls the size of the shadow below the app bar.
832 833 834
  ///
  /// Defaults to 4, the appropriate elevation for app bars.
  ///
835 836 837 838
  /// 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].
839
  final double elevation;
840

841 842 843 844 845 846 847 848 849 850 851
  /// 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;

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

887 888 889 890 891 892 893
  /// 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;

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

904 905 906 907 908
  /// 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.
909
  ///
910 911 912
  /// 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.
913 914
  final bool floating;

915 916
  /// Whether the app bar should remain visible at the start of the scroll view.
  ///
917
  /// The app bar can still expand and contract as the user scrolls, but it will
918
  /// remain visible rather than being scrolled out of view.
919 920
  final bool pinned;

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 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966
  /// 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();
  }

967 968
  @override
  Widget build(BuildContext context) {
969
    assert(!widget.primary || debugCheckHasMediaQuery(context));
970 971
    final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0;
    final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
972
      ? widget.bottom.preferredSize.height + topPadding : null;
973

974 975 976 977
    return new MediaQuery.removePadding(
      context: context,
      removeBottom: true,
      child: new SliverPersistentHeader(
978 979
        floating: widget.floating,
        pinned: widget.pinned,
980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002
        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,
        ),
1003 1004 1005
      ),
    );
  }
1006
}