| // 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:collection'; |
| import 'dart:math' as math; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/scheduler.dart'; |
| |
| import 'basic.dart'; |
| import 'framework.dart'; |
| import 'ticker_provider.dart'; |
| |
| // Examples can assume: |
| // late BuildContext context; |
| |
| /// 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. In 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. |
| /// |
| /// By default, if there is an entirely [opaque] entry over this one, then this |
| /// one will not be included in the widget tree (in particular, stateful widgets |
| /// within the overlay entry will not be instantiated). To ensure that your |
| /// overlay entry is still built even if it is not visible, set [maintainState] |
| /// to true. This is more expensive, so should be done with care. In particular, |
| /// if widgets in an overlay entry with [maintainState] set to true repeatedly |
| /// call [State.setState], the user's battery will be drained unnecessarily. |
| /// |
| /// [OverlayEntry] is a [Listenable] that notifies when the widget built by |
| /// [builder] is mounted or unmounted, whose exact state can be queried by |
| /// [mounted]. After the owner of the [OverlayEntry] calls [remove] and then |
| /// [dispose], the widget may not be immediately removed from the widget tree. |
| /// As a result listeners of the [OverlayEntry] can get notified for one last |
| /// time after the [dispose] call, when the widget is eventually unmounted. |
| /// |
| /// See also: |
| /// |
| /// * [Overlay] |
| /// * [OverlayState] |
| /// * [WidgetsApp] |
| /// * [MaterialApp] |
| class OverlayEntry implements Listenable { |
| /// 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. |
| OverlayEntry({ |
| required this.builder, |
| bool opaque = false, |
| bool maintainState = false, |
| }) : assert(builder != null), |
| assert(opaque != null), |
| assert(maintainState != null), |
| _opaque = opaque, |
| _maintainState = maintainState; |
| |
| /// This entry will include the widget built by this builder in the overlay at |
| /// the entry's position. |
| /// |
| /// To cause this builder to be called again, call [markNeedsBuild] on this |
| /// overlay entry. |
| final WidgetBuilder builder; |
| |
| /// Whether this entry occludes the entire overlay. |
| /// |
| /// If an entry claims to be opaque, then, for efficiency, the overlay will |
| /// skip building entries below that entry unless they have [maintainState] |
| /// set. |
| bool get opaque => _opaque; |
| bool _opaque; |
| set opaque(bool value) { |
| assert(!_disposedByOwner); |
| if (_opaque == value) { |
| return; |
| } |
| _opaque = value; |
| _overlay?._didChangeEntryOpacity(); |
| } |
| |
| /// Whether this entry must be included in the tree even if there is a fully |
| /// [opaque] entry above it. |
| /// |
| /// By default, if there is an entirely [opaque] entry over this one, then this |
| /// one will not be included in the widget tree (in particular, stateful widgets |
| /// within the overlay entry will not be instantiated). To ensure that your |
| /// overlay entry is still built even if it is not visible, set [maintainState] |
| /// to true. This is more expensive, so should be done with care. In particular, |
| /// if widgets in an overlay entry with [maintainState] set to true repeatedly |
| /// call [State.setState], the user's battery will be drained unnecessarily. |
| /// |
| /// This is used by the [Navigator] and [Route] objects to ensure that routes |
| /// are kept around even when in the background, so that [Future]s promised |
| /// from subsequent routes will be handled properly when they complete. |
| bool get maintainState => _maintainState; |
| bool _maintainState; |
| set maintainState(bool value) { |
| assert(!_disposedByOwner); |
| assert(_maintainState != null); |
| if (_maintainState == value) { |
| return; |
| } |
| _maintainState = value; |
| assert(_overlay != null); |
| _overlay!._didChangeEntryOpacity(); |
| } |
| |
| /// Whether the [OverlayEntry] is currently mounted in the widget tree. |
| /// |
| /// The [OverlayEntry] notifies its listeners when this value changes. |
| bool get mounted => _overlayStateMounted.value; |
| |
| /// Whether the `_OverlayState`s built using this [OverlayEntry] is currently |
| /// mounted. |
| final ValueNotifier<bool> _overlayStateMounted = ValueNotifier<bool>(false); |
| |
| @override |
| void addListener(VoidCallback listener) { |
| assert(!_disposedByOwner); |
| _overlayStateMounted.addListener(listener); |
| } |
| |
| @override |
| void removeListener(VoidCallback listener) { |
| _overlayStateMounted.removeListener(listener); |
| } |
| |
| OverlayState? _overlay; |
| final GlobalKey<_OverlayEntryWidgetState> _key = GlobalKey<_OverlayEntryWidgetState>(); |
| |
| /// Remove this entry from the overlay. |
| /// |
| /// This should only be called once. |
| /// |
| /// This method removes this overlay entry from the overlay immediately. The |
| /// UI will be updated in the same frame if this method is called before the |
| /// overlay rebuild in this frame; otherwise, the UI will be updated in the |
| /// next frame. This means that it is safe to call during builds, but also |
| /// that if you do call this after the overlay rebuild, the UI will not update |
| /// until the next frame (i.e. many milliseconds later). |
| void remove() { |
| assert(_overlay != null); |
| assert(!_disposedByOwner); |
| final OverlayState overlay = _overlay!; |
| _overlay = null; |
| if (!overlay.mounted) { |
| return; |
| } |
| |
| overlay._entries.remove(this); |
| if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { |
| SchedulerBinding.instance.addPostFrameCallback((Duration duration) { |
| overlay._markDirty(); |
| }); |
| } else { |
| overlay._markDirty(); |
| } |
| } |
| |
| /// Cause this entry to rebuild during the next pipeline flush. |
| /// |
| /// You need to call this function if the output of [builder] has changed. |
| void markNeedsBuild() { |
| assert(!_disposedByOwner); |
| _key.currentState?._markNeedsBuild(); |
| } |
| |
| void _didUnmount() { |
| assert(!mounted); |
| if (_disposedByOwner) { |
| _overlayStateMounted.dispose(); |
| } |
| } |
| |
| bool _disposedByOwner = false; |
| |
| /// Discards any resources used by this [OverlayEntry]. |
| /// |
| /// This method must be called after [remove] if the [OverlayEntry] is |
| /// inserted into an [Overlay]. |
| /// |
| /// After this is called, the object is not in a usable state and should be |
| /// discarded (calls to [addListener] will throw after the object is disposed). |
| /// However, the listeners registered may not be immediately released until |
| /// the widget built using this [OverlayEntry] is unmounted from the widget |
| /// tree. |
| /// |
| /// This method should only be called by the object's owner. |
| void dispose() { |
| assert(!_disposedByOwner); |
| assert(_overlay == null, 'An OverlayEntry must first be removed from the Overlay before dispose is called.'); |
| _disposedByOwner = true; |
| if (!mounted) { |
| _overlayStateMounted.dispose(); |
| } |
| } |
| |
| @override |
| String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)'; |
| } |
| |
| class _OverlayEntryWidget extends StatefulWidget { |
| const _OverlayEntryWidget({ |
| required Key key, |
| required this.entry, |
| this.tickerEnabled = true, |
| }) : assert(key != null), |
| assert(entry != null), |
| assert(tickerEnabled != null), |
| super(key: key); |
| |
| final OverlayEntry entry; |
| final bool tickerEnabled; |
| |
| @override |
| _OverlayEntryWidgetState createState() => _OverlayEntryWidgetState(); |
| } |
| |
| class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { |
| @override |
| void initState() { |
| super.initState(); |
| widget.entry._overlayStateMounted.value = true; |
| } |
| |
| @override |
| void dispose() { |
| widget.entry._overlayStateMounted.value = false; |
| widget.entry._didUnmount(); |
| super.dispose(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return TickerMode( |
| enabled: widget.tickerEnabled, |
| child: widget.entry.builder(context), |
| ); |
| } |
| |
| void _markNeedsBuild() { |
| setState(() { /* the state that changed is in the builder */ }); |
| } |
| } |
| |
| /// A stack of entries that can be managed independently. |
| /// |
| /// 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], [CupertinoApp] or a |
| /// [MaterialApp]. The navigator uses its overlay to manage the visual |
| /// appearance of its routes. |
| /// |
| /// The [Overlay] widget uses a custom stack implementation, which is very |
| /// similar to the [Stack] widget. The main use case of [Overlay] is related to |
| /// navigation and being able to insert widgets on top of the pages in an app. |
| /// To simply display a stack of widgets, consider using [Stack] instead. |
| /// |
| /// An [Overlay] widget requires a [Directionality] widget to be in scope, so |
| /// that it can resolve direction-sensitive coordinates of any |
| /// [Positioned.directional] children. |
| /// |
| /// {@tool dartpad} |
| /// This example shows how to use the [Overlay] to highlight the [NavigationBar] |
| /// destination. |
| /// |
| /// ** See code in examples/api/lib/widgets/overlay/overlay.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [OverlayEntry], the class that is used for describing the overlay entries. |
| /// * [OverlayState], which is used to insert the entries into the overlay. |
| /// * [WidgetsApp], which inserts an [Overlay] widget indirectly via its [Navigator]. |
| /// * [MaterialApp], which inserts an [Overlay] widget indirectly via its [Navigator]. |
| /// * [CupertinoApp], which inserts an [Overlay] widget indirectly via its [Navigator]. |
| /// * [Stack], which allows directly displaying a stack of widgets. |
| class Overlay extends StatefulWidget { |
| /// 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 is |
| /// created by the [Navigator] in a [WidgetsApp], [CupertinoApp], or a |
| /// [MaterialApp] for the application. |
| const Overlay({ |
| super.key, |
| this.initialEntries = const <OverlayEntry>[], |
| this.clipBehavior = Clip.hardEdge, |
| }) : assert(initialEntries != null), |
| assert(clipBehavior != null); |
| |
| /// The entries to include in the overlay initially. |
| /// |
| /// These entries are only used when the [OverlayState] is initialized. If you |
| /// are providing a new [Overlay] description for an overlay that's already in |
| /// the tree, then the new entries are ignored. |
| /// |
| /// To add entries to an [Overlay] that is already in the tree, use |
| /// [Overlay.of] to obtain the [OverlayState] (or assign a [GlobalKey] to the |
| /// [Overlay] widget and obtain the [OverlayState] via |
| /// [GlobalKey.currentState]), and then use [OverlayState.insert] or |
| /// [OverlayState.insertAll]. |
| /// |
| /// To remove an entry from an [Overlay], use [OverlayEntry.remove]. |
| final List<OverlayEntry> initialEntries; |
| |
| /// {@macro flutter.material.Material.clipBehavior} |
| /// |
| /// Defaults to [Clip.hardEdge], and must not be null. |
| final Clip clipBehavior; |
| |
| /// The [OverlayState] from the closest instance of [Overlay] that encloses |
| /// the given context, and, in debug mode, will throw if one is not found. |
| /// |
| /// In debug mode, if the `debugRequiredFor` argument is provided and an |
| /// overlay isn't found, then this function will throw an exception containing |
| /// the runtime type of the given widget in the error message. The exception |
| /// attempts to explain that the calling [Widget] (the one given by the |
| /// `debugRequiredFor` argument) needs an [Overlay] to be present to function. |
| /// If `debugRequiredFor` is not supplied, then the error message is more |
| /// generic. |
| /// |
| /// Typical usage is as follows: |
| /// |
| /// ```dart |
| /// OverlayState overlay = Overlay.of(context); |
| /// ``` |
| /// |
| /// If `rootOverlay` is set to true, the state from the furthest instance of |
| /// this class is given instead. Useful for installing overlay entries above |
| /// all subsequent instances of [Overlay]. |
| /// |
| /// This method can be expensive (it walks the element tree). |
| /// |
| /// See also: |
| /// |
| /// * [Overlay.maybeOf] for a similar function that returns null if an |
| /// [Overlay] is not found. |
| static OverlayState of( |
| BuildContext context, { |
| bool rootOverlay = false, |
| Widget? debugRequiredFor, |
| }) { |
| final OverlayState? result = maybeOf(context, rootOverlay: rootOverlay); |
| assert(() { |
| if (result == null) { |
| final List<DiagnosticsNode> information = <DiagnosticsNode>[ |
| ErrorSummary('No Overlay widget found.'), |
| ErrorDescription('${debugRequiredFor?.runtimeType ?? 'Some'} widgets require an Overlay widget ancestor for correct operation.'), |
| ErrorHint('The most common way to add an Overlay to an application is to include a MaterialApp, CupertinoApp or Navigator widget in the runApp() call.'), |
| if (debugRequiredFor != null) DiagnosticsProperty<Widget>('The specific widget that failed to find an overlay was', debugRequiredFor, style: DiagnosticsTreeStyle.errorProperty), |
| if (context.widget != debugRequiredFor) |
| context.describeElement('The context from which that widget was searching for an overlay was'), |
| ]; |
| |
| throw FlutterError.fromParts(information); |
| } |
| return true; |
| }()); |
| return result!; |
| } |
| |
| /// The [OverlayState] from the closest instance of [Overlay] that encloses |
| /// the given context, if any. |
| /// |
| /// Typical usage is as follows: |
| /// |
| /// ```dart |
| /// OverlayState? overlay = Overlay.maybeOf(context); |
| /// ``` |
| /// |
| /// If `rootOverlay` is set to true, the state from the furthest instance of |
| /// this class is given instead. Useful for installing overlay entries above |
| /// all subsequent instances of [Overlay]. |
| /// |
| /// This method can be expensive (it walks the element tree). |
| /// |
| /// See also: |
| /// |
| /// * [Overlay.of] for a similar function that returns a non-nullable result |
| /// and throws if an [Overlay] is not found. |
| |
| static OverlayState? maybeOf( |
| BuildContext context, { |
| bool rootOverlay = false, |
| }) { |
| return rootOverlay |
| ? context.findRootAncestorStateOfType<OverlayState>() |
| : context.findAncestorStateOfType<OverlayState>(); |
| } |
| |
| @override |
| OverlayState createState() => OverlayState(); |
| } |
| |
| /// The current state of an [Overlay]. |
| /// |
| /// Used to insert [OverlayEntry]s into the overlay using the [insert] and |
| /// [insertAll] functions. |
| class OverlayState extends State<Overlay> with TickerProviderStateMixin { |
| final List<OverlayEntry> _entries = <OverlayEntry>[]; |
| |
| @override |
| void initState() { |
| super.initState(); |
| insertAll(widget.initialEntries); |
| } |
| |
| int _insertionIndex(OverlayEntry? below, OverlayEntry? above) { |
| assert(above == null || below == null); |
| if (below != null) { |
| return _entries.indexOf(below); |
| } |
| if (above != null) { |
| return _entries.indexOf(above) + 1; |
| } |
| return _entries.length; |
| } |
| |
| /// Insert the given entry into the overlay. |
| /// |
| /// If `below` is non-null, the entry is inserted just below `below`. |
| /// If `above` is non-null, the entry is inserted just above `above`. |
| /// Otherwise, the entry is inserted on top. |
| /// |
| /// It is an error to specify both `above` and `below`. |
| void insert(OverlayEntry entry, { OverlayEntry? below, OverlayEntry? above }) { |
| assert(_debugVerifyInsertPosition(above, below)); |
| assert(!_entries.contains(entry), 'The specified entry is already present in the Overlay.'); |
| assert(entry._overlay == null, 'The specified entry is already present in another Overlay.'); |
| entry._overlay = this; |
| setState(() { |
| _entries.insert(_insertionIndex(below, above), entry); |
| }); |
| } |
| |
| /// Insert all the entries in the given iterable. |
| /// |
| /// If `below` is non-null, the entries are inserted just below `below`. |
| /// If `above` is non-null, the entries are inserted just above `above`. |
| /// Otherwise, the entries are inserted on top. |
| /// |
| /// It is an error to specify both `above` and `below`. |
| void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry? below, OverlayEntry? above }) { |
| assert(_debugVerifyInsertPosition(above, below)); |
| assert( |
| entries.every((OverlayEntry entry) => !_entries.contains(entry)), |
| 'One or more of the specified entries are already present in the Overlay.', |
| ); |
| assert( |
| entries.every((OverlayEntry entry) => entry._overlay == null), |
| 'One or more of the specified entries are already present in another Overlay.', |
| ); |
| if (entries.isEmpty) { |
| return; |
| } |
| for (final OverlayEntry entry in entries) { |
| assert(entry._overlay == null); |
| entry._overlay = this; |
| } |
| setState(() { |
| _entries.insertAll(_insertionIndex(below, above), entries); |
| }); |
| } |
| |
| bool _debugVerifyInsertPosition(OverlayEntry? above, OverlayEntry? below, { Iterable<OverlayEntry>? newEntries }) { |
| assert( |
| above == null || below == null, |
| 'Only one of `above` and `below` may be specified.', |
| ); |
| assert( |
| above == null || (above._overlay == this && _entries.contains(above) && (newEntries?.contains(above) ?? true)), |
| 'The provided entry used for `above` must be present in the Overlay${newEntries != null ? ' and in the `newEntriesList`' : ''}.', |
| ); |
| assert( |
| below == null || (below._overlay == this && _entries.contains(below) && (newEntries?.contains(below) ?? true)), |
| 'The provided entry used for `below` must be present in the Overlay${newEntries != null ? ' and in the `newEntriesList`' : ''}.', |
| ); |
| return true; |
| } |
| |
| /// Remove all the entries listed in the given iterable, then reinsert them |
| /// into the overlay in the given order. |
| /// |
| /// Entries mention in `newEntries` but absent from the overlay are inserted |
| /// as if with [insertAll]. |
| /// |
| /// Entries not mentioned in `newEntries` but present in the overlay are |
| /// positioned as a group in the resulting list relative to the entries that |
| /// were moved, as specified by one of `below` or `above`, which, if |
| /// specified, must be one of the entries in `newEntries`: |
| /// |
| /// If `below` is non-null, the group is positioned just below `below`. |
| /// If `above` is non-null, the group is positioned just above `above`. |
| /// Otherwise, the group is left on top, with all the rearranged entries |
| /// below. |
| /// |
| /// It is an error to specify both `above` and `below`. |
| void rearrange(Iterable<OverlayEntry> newEntries, { OverlayEntry? below, OverlayEntry? above }) { |
| final List<OverlayEntry> newEntriesList = newEntries is List<OverlayEntry> ? newEntries : newEntries.toList(growable: false); |
| assert(_debugVerifyInsertPosition(above, below, newEntries: newEntriesList)); |
| assert( |
| newEntriesList.every((OverlayEntry entry) => entry._overlay == null || entry._overlay == this), |
| 'One or more of the specified entries are already present in another Overlay.', |
| ); |
| assert( |
| newEntriesList.every((OverlayEntry entry) => _entries.indexOf(entry) == _entries.lastIndexOf(entry)), |
| 'One or more of the specified entries are specified multiple times.', |
| ); |
| if (newEntriesList.isEmpty) { |
| return; |
| } |
| if (listEquals(_entries, newEntriesList)) { |
| return; |
| } |
| final LinkedHashSet<OverlayEntry> old = LinkedHashSet<OverlayEntry>.of(_entries); |
| for (final OverlayEntry entry in newEntriesList) { |
| entry._overlay ??= this; |
| } |
| setState(() { |
| _entries.clear(); |
| _entries.addAll(newEntriesList); |
| old.removeAll(newEntriesList); |
| _entries.insertAll(_insertionIndex(below, above), old); |
| }); |
| } |
| |
| void _markDirty() { |
| if (mounted) { |
| setState(() {}); |
| } |
| } |
| |
| /// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an |
| /// opaque entry). |
| /// |
| /// 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 debug mode, and always returns false in release mode. |
| bool debugIsVisible(OverlayEntry entry) { |
| bool result = false; |
| assert(_entries.contains(entry)); |
| assert(() { |
| for (int i = _entries.length - 1; i > 0; i -= 1) { |
| final OverlayEntry candidate = _entries[i]; |
| if (candidate == entry) { |
| result = true; |
| break; |
| } |
| if (candidate.opaque) { |
| break; |
| } |
| } |
| return true; |
| }()); |
| return result; |
| } |
| |
| void _didChangeEntryOpacity() { |
| setState(() { |
| // We use the opacity of the entry in our build function, which means we |
| // our state has changed. |
| }); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| // This list is filled backwards and then reversed below before |
| // it is added to the tree. |
| final List<Widget> children = <Widget>[]; |
| bool onstage = true; |
| int onstageCount = 0; |
| for (int i = _entries.length - 1; i >= 0; i -= 1) { |
| final OverlayEntry entry = _entries[i]; |
| if (onstage) { |
| onstageCount += 1; |
| children.add(_OverlayEntryWidget( |
| key: entry._key, |
| entry: entry, |
| )); |
| if (entry.opaque) { |
| onstage = false; |
| } |
| } else if (entry.maintainState) { |
| children.add(_OverlayEntryWidget( |
| key: entry._key, |
| entry: entry, |
| tickerEnabled: false, |
| )); |
| } |
| } |
| return _Theatre( |
| skipCount: children.length - onstageCount, |
| clipBehavior: widget.clipBehavior, |
| children: children.reversed.toList(growable: false), |
| ); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| // TODO(jacobr): use IterableProperty instead as that would |
| // provide a slightly more consistent string summary of the List. |
| properties.add(DiagnosticsProperty<List<OverlayEntry>>('entries', _entries)); |
| } |
| } |
| |
| /// Special version of a [Stack], that doesn't layout and render the first |
| /// [skipCount] children. |
| /// |
| /// The first [skipCount] children are considered "offstage". |
| class _Theatre extends MultiChildRenderObjectWidget { |
| _Theatre({ |
| this.skipCount = 0, |
| this.clipBehavior = Clip.hardEdge, |
| super.children, |
| }) : assert(skipCount != null), |
| assert(skipCount >= 0), |
| assert(children != null), |
| assert(children.length >= skipCount), |
| assert(clipBehavior != null); |
| |
| final int skipCount; |
| |
| final Clip clipBehavior; |
| |
| @override |
| _TheatreElement createElement() => _TheatreElement(this); |
| |
| @override |
| _RenderTheatre createRenderObject(BuildContext context) { |
| return _RenderTheatre( |
| skipCount: skipCount, |
| textDirection: Directionality.of(context), |
| clipBehavior: clipBehavior, |
| ); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderTheatre renderObject) { |
| renderObject |
| ..skipCount = skipCount |
| ..textDirection = Directionality.of(context) |
| ..clipBehavior = clipBehavior; |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(IntProperty('skipCount', skipCount)); |
| } |
| } |
| |
| class _TheatreElement extends MultiChildRenderObjectElement { |
| _TheatreElement(_Theatre super.widget); |
| |
| @override |
| _RenderTheatre get renderObject => super.renderObject as _RenderTheatre; |
| |
| @override |
| void debugVisitOnstageChildren(ElementVisitor visitor) { |
| final _Theatre theatre = widget as _Theatre; |
| assert(children.length >= theatre.skipCount); |
| children.skip(theatre.skipCount).forEach(visitor); |
| } |
| } |
| |
| class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData> { |
| _RenderTheatre({ |
| List<RenderBox>? children, |
| required TextDirection textDirection, |
| int skipCount = 0, |
| Clip clipBehavior = Clip.hardEdge, |
| }) : assert(skipCount != null), |
| assert(skipCount >= 0), |
| assert(textDirection != null), |
| assert(clipBehavior != null), |
| _textDirection = textDirection, |
| _skipCount = skipCount, |
| _clipBehavior = clipBehavior { |
| addAll(children); |
| } |
| |
| bool _hasVisualOverflow = false; |
| |
| @override |
| void setupParentData(RenderBox child) { |
| if (child.parentData is! StackParentData) { |
| child.parentData = StackParentData(); |
| } |
| } |
| |
| Alignment? _resolvedAlignment; |
| |
| void _resolve() { |
| if (_resolvedAlignment != null) { |
| return; |
| } |
| _resolvedAlignment = AlignmentDirectional.topStart.resolve(textDirection); |
| } |
| |
| void _markNeedResolution() { |
| _resolvedAlignment = null; |
| markNeedsLayout(); |
| } |
| |
| TextDirection get textDirection => _textDirection; |
| TextDirection _textDirection; |
| set textDirection(TextDirection value) { |
| if (_textDirection == value) { |
| return; |
| } |
| _textDirection = value; |
| _markNeedResolution(); |
| } |
| |
| int get skipCount => _skipCount; |
| int _skipCount; |
| set skipCount(int value) { |
| assert(value != null); |
| if (_skipCount != value) { |
| _skipCount = value; |
| markNeedsLayout(); |
| } |
| } |
| |
| /// {@macro flutter.material.Material.clipBehavior} |
| /// |
| /// Defaults to [Clip.hardEdge], and must not be null. |
| Clip get clipBehavior => _clipBehavior; |
| Clip _clipBehavior = Clip.hardEdge; |
| set clipBehavior(Clip value) { |
| assert(value != null); |
| if (value != _clipBehavior) { |
| _clipBehavior = value; |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| } |
| |
| RenderBox? get _firstOnstageChild { |
| if (skipCount == super.childCount) { |
| return null; |
| } |
| RenderBox? child = super.firstChild; |
| for (int toSkip = skipCount; toSkip > 0; toSkip--) { |
| final StackParentData childParentData = child!.parentData! as StackParentData; |
| child = childParentData.nextSibling; |
| assert(child != null); |
| } |
| return child; |
| } |
| |
| RenderBox? get _lastOnstageChild => skipCount == super.childCount ? null : lastChild; |
| |
| int get _onstageChildCount => childCount - skipCount; |
| |
| @override |
| double computeMinIntrinsicWidth(double height) { |
| return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicWidth(height)); |
| } |
| |
| @override |
| double computeMaxIntrinsicWidth(double height) { |
| return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicWidth(height)); |
| } |
| |
| @override |
| double computeMinIntrinsicHeight(double width) { |
| return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicHeight(width)); |
| } |
| |
| @override |
| double computeMaxIntrinsicHeight(double width) { |
| return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicHeight(width)); |
| } |
| |
| @override |
| double? computeDistanceToActualBaseline(TextBaseline baseline) { |
| assert(!debugNeedsLayout); |
| double? result; |
| RenderBox? child = _firstOnstageChild; |
| while (child != null) { |
| assert(!child.debugNeedsLayout); |
| final StackParentData childParentData = child.parentData! as StackParentData; |
| double? candidate = child.getDistanceToActualBaseline(baseline); |
| if (candidate != null) { |
| candidate += childParentData.offset.dy; |
| if (result != null) { |
| result = math.min(result, candidate); |
| } else { |
| result = candidate; |
| } |
| } |
| child = childParentData.nextSibling; |
| } |
| return result; |
| } |
| |
| @override |
| bool get sizedByParent => true; |
| |
| @override |
| Size computeDryLayout(BoxConstraints constraints) { |
| assert(constraints.biggest.isFinite); |
| return constraints.biggest; |
| } |
| |
| @override |
| void performLayout() { |
| _hasVisualOverflow = false; |
| |
| if (_onstageChildCount == 0) { |
| return; |
| } |
| |
| _resolve(); |
| assert(_resolvedAlignment != null); |
| |
| // Same BoxConstraints as used by RenderStack for StackFit.expand. |
| final BoxConstraints nonPositionedConstraints = BoxConstraints.tight(constraints.biggest); |
| |
| RenderBox? child = _firstOnstageChild; |
| while (child != null) { |
| final StackParentData childParentData = child.parentData! as StackParentData; |
| |
| if (!childParentData.isPositioned) { |
| child.layout(nonPositionedConstraints, parentUsesSize: true); |
| childParentData.offset = _resolvedAlignment!.alongOffset(size - child.size as Offset); |
| } else { |
| _hasVisualOverflow = RenderStack.layoutPositionedChild(child, childParentData, size, _resolvedAlignment!) || _hasVisualOverflow; |
| } |
| |
| assert(child.parentData == childParentData); |
| child = childParentData.nextSibling; |
| } |
| } |
| |
| @override |
| bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
| RenderBox? child = _lastOnstageChild; |
| for (int i = 0; i < _onstageChildCount; i++) { |
| assert(child != null); |
| final StackParentData childParentData = child!.parentData! as StackParentData; |
| final bool isHit = result.addWithPaintOffset( |
| offset: childParentData.offset, |
| position: position, |
| hitTest: (BoxHitTestResult result, Offset transformed) { |
| assert(transformed == position - childParentData.offset); |
| return child!.hitTest(result, position: transformed); |
| }, |
| ); |
| if (isHit) { |
| return true; |
| } |
| child = childParentData.previousSibling; |
| } |
| return false; |
| } |
| |
| @protected |
| void paintStack(PaintingContext context, Offset offset) { |
| RenderBox? child = _firstOnstageChild; |
| while (child != null) { |
| final StackParentData childParentData = child.parentData! as StackParentData; |
| context.paintChild(child, childParentData.offset + offset); |
| child = childParentData.nextSibling; |
| } |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| if (_hasVisualOverflow && clipBehavior != Clip.none) { |
| _clipRectLayer.layer = context.pushClipRect( |
| needsCompositing, |
| offset, |
| Offset.zero & size, |
| paintStack, |
| clipBehavior: clipBehavior, |
| oldLayer: _clipRectLayer.layer, |
| ); |
| } else { |
| _clipRectLayer.layer = null; |
| paintStack(context, offset); |
| } |
| } |
| |
| final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
| |
| @override |
| void dispose() { |
| _clipRectLayer.layer = null; |
| super.dispose(); |
| } |
| |
| @override |
| void visitChildrenForSemantics(RenderObjectVisitor visitor) { |
| RenderBox? child = _firstOnstageChild; |
| while (child != null) { |
| visitor(child); |
| final StackParentData childParentData = child.parentData! as StackParentData; |
| child = childParentData.nextSibling; |
| } |
| } |
| |
| @override |
| Rect? describeApproximatePaintClip(RenderObject child) { |
| switch (clipBehavior) { |
| case Clip.none: |
| return null; |
| case Clip.hardEdge: |
| case Clip.antiAlias: |
| case Clip.antiAliasWithSaveLayer: |
| return _hasVisualOverflow ? Offset.zero & size : null; |
| } |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(IntProperty('skipCount', skipCount)); |
| properties.add(EnumProperty<TextDirection>('textDirection', textDirection)); |
| } |
| |
| @override |
| List<DiagnosticsNode> debugDescribeChildren() { |
| final List<DiagnosticsNode> offstageChildren = <DiagnosticsNode>[]; |
| final List<DiagnosticsNode> onstageChildren = <DiagnosticsNode>[]; |
| |
| int count = 1; |
| bool onstage = false; |
| RenderBox? child = firstChild; |
| final RenderBox? firstOnstageChild = _firstOnstageChild; |
| while (child != null) { |
| if (child == firstOnstageChild) { |
| onstage = true; |
| count = 1; |
| } |
| |
| if (onstage) { |
| onstageChildren.add( |
| child.toDiagnosticsNode( |
| name: 'onstage $count', |
| ), |
| ); |
| } else { |
| offstageChildren.add( |
| child.toDiagnosticsNode( |
| name: 'offstage $count', |
| style: DiagnosticsTreeStyle.offstage, |
| ), |
| ); |
| } |
| |
| final StackParentData childParentData = child.parentData! as StackParentData; |
| child = childParentData.nextSibling; |
| count += 1; |
| } |
| |
| return <DiagnosticsNode>[ |
| ...onstageChildren, |
| if (offstageChildren.isNotEmpty) |
| ...offstageChildren |
| else |
| DiagnosticsNode.message( |
| 'no offstage children', |
| style: DiagnosticsTreeStyle.offstage, |
| ), |
| ]; |
| } |
| } |