overlay.dart 8.89 KB
Newer Older
1 2 3 4 5 6 7
// 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.

import 'basic.dart';
import 'framework.dart';

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
/// A place in an [Overlay] that can contain a widget.
///
/// Overlay entries are inserted into an [Overlay] using the
/// [OverlayState.insert] or [OverlayState.insertAll] functions. To find the
/// closest enclosing overlay for a given [BuildContext], use the [Overlay.of]
/// function.
///
/// An overlay entry can be in at most one overlay at a time. To remove an entry
/// from its overlay, call the [remove] function on the overlay entry.
///
/// Because an [Overlay] uses a [Stack] layout, overlay entries can use
/// [Positioned] and [AnimatedPositioned] to position themselves within the
/// overlay.
///
/// For example, [Draggable] uses an [OverlayEntry] to show the drag avatar that
/// follows the user's finger across the screen after the drag begins. Using the
/// overlay to display the drag avatar lets the avatar float over the other
/// widgets in the app. As the user's finger moves, draggable calls
/// [markNeedsBuild] on the overlay entry to cause it to rebuild. It its build,
/// the entry includes a [Positioned] with its top and left property set to
/// position the drag avatar near the user's finger. When the drag is over,
/// [Draggable] removes the entry from the overlay to remove the drag avatar
/// from view.
///
/// See also:
///
///  * [Overlay]
///  * [OverlayState]
///  * [WidgetsApp]
///  * [MaterialApp]
38
class OverlayEntry {
39 40 41 42 43
  /// Creates an overlay entry.
  ///
  /// To insert the entry into an [Overlay], first find the overlay using
  /// [Overlay.of] and then call [OverlayState.insert]. To remove the entry,
  /// call [remove] on the overlay entry itself.
44
  OverlayEntry({
45
    this.builder,
46
    bool opaque: false
47 48 49
  }) : _opaque = opaque {
    assert(builder != null);
  }
50

51
  /// This entry will include the widget built by this builder in the overlay at the entry's position.
52
  ///
53
  /// To cause this builder to be called again, call [markNeedsBuild] on this
54
  /// overlay entry.
55
  final WidgetBuilder builder;
56

57 58 59 60
  /// Whether this entry occludes the entire overlay.
  ///
  /// If an entry claims to be opaque, the overlay will skip building all the
  /// entries below that entry for efficiency.
61 62
  bool get opaque => _opaque;
  bool _opaque;
63
  set opaque (bool value) {
Adam Barth's avatar
Adam Barth committed
64
    if (_opaque == value)
65
      return;
66
    assert(_overlay != null);
67 68 69
    _overlay.setState(() {
      _opaque = value;
    });
70 71
  }

72 73
  OverlayState _overlay;
  final GlobalKey _key = new GlobalKey();
74

75
  /// Remove this entry from the overlay.
76
  void remove() {
77 78
    _overlay?._remove(this);
    _overlay = null;
79
  }
80

81 82 83
  /// Cause this entry to rebuild during the next pipeline flush.
  ///
  /// You need to call this function if the output of [builder] has changed.
84
  void markNeedsBuild() {
85
    _key.currentState?.setState(() { /* the state that changed is in the builder */ });
86
  }
87

88
  @override
89
  String toString() => '$runtimeType@$hashCode(opaque: $opaque)';
90 91
}

92
class _OverlayEntry extends StatefulWidget {
93
  _OverlayEntry(OverlayEntry entry) : entry = entry, super(key: entry._key);
94

95
  final OverlayEntry entry;
96 97

  @override
98 99
  _OverlayEntryState createState() => new _OverlayEntryState();
}
100

101
class _OverlayEntryState extends State<_OverlayEntry> {
102
  @override
103 104 105
  Widget build(BuildContext context) => config.entry.builder(context);
}

106
/// A [Stack] of entries that can be managed independently.
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
///
/// Overlays let independent child widgets "float" visual elements on top of
/// other widgets by inserting them into the overlay's [Stack]. The overlay lets
/// each of these widgets manage their participation in the overlay using
/// [OverlayEntry] objects.
///
/// Although you can create an [Overlay] directly, it's most common to use the
/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
/// navigator uses its overlay to manage the visual appearance of its routes.
///
/// See also:
///
///  * [OverlayEntry]
///  * [OverlayState]
///  * [WidgetsApp]
///  * [MaterialApp]
123
class Overlay extends StatefulWidget {
124 125 126 127 128 129 130 131
  /// Creates an overlay.
  ///
  /// The initial entries will be inserted into the overlay when its associated
  /// [OverlayState] is initialized.
  ///
  /// Rather than creating an overlay, consider using the overlay that has
  /// already been created by the [WidgetsApp] or the [MaterialApp] for this
  /// application.
132 133
  Overlay({
    Key key,
Adam Barth's avatar
Adam Barth committed
134 135 136 137
    this.initialEntries: const <OverlayEntry>[]
  }) : super(key: key) {
    assert(initialEntries != null);
  }
138

139
  /// The entries to include in the overlay initially.
140 141
  final List<OverlayEntry> initialEntries;

142
  /// The state from the closest instance of this class that encloses the given context.
143 144 145 146 147 148 149 150 151 152 153 154 155
  ///
  /// In checked mode, if the [debugRequiredFor] argument is provided then this
  /// function will assert that an overlay was found and will throw an exception
  /// if not. The exception attempts to explain that the calling [Widget] (the
  /// one given by the [debugRequiredFor] argument) needs an [Overlay] to be
  /// present to function.
  static OverlayState of(BuildContext context, { Widget debugRequiredFor }) {
    OverlayState result = context.ancestorStateOfType(const TypeMatcher<OverlayState>());
    assert(() {
      if (debugRequiredFor != null && result == null) {
        String additional = context.widget != debugRequiredFor
          ? '\nThe context from which that widget was searching for an overlay was:\n  $context'
          : '';
156
        throw new FlutterError(
157 158 159 160 161 162 163 164 165 166 167 168
          'No Overlay widget found.\n'
          '${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.\n'
          'The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.\n'
          'The specific widget that failed to find an overlay was:\n'
          '  $debugRequiredFor'
          '$additional'
        );
      }
      return true;
    });
    return result;
  }
Hixie's avatar
Hixie committed
169

170
  @override
171 172 173
  OverlayState createState() => new OverlayState();
}

174
/// The current state of an [Overlay].
175 176 177
///
/// Used to insert [OverlayEntry]s into the overlay using the [insert] and
/// [insertAll] functions.
178 179 180
class OverlayState extends State<Overlay> {
  final List<OverlayEntry> _entries = new List<OverlayEntry>();

181
  @override
182 183
  void initState() {
    super.initState();
184
    insertAll(config.initialEntries);
185 186
  }

187 188 189 190
  /// Insert the given entry into the overlay.
  ///
  /// If [above] is non-null, the entry is inserted just above [above].
  /// Otherwise, the entry is inserted on top.
191
  void insert(OverlayEntry entry, { OverlayEntry above }) {
192 193 194
    assert(entry._overlay == null);
    assert(above == null || (above._overlay == this && _entries.contains(above)));
    entry._overlay = this;
195 196 197 198 199 200
    setState(() {
      int index = above == null ? _entries.length : _entries.indexOf(above) + 1;
      _entries.insert(index, entry);
    });
  }

201 202 203 204
  /// Insert all the entries in the given iterable.
  ///
  /// If [above] is non-null, the entries are inserted just above [above].
  /// Otherwise, the entries are inserted on top.
205
  void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry above }) {
206
    assert(above == null || (above._overlay == this && _entries.contains(above)));
Adam Barth's avatar
Adam Barth committed
207 208
    if (entries.isEmpty)
      return;
209
    for (OverlayEntry entry in entries) {
210 211
      assert(entry._overlay == null);
      entry._overlay = this;
212 213 214 215 216 217 218
    }
    setState(() {
      int index = above == null ? _entries.length : _entries.indexOf(above) + 1;
      _entries.insertAll(index, entries);
    });
  }

219
  void _remove(OverlayEntry entry) {
220 221 222
    _entries.remove(entry);
    if (mounted)
      setState(() { /* entry was removed */ });
223 224
  }

225 226
  /// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an opaque entry).
  ///
227 228 229
  /// This is an O(N) algorithm, and should not be necessary except for debug
  /// asserts. To avoid people depending on it, this function is implemented
  /// only in checked mode.
230 231 232 233 234 235 236 237 238 239
  bool debugIsVisible(OverlayEntry entry) {
    bool result = false;
    assert(_entries.contains(entry));
    assert(() {
      for (int i = _entries.length - 1; i > 0; i -= 1) {
        OverlayEntry candidate = _entries[i];
        if (candidate == entry) {
          result = true;
          break;
        }
Hixie's avatar
Hixie committed
240
        if (candidate.opaque)
241 242 243 244 245 246 247
          break;
      }
      return true;
    });
    return result;
  }

248
  @override
249 250 251 252 253
  Widget build(BuildContext context) {
    List<Widget> backwardsChildren = <Widget>[];

    for (int i = _entries.length - 1; i >= 0; --i) {
      OverlayEntry entry = _entries[i];
254
      backwardsChildren.add(new _OverlayEntry(entry));
255 256 257 258
      if (entry.opaque)
        break;
    }

259
    return new Stack(children: backwardsChildren.reversed.toList(growable: false));
260
  }
261

262
  @override
263 264 265 266
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('entries: $_entries');
  }
267
}