| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'app_bar_theme.dart'; |
| import 'back_button.dart'; |
| import 'constants.dart'; |
| import 'debug.dart'; |
| import 'flexible_space_bar.dart'; |
| import 'icon_button.dart'; |
| import 'icons.dart'; |
| import 'material.dart'; |
| import 'material_localizations.dart'; |
| import 'scaffold.dart'; |
| import 'tabs.dart'; |
| import 'text_theme.dart'; |
| import 'theme.dart'; |
| |
| const double _kLeadingWidth = kToolbarHeight; // So the leading button is square. |
| |
| // 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 Size(constraints.maxWidth, kToolbarHeight); |
| } |
| |
| @override |
| Offset getPositionForChild(Size size, Size childSize) { |
| return Offset(0.0, size.height - childSize.height); |
| } |
| |
| @override |
| bool shouldRelayout(_ToolbarContainerLayout oldDelegate) => false; |
| } |
| |
| // TODO(eseidel): Toolbar needs to change size based on orientation: |
| // https://material.io/design/components/app-bars-top.html#specs |
| // Mobile Landscape: 48dp |
| // Mobile Portrait: 56dp |
| // Tablet/Desktop: 64dp |
| |
| /// A material design app bar. |
| /// |
| /// 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 (sometimes called the "overflow |
| /// menu"). |
| /// |
| /// 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]. |
| /// |
| /// When not used as [Scaffold.appBar], or when wrapped in a [Hero], place the app |
| /// bar in a [MediaQuery] to take care of the padding around the content of the |
| /// app bar if needed, as the padding will not be handled by [Scaffold]. |
| /// |
| /// 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/assets/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 |
| /// 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. |
| /// |
| /// {@tool dartpad --template=stateless_widget_material} |
| /// |
| /// This sample shows an [AppBar] with two simple actions. The first action |
| /// opens a [SnackBar], while the second action navigates to a new page. |
| /// |
| /// ```dart preamble |
| /// final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| /// final SnackBar snackBar = const SnackBar(content: Text('Showing Snackbar')); |
| /// |
| /// void openPage(BuildContext context) { |
| /// Navigator.push(context, MaterialPageRoute( |
| /// builder: (BuildContext context) { |
| /// return Scaffold( |
| /// appBar: AppBar( |
| /// title: const Text('Next page'), |
| /// ), |
| /// body: const Center( |
| /// child: Text( |
| /// 'This is the next page', |
| /// style: TextStyle(fontSize: 24), |
| /// ), |
| /// ), |
| /// ); |
| /// }, |
| /// )); |
| /// } |
| /// ``` |
| /// |
| /// ```dart |
| /// Widget build(BuildContext context) { |
| /// return Scaffold( |
| /// key: scaffoldKey, |
| /// appBar: AppBar( |
| /// title: const Text('AppBar Demo'), |
| /// actions: <Widget>[ |
| /// IconButton( |
| /// icon: const Icon(Icons.add_alert), |
| /// tooltip: 'Show Snackbar', |
| /// onPressed: () { |
| /// scaffoldKey.currentState.showSnackBar(snackBar); |
| /// }, |
| /// ), |
| /// IconButton( |
| /// icon: const Icon(Icons.navigate_next), |
| /// tooltip: 'Next page', |
| /// onPressed: () { |
| /// openPage(context); |
| /// }, |
| /// ), |
| /// ], |
| /// ), |
| /// body: const Center( |
| /// child: Text( |
| /// 'This is the home page', |
| /// style: TextStyle(fontSize: 24), |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [Scaffold], which displays the [AppBar] in its [Scaffold.appBar] slot. |
| /// * [SliverAppBar], which uses [AppBar] to provide a flexible app bar that |
| /// can be used in a [CustomScrollView]. |
| /// * [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.io/design/components/app-bars-top.html> |
| class AppBar extends StatefulWidget implements PreferredSizeWidget { |
| /// Creates a material design app bar. |
| /// |
| /// The arguments [primary], [toolbarOpacity], [bottomOpacity] |
| /// and [automaticallyImplyLeading] must not be null. Additionally, if |
| /// [elevation] is specified, it must be non-negative. |
| /// |
| /// If [backgroundColor], [elevation], [brightness], [iconTheme], |
| /// [actionsIconTheme], or [textTheme] are null, then their [AppBarTheme] |
| /// values will be used. If the corresponding [AppBarTheme] property is null, |
| /// then the default specified in the property's documentation will be used. |
| /// |
| /// Typically used in the [Scaffold.appBar] property. |
| AppBar({ |
| Key key, |
| this.leading, |
| this.automaticallyImplyLeading = true, |
| this.title, |
| this.actions, |
| this.flexibleSpace, |
| this.bottom, |
| this.elevation, |
| this.shape, |
| this.backgroundColor, |
| this.brightness, |
| this.iconTheme, |
| this.actionsIconTheme, |
| this.textTheme, |
| this.primary = true, |
| this.centerTitle, |
| this.excludeHeaderSemantics = false, |
| this.titleSpacing = NavigationToolbar.kMiddleSpacing, |
| this.toolbarOpacity = 1.0, |
| this.bottomOpacity = 1.0, |
| }) : assert(automaticallyImplyLeading != null), |
| assert(elevation == null || elevation >= 0.0), |
| assert(primary != null), |
| assert(titleSpacing != null), |
| assert(toolbarOpacity != null), |
| assert(bottomOpacity != null), |
| preferredSize = Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)), |
| super(key: key); |
| |
| /// A widget to display before the [title]. |
| /// |
| /// Typically the [leading] widget is an [Icon] or an [IconButton]. |
| /// |
| /// Becomes the leading component of the [NavigationToolBar] built |
| /// by this widget. The [leading] widget's width and height are constrained to |
| /// be no bigger than toolbar's height, which is [kToolbarHeight]. |
| /// |
| /// 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]. |
| /// |
| /// {@tool snippet} |
| /// |
| /// The following code shows how the drawer button could be manually specified |
| /// instead of relying on [automaticallyImplyLeading]: |
| /// |
| /// ```dart |
| /// AppBar( |
| /// leading: Builder( |
| /// builder: (BuildContext context) { |
| /// return IconButton( |
| /// icon: const Icon(Icons.menu), |
| /// onPressed: () { Scaffold.of(context).openDrawer(); }, |
| /// tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, |
| /// ); |
| /// }, |
| /// ), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// The [Builder] is used in this example to ensure that the `context` refers |
| /// to that part of the subtree. That way this code snippet can be used even |
| /// inside the very code that is creating the [Scaffold] (in which case, |
| /// without the [Builder], the `context` wouldn't be able to see the |
| /// [Scaffold], since it would refer to an ancestor of that widget). |
| /// |
| /// See also: |
| /// |
| /// * [Scaffold.appBar], in which an [AppBar] is usually placed. |
| /// * [Scaffold.drawer], in which the [Drawer] is usually placed. |
| final Widget leading; |
| |
| /// 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; |
| |
| /// The primary widget displayed in the app bar. |
| /// |
| /// Typically a [Text] widget that contains a description of the current |
| /// contents of the app. |
| /// |
| /// Becomes the middle component of the [NavigationToolBar] built by this widget. |
| /// The [title]'s width is constrained to fit within the remaining space |
| /// between the toolbar's [leading] and [actions] widgets. Its height is |
| /// _not_ constrained. The [title] is vertically centered and clipped to fit |
| /// within the toolbar, whose height is [kToolbarHeight]. Typically this |
| /// isn't noticeable because a simple [Text] [title] will fit within the |
| /// toolbar by default. On the other hand, it is noticeable when a |
| /// widget with an intrinsic height that is greater than [kToolbarHeight] |
| /// is used as the [title]. For example, when the height of an Image used |
| /// as the [title] exceeds [kToolbarHeight], it will be centered and |
| /// clipped (top and bottom), which may be undesirable. In cases like this |
| /// the height of the [title] widget can be constrained. For example: |
| /// |
| /// ```dart |
| /// MaterialApp( |
| /// home: Scaffold( |
| /// appBar: AppBar( |
| /// title: SizedBox( |
| /// height: kToolbarHeight, |
| /// child: child: Image.asset(logoAsset), |
| /// ), |
| /// ), |
| /// ) |
| /// ``` |
| final Widget title; |
| |
| /// Widgets to display in a row 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. |
| /// |
| /// The [actions] become the trailing component of the [NavigationToolBar] built |
| /// by this widget. The height of each action is constrained to be no bigger |
| /// than the toolbar's height, which is [kToolbarHeight]. |
| final List<Widget> actions; |
| |
| /// This widget is stacked behind the toolbar and the tab bar. It's height will |
| /// be the same as 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] |
| /// changes the [AppBar]'s height when scrolled. |
| /// |
| /// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details. |
| final Widget flexibleSpace; |
| |
| /// This widget appears across the bottom of the app bar. |
| /// |
| /// 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; |
| |
| /// The z-coordinate at which to place this app bar relative to its parent. |
| /// |
| /// This controls the size of the shadow below the app bar. |
| /// |
| /// The value is non-negative. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.elevation] is used, |
| /// if that is also null, the default value is 4, the appropriate elevation |
| /// for app bars. |
| final double elevation; |
| |
| /// The material's shape as well its shadow. |
| /// |
| /// A shadow is only displayed if the [elevation] is greater than |
| /// zero. |
| final ShapeBorder shape; |
| |
| /// The color to use for the app bar's material. Typically this should be set |
| /// along with [brightness], [iconTheme], [textTheme]. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.color] is used, |
| /// if that is also null, then [ThemeData.primaryColor] is used. |
| final Color backgroundColor; |
| |
| /// The brightness of the app bar's material. Typically this is set along |
| /// with [backgroundColor], [iconTheme], [textTheme]. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.brightness] is used, |
| /// if that is also null, then [ThemeData.primaryColorBrightness] is used. |
| final Brightness brightness; |
| |
| /// The color, opacity, and size to use for app bar icons. Typically this |
| /// is set along with [backgroundColor], [brightness], [textTheme]. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.iconTheme] is used, |
| /// if that is also null, then [ThemeData.primaryIconTheme] is used. |
| final IconThemeData iconTheme; |
| |
| /// The color, opacity, and size to use for the icons that appear in the app |
| /// bar's [actions]. This should only be used when the [actions] should be |
| /// themed differently than the icon that appears in the app bar's [leading] |
| /// widget. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.actionsIconTheme] is |
| /// used, if that is also null, then this falls back to [iconTheme]. |
| final IconThemeData actionsIconTheme; |
| |
| /// The typographic styles to use for text in the app bar. Typically this is |
| /// set along with [brightness] [backgroundColor], [iconTheme]. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.textTheme] is used, |
| /// if that is also null, then [ThemeData.primaryTextTheme] is used. |
| final TextTheme textTheme; |
| |
| /// Whether this app bar is being displayed at the top of the screen. |
| /// |
| /// If true, the app bar'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. |
| final bool primary; |
| |
| /// Whether the title should be centered. |
| /// |
| /// Defaults to being adapted to the current [TargetPlatform]. |
| final bool centerTitle; |
| |
| /// Whether the title should be wrapped with header [Semantics]. |
| /// |
| /// Defaults to false. |
| final bool excludeHeaderSemantics; |
| |
| /// 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; |
| |
| /// 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. |
| final double toolbarOpacity; |
| |
| /// 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. |
| final double bottomOpacity; |
| |
| /// A size whose height is the sum of [kToolbarHeight] and the [bottom] widget's |
| /// preferred height. |
| /// |
| /// [Scaffold] uses this size to set its app bar's height. |
| @override |
| final Size preferredSize; |
| |
| bool _getEffectiveCenterTitle(ThemeData theme) { |
| if (centerTitle != null) |
| return centerTitle; |
| assert(theme.platform != null); |
| switch (theme.platform) { |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| return false; |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| return actions == null || actions.length < 2; |
| } |
| return null; |
| } |
| |
| @override |
| _AppBarState createState() => _AppBarState(); |
| } |
| |
| class _AppBarState extends State<AppBar> { |
| static const double _defaultElevation = 4.0; |
| |
| void _handleDrawerButton() { |
| Scaffold.of(context).openDrawer(); |
| } |
| |
| void _handleDrawerButtonEnd() { |
| Scaffold.of(context).openEndDrawer(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(!widget.primary || debugCheckHasMediaQuery(context)); |
| assert(debugCheckHasMaterialLocalizations(context)); |
| final ThemeData theme = Theme.of(context); |
| final AppBarTheme appBarTheme = AppBarTheme.of(context); |
| final ScaffoldState scaffold = Scaffold.of(context, nullOk: true); |
| final ModalRoute<dynamic> parentRoute = ModalRoute.of(context); |
| |
| final bool hasDrawer = scaffold?.hasDrawer ?? false; |
| final bool hasEndDrawer = scaffold?.hasEndDrawer ?? false; |
| final bool canPop = parentRoute?.canPop ?? false; |
| final bool useCloseButton = parentRoute is PageRoute<dynamic> && parentRoute.fullscreenDialog; |
| |
| IconThemeData overallIconTheme = widget.iconTheme |
| ?? appBarTheme.iconTheme |
| ?? theme.primaryIconTheme; |
| IconThemeData actionsIconTheme = widget.actionsIconTheme |
| ?? appBarTheme.actionsIconTheme |
| ?? overallIconTheme; |
| TextStyle centerStyle = widget.textTheme?.headline6 |
| ?? appBarTheme.textTheme?.headline6 |
| ?? theme.primaryTextTheme.headline6; |
| TextStyle sideStyle = widget.textTheme?.bodyText2 |
| ?? appBarTheme.textTheme?.bodyText2 |
| ?? theme.primaryTextTheme.bodyText2; |
| |
| if (widget.toolbarOpacity != 1.0) { |
| final double opacity = const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.toolbarOpacity); |
| if (centerStyle?.color != null) |
| centerStyle = centerStyle.copyWith(color: centerStyle.color.withOpacity(opacity)); |
| if (sideStyle?.color != null) |
| sideStyle = sideStyle.copyWith(color: sideStyle.color.withOpacity(opacity)); |
| overallIconTheme = overallIconTheme.copyWith( |
| opacity: opacity * (overallIconTheme.opacity ?? 1.0) |
| ); |
| actionsIconTheme = actionsIconTheme.copyWith( |
| opacity: opacity * (actionsIconTheme.opacity ?? 1.0) |
| ); |
| } |
| |
| Widget leading = widget.leading; |
| if (leading == null && widget.automaticallyImplyLeading) { |
| if (hasDrawer) { |
| leading = IconButton( |
| icon: const Icon(Icons.menu), |
| onPressed: _handleDrawerButton, |
| tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, |
| ); |
| } else { |
| if (canPop) |
| leading = useCloseButton ? const CloseButton() : const BackButton(); |
| } |
| } |
| if (leading != null) { |
| leading = ConstrainedBox( |
| constraints: const BoxConstraints.tightFor(width: _kLeadingWidth), |
| child: leading, |
| ); |
| } |
| |
| Widget title = widget.title; |
| if (title != null) { |
| bool namesRoute; |
| switch (theme.platform) { |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| namesRoute = true; |
| break; |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| break; |
| } |
| |
| title = _AppBarTitleBox(child: title); |
| if (!widget.excludeHeaderSemantics) { |
| title = Semantics( |
| namesRoute: namesRoute, |
| child: title, |
| header: true, |
| ); |
| } |
| |
| title = DefaultTextStyle( |
| style: centerStyle, |
| softWrap: false, |
| overflow: TextOverflow.ellipsis, |
| child: title, |
| ); |
| } |
| |
| Widget actions; |
| if (widget.actions != null && widget.actions.isNotEmpty) { |
| actions = Row( |
| mainAxisSize: MainAxisSize.min, |
| crossAxisAlignment: CrossAxisAlignment.stretch, |
| children: widget.actions, |
| ); |
| } else if (hasEndDrawer) { |
| actions = IconButton( |
| icon: const Icon(Icons.menu), |
| onPressed: _handleDrawerButtonEnd, |
| tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, |
| ); |
| } |
| |
| // Allow the trailing actions to have their own theme if necessary. |
| if (actions != null) { |
| actions = IconTheme.merge( |
| data: actionsIconTheme, |
| child: actions, |
| ); |
| } |
| |
| final Widget toolbar = NavigationToolbar( |
| leading: leading, |
| middle: title, |
| trailing: actions, |
| centerMiddle: widget._getEffectiveCenterTitle(theme), |
| middleSpacing: widget.titleSpacing, |
| ); |
| |
| // If the toolbar is allocated less than kToolbarHeight make it |
| // appear to scroll upwards within its shrinking container. |
| Widget appBar = ClipRect( |
| child: CustomSingleChildLayout( |
| delegate: const _ToolbarContainerLayout(), |
| child: IconTheme.merge( |
| data: overallIconTheme, |
| child: DefaultTextStyle( |
| style: sideStyle, |
| child: toolbar, |
| ), |
| ), |
| ), |
| ); |
| if (widget.bottom != null) { |
| appBar = Column( |
| mainAxisAlignment: MainAxisAlignment.spaceBetween, |
| children: <Widget>[ |
| Flexible( |
| child: ConstrainedBox( |
| constraints: const BoxConstraints(maxHeight: kToolbarHeight), |
| child: appBar, |
| ), |
| ), |
| if (widget.bottomOpacity == 1.0) |
| widget.bottom |
| else |
| Opacity( |
| opacity: const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.bottomOpacity), |
| child: widget.bottom, |
| ), |
| ], |
| ); |
| } |
| |
| // The padding applies to the toolbar and tabbar, not the flexible space. |
| if (widget.primary) { |
| appBar = SafeArea( |
| bottom: false, |
| top: true, |
| child: appBar, |
| ); |
| } |
| |
| appBar = Align( |
| alignment: Alignment.topCenter, |
| child: appBar, |
| ); |
| |
| if (widget.flexibleSpace != null) { |
| appBar = Stack( |
| fit: StackFit.passthrough, |
| children: <Widget>[ |
| widget.flexibleSpace, |
| appBar, |
| ], |
| ); |
| } |
| final Brightness brightness = widget.brightness |
| ?? appBarTheme.brightness |
| ?? theme.primaryColorBrightness; |
| final SystemUiOverlayStyle overlayStyle = brightness == Brightness.dark |
| ? SystemUiOverlayStyle.light |
| : SystemUiOverlayStyle.dark; |
| |
| return Semantics( |
| container: true, |
| child: AnnotatedRegion<SystemUiOverlayStyle>( |
| value: overlayStyle, |
| child: Material( |
| color: widget.backgroundColor |
| ?? appBarTheme.color |
| ?? theme.primaryColor, |
| elevation: widget.elevation |
| ?? appBarTheme.elevation |
| ?? _defaultElevation, |
| shape: widget.shape, |
| child: Semantics( |
| explicitChildNodes: true, |
| child: appBar, |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class _FloatingAppBar extends StatefulWidget { |
| const _FloatingAppBar({ Key key, this.child }) : super(key: key); |
| |
| final Widget child; |
| |
| @override |
| _FloatingAppBarState createState() => _FloatingAppBarState(); |
| } |
| |
| // A wrapper for the widget created by _SliverAppBarDelegate that starts and |
| // stops the floating app bar'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.findAncestorRenderObjectOfType<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; |
| } |
| |
| class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { |
| _SliverAppBarDelegate({ |
| @required this.leading, |
| @required this.automaticallyImplyLeading, |
| @required this.title, |
| @required this.actions, |
| @required this.flexibleSpace, |
| @required this.bottom, |
| @required this.elevation, |
| @required this.forceElevated, |
| @required this.backgroundColor, |
| @required this.brightness, |
| @required this.iconTheme, |
| @required this.actionsIconTheme, |
| @required this.textTheme, |
| @required this.primary, |
| @required this.centerTitle, |
| @required this.excludeHeaderSemantics, |
| @required this.titleSpacing, |
| @required this.expandedHeight, |
| @required this.collapsedHeight, |
| @required this.topPadding, |
| @required this.floating, |
| @required this.pinned, |
| @required this.snapConfiguration, |
| @required this.stretchConfiguration, |
| @required this.shape, |
| }) : assert(primary || topPadding == 0.0), |
| _bottomHeight = bottom?.preferredSize?.height ?? 0.0; |
| |
| final Widget leading; |
| final bool automaticallyImplyLeading; |
| final Widget title; |
| final List<Widget> actions; |
| final Widget flexibleSpace; |
| final PreferredSizeWidget bottom; |
| final double elevation; |
| final bool forceElevated; |
| final Color backgroundColor; |
| final Brightness brightness; |
| final IconThemeData iconTheme; |
| final IconThemeData actionsIconTheme; |
| final TextTheme textTheme; |
| final bool primary; |
| final bool centerTitle; |
| final bool excludeHeaderSemantics; |
| final double titleSpacing; |
| final double expandedHeight; |
| final double collapsedHeight; |
| final double topPadding; |
| final bool floating; |
| final bool pinned; |
| final ShapeBorder shape; |
| |
| final double _bottomHeight; |
| |
| @override |
| double get minExtent => collapsedHeight ?? (topPadding + kToolbarHeight + _bottomHeight); |
| |
| @override |
| double get maxExtent => math.max(topPadding + (expandedHeight ?? kToolbarHeight + _bottomHeight), minExtent); |
| |
| @override |
| final FloatingHeaderSnapConfiguration snapConfiguration; |
| |
| @override |
| final OverScrollHeaderStretchConfiguration stretchConfiguration; |
| |
| @override |
| Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { |
| final double visibleMainHeight = maxExtent - shrinkOffset - topPadding; |
| |
| // Truth table for `toolbarOpacity`: |
| // pinned | floating | bottom != null || opacity |
| // ---------------------------------------------- |
| // 0 | 0 | 0 || fade |
| // 0 | 0 | 1 || fade |
| // 0 | 1 | 0 || fade |
| // 0 | 1 | 1 || fade |
| // 1 | 0 | 0 || 1.0 |
| // 1 | 0 | 1 || 1.0 |
| // 1 | 1 | 0 || 1.0 |
| // 1 | 1 | 1 || fade |
| final double toolbarOpacity = !pinned || (floating && bottom != null) |
| ? ((visibleMainHeight - _bottomHeight) / kToolbarHeight).clamp(0.0, 1.0) as double |
| : 1.0; |
| |
| final Widget appBar = FlexibleSpaceBar.createSettings( |
| minExtent: minExtent, |
| maxExtent: maxExtent, |
| currentExtent: math.max(minExtent, maxExtent - shrinkOffset), |
| toolbarOpacity: toolbarOpacity, |
| child: AppBar( |
| leading: leading, |
| automaticallyImplyLeading: automaticallyImplyLeading, |
| title: title, |
| actions: actions, |
| flexibleSpace: (title == null && flexibleSpace != null && !excludeHeaderSemantics) |
| ? Semantics(child: flexibleSpace, header: true) |
| : flexibleSpace, |
| bottom: bottom, |
| elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4.0 : 0.0, |
| backgroundColor: backgroundColor, |
| brightness: brightness, |
| iconTheme: iconTheme, |
| actionsIconTheme: actionsIconTheme, |
| textTheme: textTheme, |
| primary: primary, |
| centerTitle: centerTitle, |
| excludeHeaderSemantics: excludeHeaderSemantics, |
| titleSpacing: titleSpacing, |
| shape: shape, |
| toolbarOpacity: toolbarOpacity, |
| bottomOpacity: pinned ? 1.0 : ((visibleMainHeight / _bottomHeight).clamp(0.0, 1.0) as double), |
| ), |
| ); |
| return floating ? _FloatingAppBar(child: appBar) : appBar; |
| } |
| |
| @override |
| bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) { |
| return leading != oldDelegate.leading |
| || automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading |
| || 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 |
| || actionsIconTheme != oldDelegate.actionsIconTheme |
| || textTheme != oldDelegate.textTheme |
| || primary != oldDelegate.primary |
| || centerTitle != oldDelegate.centerTitle |
| || titleSpacing != oldDelegate.titleSpacing |
| || expandedHeight != oldDelegate.expandedHeight |
| || topPadding != oldDelegate.topPadding |
| || pinned != oldDelegate.pinned |
| || floating != oldDelegate.floating |
| || snapConfiguration != oldDelegate.snapConfiguration |
| || stretchConfiguration != oldDelegate.stretchConfiguration; |
| } |
| |
| @override |
| String toString() { |
| return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)'; |
| } |
| } |
| |
| /// 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. |
| /// |
| /// {@youtube 560 315 https://www.youtube.com/watch?v=R9C5KMJKluE} |
| /// |
| /// 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. |
| /// |
| /// {@tool snippet} |
| /// |
| /// This is an example that could be included in a [CustomScrollView]'s |
| /// [CustomScrollView.slivers] list: |
| /// |
| /// ```dart |
| /// SliverAppBar( |
| /// expandedHeight: 150.0, |
| /// flexibleSpace: const FlexibleSpaceBar( |
| /// title: Text('Available seats'), |
| /// ), |
| /// actions: <Widget>[ |
| /// IconButton( |
| /// icon: const Icon(Icons.add_circle), |
| /// tooltip: 'Add new entry', |
| /// onPressed: () { /* ... */ }, |
| /// ), |
| /// ] |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// ## Animated Examples |
| /// |
| /// The following animations show how app bars with different configurations |
| /// behave when a user scrolls up and then down again. |
| /// |
| /// * App bar with [floating]: false, [pinned]: false, [snap]: false: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar.mp4} |
| /// |
| /// * App bar with [floating]: true, [pinned]: false, [snap]: false: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating.mp4} |
| /// |
| /// * App bar with [floating]: true, [pinned]: false, [snap]: true: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating_snap.mp4} |
| /// |
| /// * App bar with [floating]: true, [pinned]: true, [snap]: false: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_pinned_floating.mp4} |
| /// |
| /// * App bar with [floating]: true, [pinned]: true, [snap]: true: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_pinned_floating_snap.mp4} |
| /// |
| /// * App bar with [floating]: false, [pinned]: true, [snap]: false: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_pinned.mp4} |
| /// |
| /// The property [snap] can only be set to true if [floating] is also true. |
| /// |
| /// 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.io/design/components/app-bars-top.html> |
| class SliverAppBar extends StatefulWidget { |
| /// Creates a material design app bar that can be placed in a [CustomScrollView]. |
| /// |
| /// The arguments [forceElevated], [primary], [floating], [pinned], [snap] |
| /// and [automaticallyImplyLeading] must not be null. |
| const SliverAppBar({ |
| Key key, |
| this.leading, |
| this.automaticallyImplyLeading = true, |
| this.title, |
| this.actions, |
| this.flexibleSpace, |
| this.bottom, |
| this.elevation, |
| this.forceElevated = false, |
| this.backgroundColor, |
| this.brightness, |
| this.iconTheme, |
| this.actionsIconTheme, |
| this.textTheme, |
| this.primary = true, |
| this.centerTitle, |
| this.excludeHeaderSemantics = false, |
| this.titleSpacing = NavigationToolbar.kMiddleSpacing, |
| this.expandedHeight, |
| this.floating = false, |
| this.pinned = false, |
| this.snap = false, |
| this.stretch = false, |
| this.stretchTriggerOffset = 100.0, |
| this.onStretchTrigger, |
| this.shape, |
| }) : assert(automaticallyImplyLeading != null), |
| assert(forceElevated != null), |
| assert(primary != null), |
| assert(titleSpacing != null), |
| assert(floating != null), |
| assert(pinned != null), |
| assert(snap != null), |
| assert(stretch != null), |
| assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'), |
| assert(stretchTriggerOffset > 0.0), |
| super(key: key); |
| |
| /// A widget to display before the [title]. |
| /// |
| /// 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]. |
| final Widget leading; |
| |
| /// 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; |
| |
| /// The primary widget displayed in the app bar. |
| /// |
| /// 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. |
| /// |
| /// {@tool snippet} |
| /// |
| /// ```dart |
| /// Scaffold( |
| /// body: CustomScrollView( |
| /// primary: true, |
| /// slivers: <Widget>[ |
| /// SliverAppBar( |
| /// title: Text('Hello World'), |
| /// actions: <Widget>[ |
| /// IconButton( |
| /// icon: Icon(Icons.shopping_cart), |
| /// tooltip: 'Open shopping cart', |
| /// onPressed: () { |
| /// // handle the press |
| /// }, |
| /// ), |
| /// ], |
| /// ), |
| /// // ...rest of body... |
| /// ], |
| /// ), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| final List<Widget> actions; |
| |
| /// This widget is stacked behind the toolbar and the tab bar. It's height will |
| /// be the same as the app bar's overall height. |
| /// |
| /// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details. |
| final Widget flexibleSpace; |
| |
| /// This widget appears across the bottom of the app bar. |
| /// |
| /// 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; |
| |
| /// The z-coordinate at which to place this app bar when it is above other |
| /// content. This controls the size of the shadow below the app bar. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.elevation] is used, |
| /// if that is also null, the default value is 4, the appropriate elevation |
| /// for app bars. |
| /// |
| /// 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]. |
| final double elevation; |
| |
| /// 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; |
| |
| /// The color to use for the app bar's material. Typically this should be set |
| /// along with [brightness], [iconTheme], [textTheme]. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.color] is used, |
| /// if that is also null, then [ThemeData.primaryColor] is used. |
| final Color backgroundColor; |
| |
| /// The brightness of the app bar's material. Typically this is set along |
| /// with [backgroundColor], [iconTheme], [textTheme]. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.brightness] is used, |
| /// if that is also null, then [ThemeData.primaryColorBrightness] is used. |
| final Brightness brightness; |
| |
| /// The color, opacity, and size to use for app bar icons. Typically this |
| /// is set along with [backgroundColor], [brightness], [textTheme]. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.iconTheme] is used, |
| /// if that is also null, then [ThemeData.primaryIconTheme] is used. |
| final IconThemeData iconTheme; |
| |
| /// The color, opacity, and size to use for trailing app bar icons. This |
| /// should only be used when the trailing icons should be themed differently |
| /// than the leading icons. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.actionsIconTheme] is |
| /// used, if that is also null, then this falls back to [iconTheme]. |
| final IconThemeData actionsIconTheme; |
| |
| /// The typographic styles to use for text in the app bar. Typically this is |
| /// set along with [brightness] [backgroundColor], [iconTheme]. |
| /// |
| /// If this property is null, then [ThemeData.appBarTheme.textTheme] is used, |
| /// if that is also null, then [ThemeData.primaryTextTheme] is used. |
| 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; |
| |
| /// Whether the title should be wrapped with header [Semantics]. |
| /// |
| /// Defaults to false. |
| final bool excludeHeaderSemantics; |
| |
| /// 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; |
| |
| /// 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; |
| |
| /// 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. |
| /// |
| /// 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. |
| /// |
| /// ## Animated Examples |
| /// |
| /// The following animations show how the app bar changes its scrolling |
| /// behavior based on the value of this property. |
| /// |
| /// * App bar with [floating] set to false: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar.mp4} |
| /// * App bar with [floating] set to true: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating.mp4} |
| /// |
| /// See also: |
| /// |
| /// * [SliverAppBar] for more animated examples of how this property changes the |
| /// behavior of the app bar in combination with [pinned] and [snap]. |
| final bool floating; |
| |
| /// Whether the app bar should remain visible at the start of the scroll view. |
| /// |
| /// The app bar can still expand and contract as the user scrolls, but it will |
| /// remain visible rather than being scrolled out of view. |
| /// |
| /// ## Animated Examples |
| /// |
| /// The following animations show how the app bar changes its scrolling |
| /// behavior based on the value of this property. |
| /// |
| /// * App bar with [pinned] set to false: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar.mp4} |
| /// * App bar with [pinned] set to true: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_pinned.mp4} |
| /// |
| /// See also: |
| /// |
| /// * [SliverAppBar] for more animated examples of how this property changes the |
| /// behavior of the app bar in combination with [floating]. |
| final bool pinned; |
| |
| /// The material's shape as well as its shadow. |
| /// |
| /// A shadow is only displayed if the [elevation] is greater than zero. |
| final ShapeBorder shape; |
| |
| /// 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 app bar |
| /// appears at the top of its scroll view. |
| /// |
| /// ## Animated Examples |
| /// |
| /// The following animations show how the app bar changes its scrolling |
| /// behavior based on the value of this property. |
| /// |
| /// * App bar with [snap] set to false: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating.mp4} |
| /// * App bar with [snap] set to true: |
| /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating_snap.mp4} |
| /// |
| /// See also: |
| /// |
| /// * [SliverAppBar] for more animated examples of how this property changes the |
| /// behavior of the app bar in combination with [pinned] and [floating]. |
| final bool snap; |
| |
| /// Whether the app bar should stretch to fill the over-scroll area. |
| /// |
| /// The app bar can still expand and contract as the user scrolls, but it will |
| /// also stretch when the user over-scrolls. |
| final bool stretch; |
| |
| /// The offset of overscroll required to activate [onStretchTrigger]. |
| /// |
| /// This defaults to 100.0. |
| final double stretchTriggerOffset; |
| |
| /// The callback function to be executed when a user over-scrolls to the |
| /// offset specified by [stretchTriggerOffset]. |
| final AsyncCallback onStretchTrigger; |
| |
| @override |
| _SliverAppBarState createState() => _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; |
| OverScrollHeaderStretchConfiguration _stretchConfiguration; |
| |
| void _updateSnapConfiguration() { |
| if (widget.snap && widget.floating) { |
| _snapConfiguration = FloatingHeaderSnapConfiguration( |
| vsync: this, |
| curve: Curves.easeOut, |
| duration: const Duration(milliseconds: 200), |
| ); |
| } else { |
| _snapConfiguration = null; |
| } |
| } |
| |
| void _updateStretchConfiguration() { |
| if (widget.stretch) { |
| _stretchConfiguration = OverScrollHeaderStretchConfiguration( |
| stretchTriggerOffset: widget.stretchTriggerOffset, |
| onStretchTrigger: widget.onStretchTrigger, |
| ); |
| } else { |
| _stretchConfiguration = null; |
| } |
| } |
| |
| @override |
| void initState() { |
| super.initState(); |
| _updateSnapConfiguration(); |
| _updateStretchConfiguration(); |
| } |
| |
| @override |
| void didUpdateWidget(SliverAppBar oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (widget.snap != oldWidget.snap || widget.floating != oldWidget.floating) |
| _updateSnapConfiguration(); |
| if (widget.stretch != oldWidget.stretch) |
| _updateStretchConfiguration(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(!widget.primary || debugCheckHasMediaQuery(context)); |
| final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0; |
| final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null) |
| ? widget.bottom.preferredSize.height + topPadding : null; |
| |
| return MediaQuery.removePadding( |
| context: context, |
| removeBottom: true, |
| child: SliverPersistentHeader( |
| floating: widget.floating, |
| pinned: widget.pinned, |
| delegate: _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, |
| actionsIconTheme: widget.actionsIconTheme, |
| textTheme: widget.textTheme, |
| primary: widget.primary, |
| centerTitle: widget.centerTitle, |
| excludeHeaderSemantics: widget.excludeHeaderSemantics, |
| titleSpacing: widget.titleSpacing, |
| expandedHeight: widget.expandedHeight, |
| collapsedHeight: collapsedHeight, |
| topPadding: topPadding, |
| floating: widget.floating, |
| pinned: widget.pinned, |
| shape: widget.shape, |
| snapConfiguration: _snapConfiguration, |
| stretchConfiguration: _stretchConfiguration, |
| ), |
| ), |
| ); |
| } |
| } |
| |
| // Layout the AppBar's title with unconstrained height, vertically |
| // center it within its (NavigationToolbar) parent, and allow the |
| // parent to constrain the title's actual height. |
| class _AppBarTitleBox extends SingleChildRenderObjectWidget { |
| const _AppBarTitleBox({ Key key, @required Widget child }) : assert(child != null), super(key: key, child: child); |
| |
| @override |
| _RenderAppBarTitleBox createRenderObject(BuildContext context) { |
| return _RenderAppBarTitleBox( |
| textDirection: Directionality.of(context), |
| ); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderAppBarTitleBox renderObject) { |
| renderObject.textDirection = Directionality.of(context); |
| } |
| } |
| |
| class _RenderAppBarTitleBox extends RenderAligningShiftedBox { |
| _RenderAppBarTitleBox({ |
| RenderBox child, |
| TextDirection textDirection, |
| }) : super(child: child, alignment: Alignment.center, textDirection: textDirection); |
| |
| @override |
| void performLayout() { |
| final BoxConstraints constraints = this.constraints; |
| final BoxConstraints innerConstraints = constraints.copyWith(maxHeight: double.infinity); |
| child.layout(innerConstraints, parentUsesSize: true); |
| size = constraints.constrain(child.size); |
| alignChild(); |
| } |
| } |