| // 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 'dart:ui' as ui; |
| |
| import 'package:flutter/foundation.dart' show clampDouble; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'colors.dart'; |
| import 'constants.dart'; |
| import 'theme.dart'; |
| |
| /// The collapsing effect while the space bar collapses from its full size. |
| enum CollapseMode { |
| /// The background widget will scroll in a parallax fashion. |
| parallax, |
| |
| /// The background widget pin in place until it reaches the min extent. |
| pin, |
| |
| /// The background widget will act as normal with no collapsing effect. |
| none, |
| } |
| |
| /// The stretching effect while the space bar stretches beyond its full size. |
| enum StretchMode { |
| /// The background widget will expand to fill the extra space. |
| zoomBackground, |
| |
| /// The background will blur using a [ImageFilter.blur] effect. |
| blurBackground, |
| |
| /// The title will fade away as the user over-scrolls. |
| fadeTitle, |
| } |
| |
| /// The part of a Material Design [AppBar] that expands, collapses, and |
| /// stretches. |
| /// |
| /// {@youtube 560 315 https://www.youtube.com/watch?v=mSc7qFzxHDw} |
| /// |
| /// Most commonly used in the [SliverAppBar.flexibleSpace] field, a flexible |
| /// space bar expands and contracts as the app scrolls so that the [AppBar] |
| /// reaches from the top of the app to the top of the scrolling contents of the |
| /// app. When using [SliverAppBar.flexibleSpace], the [SliverAppBar.expandedHeight] |
| /// must be large enough to accommodate the [SliverAppBar.flexibleSpace] widget. |
| /// |
| /// Furthermore is included functionality for stretch behavior. When |
| /// [SliverAppBar.stretch] is true, and your [ScrollPhysics] allow for |
| /// overscroll, this space will stretch with the overscroll. |
| /// |
| /// The widget that sizes the [AppBar] must wrap it in the widget returned by |
| /// [FlexibleSpaceBar.createSettings], to convey sizing information down to the |
| /// [FlexibleSpaceBar]. |
| /// |
| /// {@tool dartpad} |
| /// This sample application demonstrates the different features of the |
| /// [FlexibleSpaceBar] when used in a [SliverAppBar]. This app bar is configured |
| /// to stretch into the overscroll space, and uses the |
| /// [FlexibleSpaceBar.stretchModes] to apply `fadeTitle`, `blurBackground` and |
| /// `zoomBackground`. The app bar also makes use of [CollapseMode.parallax] by |
| /// default. |
| /// |
| /// ** See code in examples/api/lib/material/flexible_space_bar/flexible_space_bar.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [SliverAppBar], which implements the expanding and contracting. |
| /// * [AppBar], which is used by [SliverAppBar]. |
| /// * <https://material.io/design/components/app-bars-top.html#behavior> |
| class FlexibleSpaceBar extends StatefulWidget { |
| /// Creates a flexible space bar. |
| /// |
| /// Most commonly used in the [AppBar.flexibleSpace] field. |
| const FlexibleSpaceBar({ |
| super.key, |
| this.title, |
| this.background, |
| this.centerTitle, |
| this.titlePadding, |
| this.collapseMode = CollapseMode.parallax, |
| this.stretchModes = const <StretchMode>[StretchMode.zoomBackground], |
| this.expandedTitleScale = 1.5, |
| }) : assert(expandedTitleScale >= 1); |
| |
| /// The primary contents of the flexible space bar when expanded. |
| /// |
| /// Typically a [Text] widget. |
| final Widget? title; |
| |
| /// Shown behind the [title] when expanded. |
| /// |
| /// Typically an [Image] widget with [Image.fit] set to [BoxFit.cover]. |
| final Widget? background; |
| |
| /// Whether the title should be centered. |
| /// |
| /// By default this property is true if the current target platform |
| /// is [TargetPlatform.iOS] or [TargetPlatform.macOS], false otherwise. |
| final bool? centerTitle; |
| |
| /// Collapse effect while scrolling. |
| /// |
| /// Defaults to [CollapseMode.parallax]. |
| final CollapseMode collapseMode; |
| |
| /// Stretch effect while over-scrolling. |
| /// |
| /// Defaults to include [StretchMode.zoomBackground]. |
| final List<StretchMode> stretchModes; |
| |
| /// Defines how far the [title] is inset from either the widget's |
| /// bottom-left or its center. |
| /// |
| /// Typically this property is used to adjust how far the title is |
| /// inset from the bottom-left and it is specified along with |
| /// [centerTitle] false. |
| /// |
| /// By default the value of this property is |
| /// `EdgeInsetsDirectional.only(start: 72, bottom: 16)` if the title is |
| /// not centered, `EdgeInsetsDirectional.only(start: 0, bottom: 16)` otherwise. |
| final EdgeInsetsGeometry? titlePadding; |
| |
| /// Defines how much the title is scaled when the FlexibleSpaceBar is expanded |
| /// due to the user scrolling downwards. The title is scaled uniformly on the |
| /// x and y axes while maintaining its bottom-left position (bottom-center if |
| /// [centerTitle] is true). |
| /// |
| /// Defaults to 1.5 and must be greater than 1. |
| final double expandedTitleScale; |
| |
| /// Wraps a widget that contains an [AppBar] to convey sizing information down |
| /// to the [FlexibleSpaceBar]. |
| /// |
| /// Used by [Scaffold] and [SliverAppBar]. |
| /// |
| /// `toolbarOpacity` affects how transparent the text within the toolbar |
| /// appears. `minExtent` sets the minimum height of the resulting |
| /// [FlexibleSpaceBar] when fully collapsed. `maxExtent` sets the maximum |
| /// height of the resulting [FlexibleSpaceBar] when fully expanded. |
| /// `currentExtent` sets the scale of the [FlexibleSpaceBar.background] and |
| /// [FlexibleSpaceBar.title] widgets of [FlexibleSpaceBar] upon |
| /// initialization. `scrolledUnder` is true if the [FlexibleSpaceBar] |
| /// overlaps the app's primary scrollable, false if it does not, and null |
| /// if the caller has not determined as much. |
| /// See also: |
| /// |
| /// * [FlexibleSpaceBarSettings] which creates a settings object that can be |
| /// used to specify these settings to a [FlexibleSpaceBar]. |
| static Widget createSettings({ |
| double? toolbarOpacity, |
| double? minExtent, |
| double? maxExtent, |
| bool? isScrolledUnder, |
| required double currentExtent, |
| required Widget child, |
| }) { |
| return FlexibleSpaceBarSettings( |
| toolbarOpacity: toolbarOpacity ?? 1.0, |
| minExtent: minExtent ?? currentExtent, |
| maxExtent: maxExtent ?? currentExtent, |
| isScrolledUnder: isScrolledUnder, |
| currentExtent: currentExtent, |
| child: child, |
| ); |
| } |
| |
| @override |
| State<FlexibleSpaceBar> createState() => _FlexibleSpaceBarState(); |
| } |
| |
| class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> { |
| bool _getEffectiveCenterTitle(ThemeData theme) { |
| if (widget.centerTitle != null) { |
| return widget.centerTitle!; |
| } |
| switch (theme.platform) { |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| return false; |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| return true; |
| } |
| } |
| |
| Alignment _getTitleAlignment(bool effectiveCenterTitle) { |
| if (effectiveCenterTitle) { |
| return Alignment.bottomCenter; |
| } |
| final TextDirection textDirection = Directionality.of(context); |
| switch (textDirection) { |
| case TextDirection.rtl: |
| return Alignment.bottomRight; |
| case TextDirection.ltr: |
| return Alignment.bottomLeft; |
| } |
| } |
| |
| double _getCollapsePadding(double t, FlexibleSpaceBarSettings settings) { |
| switch (widget.collapseMode) { |
| case CollapseMode.pin: |
| return -(settings.maxExtent - settings.currentExtent); |
| case CollapseMode.none: |
| return 0.0; |
| case CollapseMode.parallax: |
| final double deltaExtent = settings.maxExtent - settings.minExtent; |
| return -Tween<double>(begin: 0.0, end: deltaExtent / 4.0).transform(t); |
| } |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return LayoutBuilder( |
| builder: (BuildContext context, BoxConstraints constraints) { |
| final FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!; |
| |
| final List<Widget> children = <Widget>[]; |
| |
| final double deltaExtent = settings.maxExtent - settings.minExtent; |
| |
| // 0.0 -> Expanded |
| // 1.0 -> Collapsed to toolbar |
| final double t = clampDouble(1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent, 0.0, 1.0); |
| |
| // background |
| if (widget.background != null) { |
| final double fadeStart = math.max(0.0, 1.0 - kToolbarHeight / deltaExtent); |
| const double fadeEnd = 1.0; |
| assert(fadeStart <= fadeEnd); |
| // If the min and max extent are the same, the app bar cannot collapse |
| // and the content should be visible, so opacity = 1. |
| final double opacity = settings.maxExtent == settings.minExtent |
| ? 1.0 |
| : 1.0 - Interval(fadeStart, fadeEnd).transform(t); |
| double height = settings.maxExtent; |
| |
| // StretchMode.zoomBackground |
| if (widget.stretchModes.contains(StretchMode.zoomBackground) && |
| constraints.maxHeight > height) { |
| height = constraints.maxHeight; |
| } |
| final double topPadding = _getCollapsePadding(t, settings); |
| children.add(Positioned( |
| top: topPadding, |
| left: 0.0, |
| right: 0.0, |
| height: height, |
| child: _FlexibleSpaceHeaderOpacity( |
| // IOS is relying on this semantics node to correctly traverse |
| // through the app bar when it is collapsed. |
| alwaysIncludeSemantics: true, |
| opacity: opacity, |
| child: widget.background, |
| ), |
| )); |
| |
| // StretchMode.blurBackground |
| if (widget.stretchModes.contains(StretchMode.blurBackground) && |
| constraints.maxHeight > settings.maxExtent) { |
| final double blurAmount = (constraints.maxHeight - settings.maxExtent) / 10; |
| children.add(Positioned.fill( |
| child: BackdropFilter( |
| filter: ui.ImageFilter.blur( |
| sigmaX: blurAmount, |
| sigmaY: blurAmount, |
| ), |
| child: Container( |
| color: Colors.transparent, |
| ), |
| ), |
| )); |
| } |
| } |
| |
| // title |
| if (widget.title != null) { |
| final ThemeData theme = Theme.of(context); |
| |
| Widget? title; |
| switch (theme.platform) { |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| title = widget.title; |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| title = Semantics( |
| namesRoute: true, |
| child: widget.title, |
| ); |
| } |
| |
| // StretchMode.fadeTitle |
| if (widget.stretchModes.contains(StretchMode.fadeTitle) && |
| constraints.maxHeight > settings.maxExtent) { |
| final double stretchOpacity = 1 - |
| clampDouble( |
| (constraints.maxHeight - settings.maxExtent) / 100, |
| 0.0, |
| 1.0); |
| title = Opacity( |
| opacity: stretchOpacity, |
| child: title, |
| ); |
| } |
| |
| final double opacity = settings.toolbarOpacity; |
| if (opacity > 0.0) { |
| TextStyle titleStyle = theme.primaryTextTheme.titleLarge!; |
| titleStyle = titleStyle.copyWith( |
| color: titleStyle.color!.withOpacity(opacity), |
| ); |
| final bool effectiveCenterTitle = _getEffectiveCenterTitle(theme); |
| final EdgeInsetsGeometry padding = widget.titlePadding ?? |
| EdgeInsetsDirectional.only( |
| start: effectiveCenterTitle ? 0.0 : 72.0, |
| bottom: 16.0, |
| ); |
| final double scaleValue = Tween<double>(begin: widget.expandedTitleScale, end: 1.0).transform(t); |
| final Matrix4 scaleTransform = Matrix4.identity() |
| ..scale(scaleValue, scaleValue, 1.0); |
| final Alignment titleAlignment = _getTitleAlignment(effectiveCenterTitle); |
| children.add(Container( |
| padding: padding, |
| child: Transform( |
| alignment: titleAlignment, |
| transform: scaleTransform, |
| child: Align( |
| alignment: titleAlignment, |
| child: DefaultTextStyle( |
| style: titleStyle, |
| child: LayoutBuilder( |
| builder: (BuildContext context, BoxConstraints constraints) { |
| return Container( |
| width: constraints.maxWidth / scaleValue, |
| alignment: titleAlignment, |
| child: title, |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| )); |
| } |
| } |
| |
| return ClipRect(child: Stack(children: children)); |
| }, |
| ); |
| } |
| } |
| |
| /// Provides sizing and opacity information to a [FlexibleSpaceBar]. |
| /// |
| /// See also: |
| /// |
| /// * [FlexibleSpaceBar] which creates a flexible space bar. |
| class FlexibleSpaceBarSettings extends InheritedWidget { |
| /// Creates a Flexible Space Bar Settings widget. |
| /// |
| /// Used by [Scaffold] and [SliverAppBar]. [child] must have a |
| /// [FlexibleSpaceBar] widget in its tree for the settings to take affect. |
| /// |
| /// The required [toolbarOpacity], [minExtent], [maxExtent], [currentExtent], |
| /// and [child] parameters must not be null. |
| const FlexibleSpaceBarSettings({ |
| super.key, |
| required this.toolbarOpacity, |
| required this.minExtent, |
| required this.maxExtent, |
| required this.currentExtent, |
| required super.child, |
| this.isScrolledUnder, |
| }) : assert(minExtent >= 0), |
| assert(maxExtent >= 0), |
| assert(currentExtent >= 0), |
| assert(toolbarOpacity >= 0.0), |
| assert(minExtent <= maxExtent), |
| assert(minExtent <= currentExtent), |
| assert(currentExtent <= maxExtent); |
| |
| /// Affects how transparent the text within the toolbar appears. |
| final double toolbarOpacity; |
| |
| /// Minimum height of the resulting [FlexibleSpaceBar] when fully collapsed. |
| final double minExtent; |
| |
| /// Maximum height of the resulting [FlexibleSpaceBar] when fully expanded. |
| final double maxExtent; |
| |
| /// If the [FlexibleSpaceBar.title] or the [FlexibleSpaceBar.background] is |
| /// not null, then this value is used to calculate the relative scale of |
| /// these elements upon initialization. |
| final double currentExtent; |
| |
| /// True if the FlexibleSpaceBar overlaps the primary scrollable's contents. |
| /// |
| /// This value is used by the [AppBar] to resolve |
| /// [AppBar.backgroundColor] against [MaterialState.scrolledUnder], |
| /// i.e. to enable apps to specify different colors when content |
| /// has been scrolled up and behind the app bar. |
| /// |
| /// Null if the caller hasn't determined if the FlexibleSpaceBar |
| /// overlaps the primary scrollable's contents. |
| final bool? isScrolledUnder; |
| |
| @override |
| bool updateShouldNotify(FlexibleSpaceBarSettings oldWidget) { |
| return toolbarOpacity != oldWidget.toolbarOpacity |
| || minExtent != oldWidget.minExtent |
| || maxExtent != oldWidget.maxExtent |
| || currentExtent != oldWidget.currentExtent |
| || isScrolledUnder != oldWidget.isScrolledUnder; |
| } |
| } |
| |
| // We need the child widget to repaint, however both the opacity |
| // and potentially `widget.background` can be constant which won't |
| // lead to repainting. |
| // see: https://github.com/flutter/flutter/issues/127836 |
| class _FlexibleSpaceHeaderOpacity extends SingleChildRenderObjectWidget { |
| const _FlexibleSpaceHeaderOpacity({required this.opacity, required super.child, required this.alwaysIncludeSemantics}); |
| |
| final double opacity; |
| final bool alwaysIncludeSemantics; |
| |
| @override |
| RenderObject createRenderObject(BuildContext context) { |
| return _RenderFlexibleSpaceHeaderOpacity(opacity: opacity, alwaysIncludeSemantics: alwaysIncludeSemantics); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, covariant _RenderFlexibleSpaceHeaderOpacity renderObject) { |
| renderObject |
| ..alwaysIncludeSemantics = alwaysIncludeSemantics |
| ..opacity = opacity; |
| } |
| } |
| |
| class _RenderFlexibleSpaceHeaderOpacity extends RenderOpacity { |
| _RenderFlexibleSpaceHeaderOpacity({super.opacity, super.alwaysIncludeSemantics}); |
| |
| @override |
| bool get isRepaintBoundary => false; |
| } |