flexible_space_bar.dart 9.64 KB
Newer Older
1 2 3 4 5 6
// Copyright 2016 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.

import 'dart:math' as math;

7
import 'package:flutter/foundation.dart';
8 9 10 11 12
import 'package:flutter/widgets.dart';

import 'constants.dart';
import 'theme.dart';

13 14 15 16 17 18 19 20 21 22 23 24
/// The collapsing effect while the space bar expands or collapses.
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,
}

25 26
/// The part of a material design [AppBar] that expands and collapses.
///
27 28 29 30
/// Most commonly used in 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.
31
///
32 33 34
/// The widget that sizes the [AppBar] must wrap it in the widget returned by
/// [FlexibleSpaceBar.createSettings], to convey sizing information down to the
/// [FlexibleSpaceBar].
35 36
///
/// See also:
Ian Hickson's avatar
Ian Hickson committed
37
///
38 39
///  * [SliverAppBar], which implements the expanding and contracting.
///  * [AppBar], which is used by [SliverAppBar].
40
///  * <https://material.io/design/components/app-bars-top.html#behavior>
41
class FlexibleSpaceBar extends StatefulWidget {
42 43
  /// Creates a flexible space bar.
  ///
44
  /// Most commonly used in the [AppBar.flexibleSpace] field.
45
  const FlexibleSpaceBar({
46 47 48
    Key key,
    this.title,
    this.background,
49
    this.centerTitle,
50
    this.titlePadding,
51
    this.collapseMode = CollapseMode.parallax,
52 53
  }) : assert(collapseMode != null),
       super(key: key);
54

55 56 57
  /// The primary contents of the flexible space bar when expanded.
  ///
  /// Typically a [Text] widget.
58
  final Widget title;
59 60 61

  /// Shown behind the [title] when expanded.
  ///
62
  /// Typically an [Image] widget with [Image.fit] set to [BoxFit.cover].
63
  final Widget background;
64

65 66
  /// Whether the title should be centered.
  ///
67 68
  /// By default this property is true if the current target platform
  /// is [TargetPlatform.iOS], false otherwise.
69 70
  final bool centerTitle;

71 72 73 74 75
  /// Collapse effect while scrolling.
  ///
  /// Defaults to [CollapseMode.parallax].
  final CollapseMode collapseMode;

76 77 78 79 80 81 82 83 84 85 86 87
  /// 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
  /// 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;

88 89 90 91
  /// Wraps a widget that contains an [AppBar] to convey sizing information down
  /// to the [FlexibleSpaceBar].
  ///
  /// Used by [Scaffold] and [SliverAppBar].
92 93 94 95 96 97 98 99 100 101 102
  ///
  /// `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.
  ///
  /// See also:
  ///
103 104
  ///  * [FlexibleSpaceBarSettings] which creates a settings object that can be
  ///    used to specify these settings to a [FlexibleSpaceBar].
105 106 107 108 109 110 111 112
  static Widget createSettings({
    double toolbarOpacity,
    double minExtent,
    double maxExtent,
    @required double currentExtent,
    @required Widget child,
  }) {
    assert(currentExtent != null);
113
    return FlexibleSpaceBarSettings(
114 115 116 117 118 119 120 121
      toolbarOpacity: toolbarOpacity ?? 1.0,
      minExtent: minExtent ?? currentExtent,
      maxExtent: maxExtent ?? currentExtent,
      currentExtent: currentExtent,
      child: child,
    );
  }

122
  @override
123
  _FlexibleSpaceBarState createState() => _FlexibleSpaceBarState();
124 125 126
}

class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
127
  bool _getEffectiveCenterTitle(ThemeData theme) {
128 129
    if (widget.centerTitle != null)
      return widget.centerTitle;
130 131 132
    assert(theme.platform != null);
    switch (theme.platform) {
      case TargetPlatform.android:
133
      case TargetPlatform.fuchsia:
134 135 136 137
        return false;
      case TargetPlatform.iOS:
        return true;
    }
138
    return null;
139 140
  }

141
  Alignment _getTitleAlignment(bool effectiveCenterTitle) {
142
    if (effectiveCenterTitle)
143
      return Alignment.bottomCenter;
144 145 146 147
    final TextDirection textDirection = Directionality.of(context);
    assert(textDirection != null);
    switch (textDirection) {
      case TextDirection.rtl:
148
        return Alignment.bottomRight;
149
      case TextDirection.ltr:
150
        return Alignment.bottomLeft;
151 152 153 154
    }
    return null;
  }

155
  double _getCollapsePadding(double t, FlexibleSpaceBarSettings settings) {
156 157 158 159 160 161 162
    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;
163
        return -Tween<double>(begin: 0.0, end: deltaExtent / 4.0).transform(t);
164 165 166 167
    }
    return null;
  }

168 169
  @override
  Widget build(BuildContext context) {
170
    final FlexibleSpaceBarSettings settings = context.inheritFromWidgetOfExactType(FlexibleSpaceBarSettings);
171 172 173
    assert(settings != null, 'A FlexibleSpaceBar must be wrapped in the widget returned by FlexibleSpaceBar.createSettings().');

    final List<Widget> children = <Widget>[];
174

175
    final double deltaExtent = settings.maxExtent - settings.minExtent;
176 177 178

    // 0.0 -> Expanded
    // 1.0 -> Collapsed to toolbar
179
    final double t = (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent).clamp(0.0, 1.0);
180 181

    // background image
182
    if (widget.background != null) {
183
      final double fadeStart = math.max(0.0, 1.0 - kToolbarHeight / deltaExtent);
184
      const double fadeEnd = 1.0;
185
      assert(fadeStart <= fadeEnd);
186
      final double opacity = 1.0 - Interval(fadeStart, fadeEnd).transform(t);
187
      if (opacity > 0.0) {
188
        children.add(Positioned(
189
          top: _getCollapsePadding(t, settings),
190 191
          left: 0.0,
          right: 0.0,
192
          height: settings.maxExtent,
193
          child: Opacity(
194
            opacity: opacity,
195 196
            child: widget.background,
          ),
197 198
        ));
      }
199 200
    }

201
    if (widget.title != null) {
202 203 204 205 206 207 208
      Widget title;
      switch (defaultTargetPlatform) {
        case TargetPlatform.iOS:
          title = widget.title;
          break;
        case TargetPlatform.fuchsia:
        case TargetPlatform.android:
209
          title = Semantics(
210 211 212 213 214
            namesRoute: true,
            child: widget.title,
          );
      }

215
      final ThemeData theme = Theme.of(context);
216
      final double opacity = settings.toolbarOpacity;
217
      if (opacity > 0.0) {
218
        TextStyle titleStyle = theme.primaryTextTheme.title;
219
        titleStyle = titleStyle.copyWith(
220
          color: titleStyle.color.withOpacity(opacity)
221
        );
222
        final bool effectiveCenterTitle = _getEffectiveCenterTitle(theme);
223 224 225
        final EdgeInsetsGeometry padding = widget.titlePadding ??
          EdgeInsetsDirectional.only(
            start: effectiveCenterTitle ? 0.0 : 72.0,
226
            bottom: 16.0,
227
          );
228
        final double scaleValue = Tween<double>(begin: 1.5, end: 1.0).transform(t);
229
        final Matrix4 scaleTransform = Matrix4.identity()
230
          ..scale(scaleValue, scaleValue, 1.0);
231
        final Alignment titleAlignment = _getTitleAlignment(effectiveCenterTitle);
232
        children.add(Container(
233
          padding: padding,
234
          child: Transform(
235
            alignment: titleAlignment,
236
            transform: scaleTransform,
237
            child: Align(
238
              alignment: titleAlignment,
239
              child: DefaultTextStyle(
240 241
                style: titleStyle,
                child: title,
242 243 244
              ),
            ),
          ),
245 246
        ));
      }
247 248
    }

249
    return ClipRect(child: Stack(children: children));
250
  }
251 252
}

253 254 255 256
/// Provides sizing and opacity information to a [FlexibleSpaceBar].
///
/// See also:
///
257
///  * [FlexibleSpaceBar] which creates a flexible space bar.
258 259 260 261 262 263
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.
  const FlexibleSpaceBarSettings({
264 265 266 267
    Key key,
    this.toolbarOpacity,
    this.minExtent,
    this.maxExtent,
268 269
    @required this.currentExtent,
    @required Widget child,
270 271
  }) : assert(currentExtent != null),
       super(key: key, child: child);
272

273
  /// Affects how transparent the text within the toolbar appears.
274
  final double toolbarOpacity;
275 276

  /// Minimum height of the resulting [FlexibleSpaceBar] when fully collapsed.
277
  final double minExtent;
278 279

  /// Maximum height of the resulting [FlexibleSpaceBar] when fully expanded.
280
  final double maxExtent;
281 282 283 284

  /// 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.
285
  final double currentExtent;
286 287

  @override
288
  bool updateShouldNotify(FlexibleSpaceBarSettings oldWidget) {
289 290 291 292
    return toolbarOpacity != oldWidget.toolbarOpacity
        || minExtent != oldWidget.minExtent
        || maxExtent != oldWidget.maxExtent
        || currentExtent != oldWidget.currentExtent;
293
  }
294
}