flexible_space_bar.dart 6.31 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
/// The part of a material design [AppBar] that expands and collapses.
///
15 16 17 18
/// 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.
19
///
20 21 22
/// The widget that sizes the [AppBar] must wrap it in the widget returned by
/// [FlexibleSpaceBar.createSettings], to convey sizing information down to the
/// [FlexibleSpaceBar].
23 24
///
/// See also:
Ian Hickson's avatar
Ian Hickson committed
25
///
26 27
///  * [SliverAppBar], which implements the expanding and contracting.
///  * [AppBar], which is used by [SliverAppBar].
28
///  * <https://material.google.com/patterns/scrolling-techniques.html>
29
class FlexibleSpaceBar extends StatefulWidget {
30 31
  /// Creates a flexible space bar.
  ///
32
  /// Most commonly used in the [AppBar.flexibleSpace] field.
33
  const FlexibleSpaceBar({
34 35 36 37 38
    Key key,
    this.title,
    this.background,
    this.centerTitle
  }) : super(key: key);
39

40 41 42
  /// The primary contents of the flexible space bar when expanded.
  ///
  /// Typically a [Text] widget.
43
  final Widget title;
44 45 46

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

50 51 52 53 54
  /// Whether the title should be centered.
  ///
  /// Defaults to being adapted to the current [TargetPlatform].
  final bool centerTitle;

55 56 57 58
  /// Wraps a widget that contains an [AppBar] to convey sizing information down
  /// to the [FlexibleSpaceBar].
  ///
  /// Used by [Scaffold] and [SliverAppBar].
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
  static Widget createSettings({
    double toolbarOpacity,
    double minExtent,
    double maxExtent,
    @required double currentExtent,
    @required Widget child,
  }) {
    assert(currentExtent != null);
    return new _FlexibleSpaceBarSettings(
      toolbarOpacity: toolbarOpacity ?? 1.0,
      minExtent: minExtent ?? currentExtent,
      maxExtent: maxExtent ?? currentExtent,
      currentExtent: currentExtent,
      child: child,
    );
  }

76
  @override
77 78 79 80
  _FlexibleSpaceBarState createState() => new _FlexibleSpaceBarState();
}

class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
81
  bool _getEffectiveCenterTitle(ThemeData theme) {
82 83
    if (widget.centerTitle != null)
      return widget.centerTitle;
84 85 86
    assert(theme.platform != null);
    switch (theme.platform) {
      case TargetPlatform.android:
87
      case TargetPlatform.fuchsia:
88 89 90 91
        return false;
      case TargetPlatform.iOS:
        return true;
    }
92
    return null;
93 94
  }

95
  Alignment _getTitleAlignment(bool effectiveCenterTitle) {
96
    if (effectiveCenterTitle)
97
      return Alignment.bottomCenter;
98 99 100 101
    final TextDirection textDirection = Directionality.of(context);
    assert(textDirection != null);
    switch (textDirection) {
      case TextDirection.rtl:
102
        return Alignment.bottomRight;
103
      case TextDirection.ltr:
104
        return Alignment.bottomLeft;
105 106 107 108
    }
    return null;
  }

109 110
  @override
  Widget build(BuildContext context) {
111
    final _FlexibleSpaceBarSettings settings = context.inheritFromWidgetOfExactType(_FlexibleSpaceBarSettings);
112 113 114
    assert(settings != null, 'A FlexibleSpaceBar must be wrapped in the widget returned by FlexibleSpaceBar.createSettings().');

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

116
    final double deltaExtent = settings.maxExtent - settings.minExtent;
117 118 119

    // 0.0 -> Expanded
    // 1.0 -> Collapsed to toolbar
120
    final double t = (1.0 - (settings.currentExtent - settings.minExtent) / (deltaExtent)).clamp(0.0, 1.0);
121 122

    // background image
123
    if (widget.background != null) {
124
      final double fadeStart = math.max(0.0, 1.0 - kToolbarHeight / deltaExtent);
125
      const double fadeEnd = 1.0;
126
      assert(fadeStart <= fadeEnd);
127
      final double opacity = 1.0 - new Interval(fadeStart, fadeEnd).transform(t);
128
      final double parallax = new Tween<double>(begin: 0.0, end: deltaExtent / 4.0).lerp(t);
129 130 131 132 133
      if (opacity > 0.0) {
        children.add(new Positioned(
          top: -parallax,
          left: 0.0,
          right: 0.0,
134
          height: settings.maxExtent,
135 136
          child: new Opacity(
            opacity: opacity,
137
            child: widget.background
138
          )
139 140
        ));
      }
141 142
    }

143
    if (widget.title != null) {
144
      final ThemeData theme = Theme.of(context);
145
      final double opacity = settings.toolbarOpacity;
146
      if (opacity > 0.0) {
147
        TextStyle titleStyle = theme.primaryTextTheme.title;
148
        titleStyle = titleStyle.copyWith(
149
          color: titleStyle.color.withOpacity(opacity)
150
        );
151
        final bool effectiveCenterTitle = _getEffectiveCenterTitle(theme);
152 153 154
        final double scaleValue = new Tween<double>(begin: 1.5, end: 1.0).lerp(t);
        final Matrix4 scaleTransform = new Matrix4.identity()
          ..scale(scaleValue, scaleValue, 1.0);
155
        final Alignment titleAlignment = _getTitleAlignment(effectiveCenterTitle);
156
        children.add(new Container(
157 158
          padding: new EdgeInsetsDirectional.only(
            start: effectiveCenterTitle ? 0.0 : 72.0,
159 160 161
            bottom: 16.0
          ),
          child: new Transform(
162
            alignment: titleAlignment,
163
            transform: scaleTransform,
164
            child: new Align(
165
              alignment: titleAlignment,
166
              child: new DefaultTextStyle(style: titleStyle, child: widget.title)
167 168
            )
          )
169 170
        ));
      }
171 172 173 174
    }

    return new ClipRect(child: new Stack(children: children));
  }
175 176 177
}

class _FlexibleSpaceBarSettings extends InheritedWidget {
178
  const _FlexibleSpaceBarSettings({
179 180 181 182 183 184 185 186 187 188 189 190
    Key key,
    this.toolbarOpacity,
    this.minExtent,
    this.maxExtent,
    this.currentExtent,
    Widget child,
  }) : super(key: key, child: child);

  final double toolbarOpacity;
  final double minExtent;
  final double maxExtent;
  final double currentExtent;
191 192

  @override
193 194 195 196 197
  bool updateShouldNotify(_FlexibleSpaceBarSettings oldWidget) {
    return toolbarOpacity != oldWidget.toolbarOpacity
        || minExtent != oldWidget.minExtent
        || maxExtent != oldWidget.maxExtent
        || currentExtent != oldWidget.currentExtent;
198
  }
199
}