mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
OverlayPortal.overlayChildLayoutBuilder (#164034)
This API allows the widget tree of an `OverlayPortal.overlayChild` to depend on various layout information (e.g. the incoming constraints, or the size of a RenderObject) from another child subtree of `Overlay` that does layout before the `overlayChild`. Most RenderObject subclasses can only access its child's or children's layout info, but not the layout info of its other descendants because of the "relayout boundary" optimization. Such locality makes the layout dependencies easier to reason about but it also makes it difficult to implement certain common UI patterns (see the examples in the description of the [previous PR](https://github.com/flutter/flutter/pull/163575)) The API is currently only available on `OverlayPortal` as it is the only Overlay API (AFAIK) that guarantees every render object in a "path" within the render tree has finished doing layout. TODO: polish the API docs and code comments TODO: more tests? TODO: markNeedsLayout should not imply markNeedsPaint in this case (or in layout builders in general). part1: https://github.com/flutter/flutter/pull/163575 diff this ... part1 https://github.com/LongCatIsLooong/flutter/compare/OverlayPortal-always-add-to-dirty-list...LongCatIsLooong:flutter:OverlayPortal-paint-transform-builder ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
d261411b4c
commit
7ce41abbd2
@ -2877,6 +2877,13 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
|
||||
/// parentUsesSize ensures that this render object will undergo layout if the
|
||||
/// child undergoes layout. Otherwise, the child can change its layout
|
||||
/// information without informing this render object.
|
||||
///
|
||||
/// Some special [RenderObject] subclasses (such as the one used by
|
||||
/// [OverlayPortal.overlayChildLayoutBuilder]) call [applyPaintTransform] in
|
||||
/// their [performLayout] implementation. To ensure such [RenderObject]s get
|
||||
/// the up-to-date paint transform, [RenderObject] subclasses should typically
|
||||
/// update the paint transform (as reported by [applyPaintTransform]) in this
|
||||
/// method instead of [paint].
|
||||
@protected
|
||||
void performLayout();
|
||||
|
||||
@ -3494,8 +3501,9 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
|
||||
/// Applies the transform that would be applied when painting the given child
|
||||
/// to the given matrix.
|
||||
///
|
||||
/// Used by coordinate conversion functions to translate coordinates local to
|
||||
/// one render object into coordinates local to another render object.
|
||||
/// Used by coordinate conversion functions ([getTransformTo], for example) to
|
||||
/// translate coordinates local to one render object into coordinates local to
|
||||
/// another render object.
|
||||
///
|
||||
/// Some RenderObjects will provide a zeroed out matrix in this method,
|
||||
/// indicating that the child should not paint anything or respond to hit
|
||||
|
||||
@ -577,13 +577,15 @@ mixin SchedulerBinding on BindingBase {
|
||||
|
||||
/// Schedules the given transient frame callback.
|
||||
///
|
||||
/// Adds the given callback to the list of frame callbacks and ensures that a
|
||||
/// frame is scheduled.
|
||||
/// Adds the given callback to the list of frame callbacks, and ensures that a
|
||||
/// frame is scheduled if the `scheduleNewFrame` argument is true.
|
||||
///
|
||||
/// The `scheduleNewFrame` argument dictates whether [scheduleFrame] should be
|
||||
/// called to ensure a new frame. Defaults to true.
|
||||
///
|
||||
/// If this is called during the frame's animation phase (when transient frame
|
||||
/// callbacks are still being invoked), a new frame will be scheduled, and
|
||||
/// `callback` will be called in the newly scheduled frame, not in the current
|
||||
/// frame.
|
||||
/// callbacks are still being invoked), `callback` will be called in the next
|
||||
/// frame, not in the current frame.
|
||||
///
|
||||
/// If this is a one-off registration, ignore the `rescheduling` argument.
|
||||
///
|
||||
@ -605,8 +607,14 @@ mixin SchedulerBinding on BindingBase {
|
||||
/// * [WidgetsBinding.drawFrame], which explains the phases of each frame
|
||||
/// for those apps that use Flutter widgets (and where transient frame
|
||||
/// callbacks fit into those phases).
|
||||
int scheduleFrameCallback(FrameCallback callback, {bool rescheduling = false}) {
|
||||
scheduleFrame();
|
||||
int scheduleFrameCallback(
|
||||
FrameCallback callback, {
|
||||
bool rescheduling = false,
|
||||
bool scheduleNewFrame = true,
|
||||
}) {
|
||||
if (scheduleNewFrame) {
|
||||
scheduleFrame();
|
||||
}
|
||||
_nextFrameCallbackId += 1;
|
||||
_transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(
|
||||
callback,
|
||||
|
||||
@ -19,37 +19,35 @@ typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstrain
|
||||
|
||||
/// An abstract superclass for widgets that defer their building until layout.
|
||||
///
|
||||
/// Similar to the [Builder] widget except that the framework calls the [builder]
|
||||
/// function at layout time and provides the constraints that this widget should
|
||||
/// adhere to. This is useful when the parent constrains the child's size and layout,
|
||||
/// and doesn't depend on the child's intrinsic size.
|
||||
/// Similar to the [Builder] widget except that the implementation calls the [builder]
|
||||
/// function at layout time and provides the [LayoutInfoType] that is required to
|
||||
/// configure the child widget subtree.
|
||||
///
|
||||
/// {@template flutter.widgets.ConstrainedLayoutBuilder}
|
||||
/// The [builder] function is called in the following situations:
|
||||
/// This is useful when the child widget tree relies on information that are only
|
||||
/// available during layout, and doesn't depend on the child's intrinsic size.
|
||||
///
|
||||
/// * The first time the widget is laid out.
|
||||
/// * When the parent widget passes different layout constraints.
|
||||
/// * When the parent widget updates this widget.
|
||||
/// * When the dependencies that the [builder] function subscribes to change.
|
||||
/// The [LayoutInfoType] should typically be immutable. The equality of the
|
||||
/// [LayoutInfoType] type is used by the implementation to avoid unnecessary
|
||||
/// rebuilds: if the new [LayoutInfoType] computed during layout is the same as
|
||||
/// (defined by `LayoutInfoType.==`) the previous [LayoutInfoType], the
|
||||
/// implementation will try to avoid calling the [builder] again unless
|
||||
/// [updateShouldRebuild] returns true. The corresponding [RenderObject] produced
|
||||
/// by this widget retains the most up-to-date [LayoutInfoType] for this purpose,
|
||||
/// which may keep a [LayoutInfoType] object in memory until the widget is removed
|
||||
/// from the tree.
|
||||
///
|
||||
/// The [builder] function is _not_ called during layout if the parent passes
|
||||
/// the same constraints repeatedly.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// Subclasses must return a [RenderObject] that mixes in
|
||||
/// [RenderConstrainedLayoutBuilder].
|
||||
abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints>
|
||||
extends RenderObjectWidget {
|
||||
/// Subclasses must return a [RenderObject] that mixes in [RenderAbstractLayoutBuilderMixin].
|
||||
abstract class AbstractLayoutBuilder<LayoutInfoType> extends RenderObjectWidget {
|
||||
/// Creates a widget that defers its building until layout.
|
||||
const ConstrainedLayoutBuilder({super.key, required this.builder});
|
||||
|
||||
@override
|
||||
RenderObjectElement createElement() => _LayoutBuilderElement<ConstraintType>(this);
|
||||
const AbstractLayoutBuilder({super.key});
|
||||
|
||||
/// Called at layout time to construct the widget tree.
|
||||
///
|
||||
/// The builder must not return null.
|
||||
final Widget Function(BuildContext context, ConstraintType constraints) builder;
|
||||
Widget Function(BuildContext context, LayoutInfoType layoutInfo) get builder;
|
||||
|
||||
@override
|
||||
RenderObjectElement createElement() => _LayoutBuilderElement<LayoutInfoType>(this);
|
||||
|
||||
/// Whether [builder] needs to be called again even if the layout constraints
|
||||
/// are the same.
|
||||
@ -71,17 +69,45 @@ abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints>
|
||||
/// * [Element.update], the method that actually updates the widget's
|
||||
/// configuration.
|
||||
@protected
|
||||
bool updateShouldRebuild(covariant ConstrainedLayoutBuilder<ConstraintType> oldWidget) => true;
|
||||
bool updateShouldRebuild(covariant AbstractLayoutBuilder<LayoutInfoType> oldWidget) => true;
|
||||
|
||||
@override
|
||||
RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject> createRenderObject(
|
||||
BuildContext context,
|
||||
);
|
||||
|
||||
// updateRenderObject is redundant with the logic in the LayoutBuilderElement below.
|
||||
}
|
||||
|
||||
class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderObjectElement {
|
||||
_LayoutBuilderElement(ConstrainedLayoutBuilder<ConstraintType> super.widget);
|
||||
/// A specialized [AbstractLayoutBuilder] whose widget subtree depends on the
|
||||
/// incoming [ConstraintType] that will be imposed on the widget.
|
||||
///
|
||||
/// {@template flutter.widgets.ConstrainedLayoutBuilder}
|
||||
/// The [builder] function is called in the following situations:
|
||||
///
|
||||
/// * The first time the widget is laid out.
|
||||
/// * When the parent widget passes different layout constraints.
|
||||
/// * When the parent widget updates this widget and [updateShouldRebuild] returns `true`.
|
||||
/// * When the dependencies that the [builder] function subscribes to change.
|
||||
///
|
||||
/// The [builder] function is _not_ called during layout if the parent passes
|
||||
/// the same constraints repeatedly.
|
||||
/// {@endtemplate}
|
||||
abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints>
|
||||
extends AbstractLayoutBuilder<ConstraintType> {
|
||||
/// Creates a widget that defers its building until layout.
|
||||
const ConstrainedLayoutBuilder({super.key, required this.builder});
|
||||
|
||||
@override
|
||||
RenderConstrainedLayoutBuilder<ConstraintType, RenderObject> get renderObject =>
|
||||
super.renderObject as RenderConstrainedLayoutBuilder<ConstraintType, RenderObject>;
|
||||
final Widget Function(BuildContext context, ConstraintType constraints) builder;
|
||||
}
|
||||
|
||||
class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
|
||||
_LayoutBuilderElement(AbstractLayoutBuilder<LayoutInfoType> super.widget);
|
||||
|
||||
@override
|
||||
RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject> get renderObject =>
|
||||
super.renderObject as RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject>;
|
||||
|
||||
Element? _child;
|
||||
|
||||
@ -140,18 +166,18 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
|
||||
@override
|
||||
void mount(Element? parent, Object? newSlot) {
|
||||
super.mount(parent, newSlot); // Creates the renderObject.
|
||||
renderObject.updateCallback(_rebuildWithConstraints);
|
||||
renderObject._updateCallback(_rebuildWithConstraints);
|
||||
}
|
||||
|
||||
@override
|
||||
void update(ConstrainedLayoutBuilder<ConstraintType> newWidget) {
|
||||
void update(AbstractLayoutBuilder<LayoutInfoType> newWidget) {
|
||||
assert(widget != newWidget);
|
||||
final ConstrainedLayoutBuilder<ConstraintType> oldWidget =
|
||||
widget as ConstrainedLayoutBuilder<ConstraintType>;
|
||||
final AbstractLayoutBuilder<LayoutInfoType> oldWidget =
|
||||
widget as AbstractLayoutBuilder<LayoutInfoType>;
|
||||
super.update(newWidget);
|
||||
assert(widget == newWidget);
|
||||
|
||||
renderObject.updateCallback(_rebuildWithConstraints);
|
||||
renderObject._updateCallback(_rebuildWithConstraints);
|
||||
if (newWidget.updateShouldRebuild(oldWidget)) {
|
||||
_needsBuild = true;
|
||||
renderObject.markNeedsLayout();
|
||||
@ -183,22 +209,24 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
|
||||
|
||||
@override
|
||||
void unmount() {
|
||||
renderObject.updateCallback(null);
|
||||
renderObject._updateCallback(null);
|
||||
super.unmount();
|
||||
}
|
||||
|
||||
// The constraints that were passed to this class last time it was laid out.
|
||||
// These constraints are compared to the new constraints to determine whether
|
||||
// [ConstrainedLayoutBuilder.builder] needs to be called.
|
||||
ConstraintType? _previousConstraints;
|
||||
// The LayoutInfoType that was used to invoke the layout callback with last time,
|
||||
// during layout. The `_previousLayoutInfo` value is compared to the new one
|
||||
// to determine whether [LayoutBuilderBase.builder] needs to be called.
|
||||
LayoutInfoType? _previousLayoutInfo;
|
||||
bool _needsBuild = true;
|
||||
|
||||
void _rebuildWithConstraints(ConstraintType constraints) {
|
||||
void _rebuildWithConstraints(Constraints _) {
|
||||
final LayoutInfoType layoutInfo = renderObject.layoutInfo;
|
||||
@pragma('vm:notify-debugger-on-exception')
|
||||
void updateChildCallback() {
|
||||
Widget built;
|
||||
try {
|
||||
built = (widget as ConstrainedLayoutBuilder<ConstraintType>).builder(this, constraints);
|
||||
assert(layoutInfo == renderObject.layoutInfo);
|
||||
built = (widget as AbstractLayoutBuilder<LayoutInfoType>).builder(this, layoutInfo);
|
||||
debugWidgetBuilderValue(widget, built);
|
||||
} catch (e, stack) {
|
||||
built = ErrorWidget.builder(
|
||||
@ -231,12 +259,12 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
|
||||
_child = updateChild(null, built, slot);
|
||||
} finally {
|
||||
_needsBuild = false;
|
||||
_previousConstraints = constraints;
|
||||
_previousLayoutInfo = layoutInfo;
|
||||
}
|
||||
}
|
||||
|
||||
final VoidCallback? callback =
|
||||
_needsBuild || (constraints != _previousConstraints) ? updateChildCallback : null;
|
||||
_needsBuild || (layoutInfo != _previousLayoutInfo) ? updateChildCallback : null;
|
||||
owner!.buildScope(this, callback);
|
||||
}
|
||||
|
||||
@ -256,7 +284,7 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
|
||||
|
||||
@override
|
||||
void removeRenderObjectChild(RenderObject child, Object? slot) {
|
||||
final RenderConstrainedLayoutBuilder<ConstraintType, RenderObject> renderObject =
|
||||
final RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject> renderObject =
|
||||
this.renderObject;
|
||||
assert(renderObject.child == child);
|
||||
renderObject.child = null;
|
||||
@ -264,19 +292,21 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic mixin for [RenderObject]s created by [ConstrainedLayoutBuilder].
|
||||
/// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with
|
||||
/// the the same `LayoutInfoType`.
|
||||
///
|
||||
/// Provides a callback that should be called at layout time, typically in
|
||||
/// [RenderObject.performLayout].
|
||||
mixin RenderConstrainedLayoutBuilder<
|
||||
ConstraintType extends Constraints,
|
||||
ChildType extends RenderObject
|
||||
>
|
||||
/// Provides a [rebuildIfNecessary] method that should be called at layout time,
|
||||
/// typically in [RenderObject.performLayout]. The method invokes
|
||||
/// [AbstractLayoutBuilder]'s builder callback if needed.
|
||||
///
|
||||
/// Implementers must provide a [layoutInfo] implementation that is safe to
|
||||
/// access in [rebuildIfNecessary], which is typically called in [performLayout].
|
||||
mixin RenderAbstractLayoutBuilderMixin<LayoutInfoType, ChildType extends RenderObject>
|
||||
on RenderObjectWithChildMixin<ChildType> {
|
||||
LayoutCallback<ConstraintType>? _callback;
|
||||
LayoutCallback<Constraints>? _callback;
|
||||
|
||||
/// Change the layout callback.
|
||||
void updateCallback(LayoutCallback<ConstraintType>? value) {
|
||||
void _updateCallback(LayoutCallback<Constraints>? value) {
|
||||
if (value == _callback) {
|
||||
return;
|
||||
}
|
||||
@ -284,14 +314,28 @@ mixin RenderConstrainedLayoutBuilder<
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
/// Invoke the callback supplied via [updateCallback].
|
||||
/// Invoke the builder callback supplied via [AbstractLayoutBuilder] and
|
||||
/// rebuilds the [AbstractLayoutBuilder]'s widget tree, if needed.
|
||||
///
|
||||
/// Typically this results in [ConstrainedLayoutBuilder.builder] being called
|
||||
/// during layout.
|
||||
/// No work will be done if [layoutInfo] has not changed since the last time
|
||||
/// this method was called, and [AbstractLayoutBuilder.updateShouldRebuild]
|
||||
/// returned `false` when the widget was rebuilt.
|
||||
///
|
||||
/// This method should typically be called as soon as possible in the class's
|
||||
/// [performLayout] implementation, before any layout work is done.
|
||||
@protected
|
||||
void rebuildIfNecessary() {
|
||||
assert(_callback != null);
|
||||
invokeLayoutCallback(_callback!);
|
||||
}
|
||||
|
||||
/// The information to invoke the [AbstractLayoutBuilder.builder] callback with.
|
||||
///
|
||||
/// This is typically the information that are only made available in
|
||||
/// [performLayout], which is inaccessible for regular [Builder] widget,
|
||||
/// such as the incoming [Constraints].
|
||||
@protected
|
||||
LayoutInfoType get layoutInfo;
|
||||
}
|
||||
|
||||
/// Builds a widget tree that can depend on the parent widget's size.
|
||||
@ -329,13 +373,15 @@ class LayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
|
||||
const LayoutBuilder({super.key, required super.builder});
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) => _RenderLayoutBuilder();
|
||||
RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox> createRenderObject(
|
||||
BuildContext context,
|
||||
) => _RenderLayoutBuilder();
|
||||
}
|
||||
|
||||
class _RenderLayoutBuilder extends RenderBox
|
||||
with
|
||||
RenderObjectWithChildMixin<RenderBox>,
|
||||
RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox> {
|
||||
RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox> {
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
assert(_debugThrowIfNotCheckingIntrinsics());
|
||||
@ -428,6 +474,10 @@ class _RenderLayoutBuilder extends RenderBox
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
BoxConstraints get layoutInfo => constraints;
|
||||
}
|
||||
|
||||
FlutterErrorDetails _reportException(
|
||||
|
||||
@ -24,9 +24,31 @@ import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'framework.dart';
|
||||
import 'layout_builder.dart';
|
||||
import 'lookup_boundary.dart';
|
||||
import 'ticker_provider.dart';
|
||||
|
||||
/// The signature of the widget builder callback used in
|
||||
/// [OverlayPortal.overlayChildLayoutBuilder].
|
||||
typedef OverlayChildLayoutBuilder =
|
||||
Widget Function(BuildContext context, OverlayChildLayoutInfo info);
|
||||
|
||||
/// The additional layout information available to the
|
||||
/// [OverlayPortal.overlayChildLayoutBuilder] callback.
|
||||
extension type OverlayChildLayoutInfo._(
|
||||
(Size childSize, Matrix4 childPaintTransform, Size overlaySize) _info
|
||||
) {
|
||||
/// The size of [OverlayPortal.child] in its own coordinates.
|
||||
Size get childSize => _info.$1;
|
||||
|
||||
/// The paint transform of [OverlayPortal.child], in the target [Overlay]'s
|
||||
/// coordinates.
|
||||
Matrix4 get childPaintTransform => _info.$2;
|
||||
|
||||
/// The size of the target [Overlay] in its own coordinates.
|
||||
Size get overlaySize => _info.$3;
|
||||
}
|
||||
|
||||
// Examples can assume:
|
||||
// late BuildContext context;
|
||||
|
||||
@ -337,21 +359,25 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
|
||||
// mutated. The reason for that is it's allowed to add/remove/move deferred
|
||||
// children to a _RenderTheater during performLayout, but the affected
|
||||
// children don't have to be laid out in the same performLayout call.
|
||||
late final Iterable<RenderBox> _paintOrderIterable = _createChildIterable(reversed: false);
|
||||
late final Iterable<_RenderDeferredLayoutBox> _paintOrderIterable = _createChildIterable(
|
||||
reversed: false,
|
||||
);
|
||||
// An Iterable that traverse the children in the child model in
|
||||
// hit-test order (from closest to the user to the farthest to the user).
|
||||
late final Iterable<RenderBox> _hitTestOrderIterable = _createChildIterable(reversed: true);
|
||||
late final Iterable<_RenderDeferredLayoutBox> _hitTestOrderIterable = _createChildIterable(
|
||||
reversed: true,
|
||||
);
|
||||
|
||||
// The following uses sync* because hit-testing is lazy, and LinkedList as a
|
||||
// Iterable doesn't support concurrent modification.
|
||||
Iterable<RenderBox> _createChildIterable({required bool reversed}) sync* {
|
||||
Iterable<_RenderDeferredLayoutBox> _createChildIterable({required bool reversed}) sync* {
|
||||
final LinkedList<_OverlayEntryLocation>? children = _sortedTheaterSiblings;
|
||||
if (children == null || children.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_OverlayEntryLocation? candidate = reversed ? children.last : children.first;
|
||||
while (candidate != null) {
|
||||
final RenderBox? renderBox = candidate._overlayChildRenderBox;
|
||||
final _RenderDeferredLayoutBox? renderBox = candidate._overlayChildRenderBox;
|
||||
candidate = reversed ? candidate.previous : candidate.next;
|
||||
if (renderBox != null) {
|
||||
yield renderBox;
|
||||
@ -982,8 +1008,10 @@ class _TheaterElement extends MultiChildRenderObjectElement {
|
||||
super.moveRenderObjectChild(child, oldSlot, newSlot);
|
||||
assert(() {
|
||||
final _TheaterParentData parentData = child.parentData! as _TheaterParentData;
|
||||
return parentData.overlayEntry ==
|
||||
final OverlayEntry entryAtNewSlot =
|
||||
((widget as _Theater).children[newSlot.index] as _OverlayEntryWidget).entry;
|
||||
assert(parentData.overlayEntry == entryAtNewSlot);
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
@ -1113,9 +1141,9 @@ class _TheaterParentData extends StackParentData {
|
||||
// _overlayStateMounted is set to null in _OverlayEntryWidgetState's dispose
|
||||
// method. This property is only accessed during layout, paint and hit-test so
|
||||
// the `value!` should be safe.
|
||||
Iterator<RenderBox>? get paintOrderIterator =>
|
||||
Iterator<_RenderDeferredLayoutBox>? get paintOrderIterator =>
|
||||
overlayEntry?._overlayEntryStateNotifier?.value!._paintOrderIterable.iterator;
|
||||
Iterator<RenderBox>? get hitTestOrderIterator =>
|
||||
Iterator<_RenderDeferredLayoutBox>? get hitTestOrderIterator =>
|
||||
overlayEntry?._overlayEntryStateNotifier?.value!._hitTestOrderIterable.iterator;
|
||||
|
||||
// A convenience method for traversing `paintOrderIterator` with a
|
||||
@ -1239,7 +1267,7 @@ class _RenderTheater extends RenderBox
|
||||
|
||||
// After adding `child` to the render tree, we want to make sure it will be
|
||||
// laid out in the same frame. This is done by calling markNeedsLayout on the
|
||||
// layout surrogate. This ensures `child` is reachable via tree walk (see
|
||||
// layout surrogate. This ensures `child` is added to the dirty list (see
|
||||
// _RenderLayoutSurrogateProxyBox.performLayout).
|
||||
child._layoutSurrogate.markNeedsLayout();
|
||||
}
|
||||
@ -1776,6 +1804,39 @@ class OverlayPortal extends StatefulWidget {
|
||||
this.child,
|
||||
}) : _targetRootOverlay = true;
|
||||
|
||||
/// Creates an [OverlayPortal] that renders the widget `overlayChildBuilder`
|
||||
/// builds on the closest [Overlay] when [OverlayPortalController.show] is
|
||||
/// called.
|
||||
///
|
||||
/// Developers can use `overlayChildBuilder` to configure the overlay child
|
||||
/// based on the the size and the location of [OverlayPortal.child] within the
|
||||
/// target [Overlay], as well as the size of the [Overlay] itself. This allows
|
||||
/// the overlay child to, for example, always follow [OverlayPortal.child] and
|
||||
/// at the same time resize itself base on how close it is to the edges of
|
||||
/// the [Overlay].
|
||||
///
|
||||
/// The `overlayChildBuilder` callback is called during layout. To ensure the
|
||||
/// paint transform of [OverlayPortal.child] in relation to the target
|
||||
/// [Overlay] is up-to-date by then, all [RenderObject]s between the
|
||||
/// [OverlayPortal] to the target [Overlay] must establish their paint
|
||||
/// transform during the layout phase, which most [RenderObject]s do. One
|
||||
/// exception is the [CompositedTransformFollower] widget, whose [RenderObject]
|
||||
/// only establishes the paint transform when composited. Putting a
|
||||
/// [CompositedTransformFollower] between the [OverlayPortal] and the [Overlay]
|
||||
/// may resulting in an incorrect child paint transform being provided to the
|
||||
/// `overlayChildBuilder` and will cause an assertion in debug mode.
|
||||
OverlayPortal.overlayChildLayoutBuilder({
|
||||
Key? key,
|
||||
required OverlayPortalController controller,
|
||||
required OverlayChildLayoutBuilder overlayChildBuilder,
|
||||
required Widget? child,
|
||||
}) : this(
|
||||
key: key,
|
||||
controller: controller,
|
||||
overlayChildBuilder: (_) => _OverlayChildLayoutBuilder(builder: overlayChildBuilder),
|
||||
child: child,
|
||||
);
|
||||
|
||||
/// The controller to show, hide and bring to top the overlay child.
|
||||
final OverlayPortalController controller;
|
||||
|
||||
@ -2313,14 +2374,16 @@ class _DeferredLayout extends SingleChildRenderObjectWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// A `RenderProxyBox` that defers its layout until its `_layoutSurrogate` (which
|
||||
// is not necessarily an ancestor of this RenderBox, but shares at least one
|
||||
// `_RenderTheater` ancestor with this RenderBox) is laid out.
|
||||
// This `RenderObject` must be a child of a `_RenderTheater`. It guarantees that
|
||||
// it only does layout after the sizes of the render objects from its
|
||||
// `_layoutSurrogate` (which must be a descendant of this `RenderObject`'s
|
||||
// parent) through the parent `_RenderTheater` are known. To this end:
|
||||
//
|
||||
// This `RenderObject` must be a child of a `_RenderTheater`. It guarantees that:
|
||||
//
|
||||
// 1. It's a relayout boundary, so calling `markNeedsLayout` on it never dirties
|
||||
// its `_RenderTheater`.
|
||||
// 1. It's a relayout boundary, and calling `markNeedsLayout` on it or adding it
|
||||
// to the `_RenderTheater` as a child never dirties its `_RenderTheater`.
|
||||
// Instead, it is always added to the `PipelineOwner`'s dirty list when it
|
||||
// needs layout (even for the initial layout when it is first added to the
|
||||
// tree).
|
||||
//
|
||||
// 2. Its `layout` implementation is overridden such that `performLayout` does
|
||||
// not do anything when its called from `layout`, preventing the parent
|
||||
@ -2329,9 +2392,8 @@ class _DeferredLayout extends SingleChildRenderObjectWidget {
|
||||
// called from within `layout` to schedule a layout update for this relayout
|
||||
// boundary when needed.
|
||||
//
|
||||
// 3. When invoked from `PipelineOwner.flushLayout`, or
|
||||
// `_layoutSurrogate.performLayout`, this `RenderObject` behaves like an
|
||||
// `Overlay` that has only one entry.
|
||||
// When invoked from `PipelineOwner.flushLayout`, this `RenderObject` behaves
|
||||
// like an `Overlay` that has only one entry.
|
||||
final class _RenderDeferredLayoutBox extends RenderProxyBox
|
||||
with _RenderTheaterMixin, LinkedListEntry<_RenderDeferredLayoutBox> {
|
||||
_RenderDeferredLayoutBox(this._layoutSurrogate);
|
||||
@ -2351,12 +2413,10 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox
|
||||
Iterable<RenderBox> _childrenInHitTestOrder() => _childrenInPaintOrder();
|
||||
|
||||
@override
|
||||
_RenderTheater get theater {
|
||||
final RenderObject? parent = this.parent;
|
||||
return parent is _RenderTheater
|
||||
? parent
|
||||
: throw FlutterError('$parent of $this is not a _RenderTheater');
|
||||
}
|
||||
_RenderTheater get theater => switch (parent) {
|
||||
final _RenderTheater parent => parent,
|
||||
_ => throw FlutterError('$parent of $this is not a _RenderTheater'),
|
||||
};
|
||||
|
||||
@override
|
||||
void redepthChildren() {
|
||||
@ -2367,6 +2427,11 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox
|
||||
@override
|
||||
bool get sizedByParent => true;
|
||||
|
||||
bool get needsLayout {
|
||||
assert(debugNeedsLayout == _needsLayout);
|
||||
return _needsLayout;
|
||||
}
|
||||
|
||||
bool _needsLayout = true;
|
||||
@override
|
||||
void markNeedsLayout() {
|
||||
@ -2395,53 +2460,47 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox
|
||||
@override
|
||||
RenderObject? get debugLayoutParent => _layoutSurrogate;
|
||||
|
||||
void layoutByLayoutSurrogate() {
|
||||
assert(!_theaterDoingThisLayout);
|
||||
final _RenderTheater? theater = parent as _RenderTheater?;
|
||||
if (theater == null || !attached) {
|
||||
assert(false, '$this is not attached to parent');
|
||||
return;
|
||||
}
|
||||
if (theater._layingOutSizeDeterminingChild) {
|
||||
theater.invokeLayoutCallback((BoxConstraints constraints) {
|
||||
/// Whether this RenderBox's layout method is currently being called by the
|
||||
/// theater or the layoutSurrogate's [performLayout] implementation.
|
||||
bool _doingLayoutFromTreeWalk = false;
|
||||
void _doLayoutFrom(RenderObject treewalkParent, {required Constraints constraints}) {
|
||||
final bool shouldAddToDirtyList = needsLayout || this.constraints != constraints;
|
||||
assert(!_doingLayoutFromTreeWalk);
|
||||
_doingLayoutFromTreeWalk = true;
|
||||
super.layout(constraints);
|
||||
assert(_doingLayoutFromTreeWalk);
|
||||
_doingLayoutFromTreeWalk = false;
|
||||
_needsLayout = false;
|
||||
assert(!debugNeedsLayout);
|
||||
if (shouldAddToDirtyList) {
|
||||
// Instead of laying out this subtree via treewalk, adding it to the dirty
|
||||
// list. This ensures:
|
||||
//
|
||||
// 1. this node will be laid out by the PipelineOwner *after* the two
|
||||
// nodes it depends on (the theater and the layout surrogate) are
|
||||
// laid out, as it has a greater depth value than its dependencies.
|
||||
//
|
||||
// 2. when the deferred child's child starts to do layout, the nodes
|
||||
// from the layout surrogate to the theater (exclusive) have finishd
|
||||
// doing layout, so the deferred child's child can read their sizes
|
||||
// and (usually) compute the paint transform of the regular child
|
||||
// within the Overlay.
|
||||
//
|
||||
// Invoking markNeedsLayout as a layout callback allows this node to be
|
||||
// merged back to the `PipelineOwner`'s dirty list in the right order, if
|
||||
// it's not already dirty, such that this subtree does not get laid out
|
||||
// twice.
|
||||
treewalkParent.invokeLayoutCallback((BoxConstraints _) {
|
||||
markNeedsLayout();
|
||||
});
|
||||
} else {
|
||||
final BoxConstraints theaterConstraints = theater.constraints;
|
||||
final Size boxSize =
|
||||
theaterConstraints.biggest.isFinite
|
||||
? theaterConstraints.biggest
|
||||
// Accessing the theater's size is only unsafe if it is laying out the
|
||||
// size-determining child.
|
||||
: theater.size;
|
||||
super.layout(BoxConstraints.tight(boxSize));
|
||||
}
|
||||
}
|
||||
|
||||
bool _theaterDoingThisLayout = false;
|
||||
@override
|
||||
void layout(Constraints constraints, {bool parentUsesSize = false}) {
|
||||
assert(_needsLayout == debugNeedsLayout);
|
||||
// Only _RenderTheater calls this implementation.
|
||||
assert(parent != null);
|
||||
final bool scheduleDeferredLayout = _needsLayout || this.constraints != constraints;
|
||||
assert(!_theaterDoingThisLayout);
|
||||
_theaterDoingThisLayout = true;
|
||||
super.layout(constraints, parentUsesSize: parentUsesSize);
|
||||
assert(_theaterDoingThisLayout);
|
||||
_theaterDoingThisLayout = false;
|
||||
_needsLayout = false;
|
||||
assert(!debugNeedsLayout);
|
||||
if (scheduleDeferredLayout) {
|
||||
final _RenderTheater parent = this.parent! as _RenderTheater;
|
||||
// Invoking markNeedsLayout as a layout callback allows this node to be
|
||||
// merged back to the `PipelineOwner`'s dirty list in the right order, if
|
||||
// it's not already dirty. Otherwise this may cause some dirty descendants
|
||||
// to performLayout a second time.
|
||||
parent.invokeLayoutCallback((BoxConstraints constraints) {
|
||||
markNeedsLayout();
|
||||
});
|
||||
}
|
||||
// The `parentUsesSize` flag can be safely ignored since this render box is
|
||||
// sized by the parent.
|
||||
_doLayoutFrom(parent!, constraints: constraints);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -2453,7 +2512,7 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox
|
||||
@override
|
||||
void performLayout() {
|
||||
assert(!_debugMutationsLocked);
|
||||
if (_theaterDoingThisLayout) {
|
||||
if (_doingLayoutFromTreeWalk) {
|
||||
_needsLayout = false;
|
||||
return;
|
||||
}
|
||||
@ -2495,10 +2554,9 @@ class _RenderLayoutSurrogateProxyBox extends RenderProxyBox {
|
||||
void redepthChildren() {
|
||||
super.redepthChildren();
|
||||
final _RenderDeferredLayoutBox? child = _deferredLayoutChild;
|
||||
// If child is not attached, this method will be invoked by child's real
|
||||
// parent when it's attached.
|
||||
// If child is not attached yet, this method will be invoked by child's real
|
||||
// parent (the theater) when it becomes attached.
|
||||
if (child != null && child.attached) {
|
||||
assert(child.attached);
|
||||
redepthChild(child);
|
||||
}
|
||||
}
|
||||
@ -2506,11 +2564,33 @@ class _RenderLayoutSurrogateProxyBox extends RenderProxyBox {
|
||||
@override
|
||||
void performLayout() {
|
||||
super.performLayout();
|
||||
// Try to layout `_deferredLayoutChild` here now that its configuration
|
||||
// and constraints are up-to-date. Additionally, during the very first
|
||||
// layout, this makes sure that _deferredLayoutChild is reachable via tree
|
||||
// walk.
|
||||
_deferredLayoutChild?.layoutByLayoutSurrogate();
|
||||
final _RenderDeferredLayoutBox? deferredChild = _deferredLayoutChild;
|
||||
if (deferredChild == null) {
|
||||
return;
|
||||
}
|
||||
// To make sure all ancestors' performLayout calls have returned when
|
||||
// the deferred child does layout, the deferred child needs to be put in
|
||||
// the dirty list if it is dirty, and make the deferred child subtree
|
||||
// unreachable via layout tree walk.
|
||||
//
|
||||
// The deferred child is guaranteed to be a relayout boundary but it may
|
||||
// still not be in the dirty list if it has never been laid out before
|
||||
// (its _relayoutBoundary is unknown to the framework so it's not treated as
|
||||
// one). The code below handles this case and makes sure the deferred child
|
||||
// is in the dirty list.
|
||||
final _RenderTheater theater = deferredChild.parent! as _RenderTheater;
|
||||
// If the theater is laying out the size-determining child, its size is not
|
||||
// available yet. Since the theater always lays out the size-determining
|
||||
// child first and the deferred child can never be size-determining,
|
||||
// this method does not have to do anything, the theater will update the
|
||||
// constraints of the deferred child and resize / put it in the dirty list if
|
||||
// needed.
|
||||
if (!theater._layingOutSizeDeterminingChild) {
|
||||
final BoxConstraints theaterConstraints = theater.constraints;
|
||||
final Size boxSize =
|
||||
theaterConstraints.biggest.isFinite ? theaterConstraints.biggest : theater.size;
|
||||
deferredChild._doLayoutFrom(this, constraints: BoxConstraints.tight(boxSize));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -2522,3 +2602,190 @@ class _RenderLayoutSurrogateProxyBox extends RenderProxyBox {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _OverlayChildLayoutBuilder extends AbstractLayoutBuilder<OverlayChildLayoutInfo> {
|
||||
const _OverlayChildLayoutBuilder({required this.builder});
|
||||
|
||||
@override
|
||||
final OverlayChildLayoutBuilder builder;
|
||||
|
||||
@override
|
||||
RenderAbstractLayoutBuilderMixin<OverlayChildLayoutInfo, RenderBox> createRenderObject(
|
||||
BuildContext context,
|
||||
) => _RenderLayoutBuilder();
|
||||
|
||||
@override
|
||||
bool updateShouldRebuild(_OverlayChildLayoutBuilder oldWidget) => oldWidget.builder != builder;
|
||||
}
|
||||
|
||||
// A RenderBox that:
|
||||
// - has the same size and paint transform, as its parent and its theater, in
|
||||
// other words the three RenderBoxes describe the same rect on screen.
|
||||
// - is a relayout boundary, and gets marked dirty for relayout every frame
|
||||
// (but only when a frame is already scheduled, and markNeedsLayout does not
|
||||
// schedule a new frame since it's called in a transient callback).
|
||||
// - runs a layout callback in performLayout.
|
||||
//
|
||||
// Additionally, like RenderDeferredLayoutBox, this RenderBox also uses the Stack
|
||||
// layout algorithm so developers can use the Positioned widget.
|
||||
class _RenderLayoutBuilder extends RenderProxyBox
|
||||
with _RenderTheaterMixin, RenderAbstractLayoutBuilderMixin<OverlayChildLayoutInfo, RenderBox> {
|
||||
@override
|
||||
Iterable<RenderBox> _childrenInPaintOrder() {
|
||||
final RenderBox? child = this.child;
|
||||
return child == null
|
||||
? const Iterable<RenderBox>.empty()
|
||||
: Iterable<RenderBox>.generate(1, (int i) => child);
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<RenderBox> _childrenInHitTestOrder() => _childrenInPaintOrder();
|
||||
|
||||
@override
|
||||
_RenderTheater get theater => switch (parent) {
|
||||
final _RenderDeferredLayoutBox parent => parent.theater,
|
||||
_ => throw FlutterError('$parent of $this is not a _RenderDeferredLayoutBox'),
|
||||
};
|
||||
|
||||
@override
|
||||
bool get sizedByParent => true;
|
||||
|
||||
@override
|
||||
void performResize() => size = constraints.biggest;
|
||||
|
||||
@override
|
||||
void applyPaintTransform(RenderBox child, Matrix4 transform) {
|
||||
final BoxParentData childParentData = child.parentData! as BoxParentData;
|
||||
final Offset offset = childParentData.offset;
|
||||
transform.translate(offset.dx, offset.dy);
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
OverlayChildLayoutInfo get layoutInfo => _layoutInfo!;
|
||||
// The size here is the child size of the regular child in its own parent's coordinates.
|
||||
OverlayChildLayoutInfo? _layoutInfo;
|
||||
OverlayChildLayoutInfo _computeNewLayoutInfo() {
|
||||
final _RenderTheater theater = this.theater;
|
||||
final _RenderDeferredLayoutBox parent = this.parent! as _RenderDeferredLayoutBox;
|
||||
final _RenderLayoutSurrogateProxyBox layoutSurrogate = parent._layoutSurrogate;
|
||||
assert(() {
|
||||
for (
|
||||
RenderObject? node = layoutSurrogate;
|
||||
node != null && node != theater;
|
||||
node = node.parent
|
||||
) {
|
||||
if (node is RenderFollowerLayer) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'The paint transform cannot be reliably computed because of RenderFollowerLayer(s)',
|
||||
),
|
||||
node.describeForError('The RenderFollowerLayer was'),
|
||||
ErrorDescription(
|
||||
'RenderFollowerLayer establishes its paint transform only after the layout phase.',
|
||||
),
|
||||
ErrorHint(
|
||||
'Consider replacing the corresponding CompositedTransformFollower with OverlayPortal.overlayChildLayoutBuilder if possible.',
|
||||
),
|
||||
]);
|
||||
}
|
||||
assert(node.depth > theater.depth);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
assert(layoutSurrogate.hasSize);
|
||||
assert(layoutSurrogate.child?.hasSize ?? true);
|
||||
assert(layoutSurrogate.child == null || layoutSurrogate.child!.size == layoutSurrogate.size);
|
||||
assert(size == theater.size);
|
||||
assert(layoutSurrogate.child?.getTransformTo(layoutSurrogate).isIdentity() ?? true);
|
||||
// The paint transform we're about to compute is only useful if this RenderBox
|
||||
// uses the same coordinates as the theater.
|
||||
assert(getTransformTo(theater).isIdentity());
|
||||
final Size overlayPortalSize = parent._layoutSurrogate.size;
|
||||
final Matrix4 paintTransform = layoutSurrogate.getTransformTo(theater);
|
||||
return OverlayChildLayoutInfo._((overlayPortalSize, paintTransform, size));
|
||||
}
|
||||
|
||||
int? _callbackId;
|
||||
@override
|
||||
void performLayout() {
|
||||
late OverlayChildLayoutInfo newLayoutInfo;
|
||||
// The invokeLayoutCallback allows arbitrary access to the sizes of
|
||||
// RenderBoxes the we know that have finished doing layout.
|
||||
invokeLayoutCallback((_) => newLayoutInfo = _computeNewLayoutInfo());
|
||||
if (newLayoutInfo != _layoutInfo) {
|
||||
_layoutInfo = newLayoutInfo;
|
||||
rebuildIfNecessary();
|
||||
}
|
||||
assert(_callbackId == null);
|
||||
_callbackId ??= SchedulerBinding.instance.scheduleFrameCallback(
|
||||
_frameCallback,
|
||||
scheduleNewFrame: false,
|
||||
);
|
||||
layoutChild(child!, constraints);
|
||||
}
|
||||
|
||||
// This RenderObject is a child of _RenderDeferredLayouts which in turn is a
|
||||
// child of _RenderTheater. None of them do speculative layout and
|
||||
// _RenderDeferredLayouts don't participate in _RenderTheater's intrinsics
|
||||
// calculations. Since the layout callback may mutate the live render tree
|
||||
// during layout, intrinsic calculations are neither available nor needed.
|
||||
static const String _speculativeLayoutErrorMessage =
|
||||
'This RenderObject should not be reachable in intrinsic dimension calculations.';
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
assert(debugCannotComputeDryLayout(reason: _speculativeLayoutErrorMessage));
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
assert(debugCannotComputeDryLayout(reason: _speculativeLayoutErrorMessage));
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
assert(debugCannotComputeDryLayout(reason: _speculativeLayoutErrorMessage));
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
assert(debugCannotComputeDryLayout(reason: _speculativeLayoutErrorMessage));
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
assert(debugCannotComputeDryLayout(reason: _speculativeLayoutErrorMessage));
|
||||
return Size.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) {
|
||||
assert(
|
||||
debugCannotComputeDryLayout(
|
||||
reason:
|
||||
'Calculating the dry baseline would require running the layout callback '
|
||||
'speculatively, which might mutate the live render object tree.',
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
void _frameCallback(Duration _) {
|
||||
assert(!debugDisposed!);
|
||||
_callbackId = null;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_callbackId case final int callbackId) {
|
||||
SchedulerBinding.instance.cancelFrameCallbackWithId(callbackId);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,19 +29,25 @@ class SliverLayoutBuilder extends ConstrainedLayoutBuilder<SliverConstraints> {
|
||||
const SliverLayoutBuilder({super.key, required super.builder});
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) => _RenderSliverLayoutBuilder();
|
||||
RenderAbstractLayoutBuilderMixin<SliverConstraints, RenderSliver> createRenderObject(
|
||||
BuildContext context,
|
||||
) => _RenderSliverLayoutBuilder();
|
||||
}
|
||||
|
||||
class _RenderSliverLayoutBuilder extends RenderSliver
|
||||
with
|
||||
RenderObjectWithChildMixin<RenderSliver>,
|
||||
RenderConstrainedLayoutBuilder<SliverConstraints, RenderSliver> {
|
||||
RenderAbstractLayoutBuilderMixin<SliverConstraints, RenderSliver> {
|
||||
@override
|
||||
double childMainAxisPosition(RenderObject child) {
|
||||
assert(child == this.child);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
SliverConstraints get layoutInfo => constraints;
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
rebuildIfNecessary();
|
||||
|
||||
@ -58,6 +58,9 @@ void main() {
|
||||
tearDown(() {
|
||||
scheduler.additionalHandleBeginFrame = null;
|
||||
scheduler.additionalHandleDrawFrame = null;
|
||||
PlatformDispatcher.instance
|
||||
..onBeginFrame = null
|
||||
..onDrawFrame = null;
|
||||
});
|
||||
|
||||
test('Tasks are executed in the right order', () {
|
||||
@ -333,6 +336,29 @@ void main() {
|
||||
|
||||
expect(isCompleted, true);
|
||||
});
|
||||
|
||||
test('Can schedule a frame callback with / without scheduling a new frame', () {
|
||||
scheduler.handleBeginFrame(null);
|
||||
scheduler.handleDrawFrame();
|
||||
bool callbackInvoked = false;
|
||||
|
||||
assert(!scheduler.hasScheduledFrame);
|
||||
scheduler.scheduleFrameCallback(scheduleNewFrame: false, (_) => callbackInvoked = true);
|
||||
expect(scheduler.hasScheduledFrame, isFalse);
|
||||
scheduler.handleBeginFrame(null);
|
||||
scheduler.handleDrawFrame();
|
||||
expect(callbackInvoked, isTrue);
|
||||
|
||||
assert(!scheduler.hasScheduledFrame);
|
||||
callbackInvoked = false;
|
||||
scheduler.scheduleFrameCallback((_) => callbackInvoked = true);
|
||||
expect(scheduler.hasScheduledFrame, isTrue);
|
||||
scheduler.handleBeginFrame(null);
|
||||
scheduler.handleDrawFrame();
|
||||
expect(callbackInvoked, isTrue);
|
||||
|
||||
assert(!scheduler.hasScheduledFrame);
|
||||
});
|
||||
}
|
||||
|
||||
class DummyTimer implements Timer {
|
||||
|
||||
@ -903,7 +903,7 @@ class _SmartLayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
|
||||
}
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
_RenderSmartLayoutBuilder createRenderObject(BuildContext context) {
|
||||
return _RenderSmartLayoutBuilder(
|
||||
offsetPercentage: offsetPercentage,
|
||||
onChildWasPainted: onChildWasPainted,
|
||||
@ -921,7 +921,7 @@ class _SmartLayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
|
||||
typedef _OnChildWasPaintedCallback = void Function(Offset extraOffset);
|
||||
|
||||
class _RenderSmartLayoutBuilder extends RenderProxyBox
|
||||
with RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox> {
|
||||
with RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox> {
|
||||
_RenderSmartLayoutBuilder({required double offsetPercentage, required this.onChildWasPainted})
|
||||
: _offsetPercentage = offsetPercentage;
|
||||
|
||||
@ -961,6 +961,10 @@ class _RenderSmartLayoutBuilder extends RenderProxyBox
|
||||
onChildWasPainted(extraOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
BoxConstraints get layoutInfo => constraints;
|
||||
}
|
||||
|
||||
class _LayoutSpy extends LeafRenderObjectWidget {
|
||||
|
||||
320
packages/flutter/test/widgets/overlay_layout_builder_test.dart
Normal file
320
packages/flutter/test/widgets/overlay_layout_builder_test.dart
Normal file
@ -0,0 +1,320 @@
|
||||
// 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 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
final OverlayPortalController controller1 = OverlayPortalController(debugLabel: 'controller1');
|
||||
setUp(controller1.show);
|
||||
|
||||
testWidgets('Basic test', (WidgetTester tester) async {
|
||||
late StateSetter setState;
|
||||
Matrix4 transform = Matrix4.identity();
|
||||
late final OverlayEntry overlayEntry;
|
||||
addTearDown(() {
|
||||
overlayEntry
|
||||
..remove()
|
||||
..dispose();
|
||||
});
|
||||
|
||||
late Matrix4 paintTransform;
|
||||
late Size regularChildSize;
|
||||
late Rect regularChildRectInTheater;
|
||||
late Size theaterSize;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Positioned(
|
||||
left: 10,
|
||||
top: 20,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setter) {
|
||||
setState = setter;
|
||||
return Transform(
|
||||
transform: transform,
|
||||
// RenderTransform uses size in its applyPaintTransform
|
||||
// implementation if alignment is set.
|
||||
alignment: Alignment.topLeft,
|
||||
child: OverlayPortal.overlayChildLayoutBuilder(
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (
|
||||
BuildContext context,
|
||||
OverlayChildLayoutInfo layoutInfo,
|
||||
) {
|
||||
paintTransform = layoutInfo.childPaintTransform;
|
||||
regularChildSize = layoutInfo.childSize;
|
||||
regularChildRectInTheater = MatrixUtils.transformRect(
|
||||
paintTransform,
|
||||
Offset.zero & layoutInfo.childSize,
|
||||
);
|
||||
theaterSize = layoutInfo.overlaySize;
|
||||
return const SizedBox();
|
||||
},
|
||||
child: const SizedBox(width: 40, height: 50),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
// Does not schedule a new frame by itself.
|
||||
expect(tester.binding.hasScheduledFrame, isFalse);
|
||||
expect(paintTransform, Matrix4.translationValues(10.0, 20.0, 0.0));
|
||||
expect(regularChildSize, const Size(40, 50));
|
||||
expect(theaterSize, const Size(800, 600));
|
||||
expect(regularChildRectInTheater, const Offset(10.0, 20.0) & regularChildSize);
|
||||
|
||||
setState(() => transform = Matrix4.diagonal3Values(2.0, 4.0, 1.0));
|
||||
assert(tester.binding.hasScheduledFrame);
|
||||
await tester.pump();
|
||||
|
||||
expect(paintTransform, Matrix4.translationValues(10.0, 20.0, 0.0) * transform);
|
||||
expect(regularChildSize, const Size(40, 50));
|
||||
expect(theaterSize, const Size(800, 600));
|
||||
expect(regularChildRectInTheater, const Offset(10.0, 20.0) & const Size(80.0, 200.0));
|
||||
});
|
||||
|
||||
testWidgets('child changes size', (WidgetTester tester) async {
|
||||
late StateSetter setState;
|
||||
late final OverlayEntry overlayEntry;
|
||||
addTearDown(
|
||||
() =>
|
||||
overlayEntry
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
|
||||
late Size regularChildSize;
|
||||
Size childSize = const Size(40, 50);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Positioned(
|
||||
left: 10,
|
||||
top: 20,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setter) {
|
||||
setState = setter;
|
||||
return OverlayPortal.overlayChildLayoutBuilder(
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (
|
||||
BuildContext context,
|
||||
OverlayChildLayoutInfo layoutInfo,
|
||||
) {
|
||||
regularChildSize = layoutInfo.childSize;
|
||||
return const SizedBox();
|
||||
},
|
||||
child: SizedBox.fromSize(size: childSize),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(regularChildSize, childSize);
|
||||
|
||||
setState(() => childSize = const Size(123.0, 321.0));
|
||||
|
||||
await tester.pump();
|
||||
expect(regularChildSize, childSize);
|
||||
});
|
||||
|
||||
testWidgets('Positioned works in the builder', (WidgetTester tester) async {
|
||||
late final OverlayEntry overlayEntry;
|
||||
addTearDown(
|
||||
() =>
|
||||
overlayEntry
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
|
||||
final GlobalKey key = GlobalKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return OverlayPortal.overlayChildLayoutBuilder(
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (_, _) {
|
||||
return Positioned(
|
||||
left: 123.0,
|
||||
top: 37.0,
|
||||
width: 12.0,
|
||||
height: 23.0,
|
||||
child: SizedBox(key: key),
|
||||
);
|
||||
},
|
||||
child: const SizedBox(width: 10.0, height: 20.0),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Rect rect = tester.getRect(find.byKey(key));
|
||||
expect(rect, const Rect.fromLTWH(123.0, 37.0, 12.0, 23.0));
|
||||
});
|
||||
|
||||
testWidgets('Rebuilds when the layout info changes', (WidgetTester tester) async {
|
||||
late StateSetter setState;
|
||||
Matrix4 transform = Matrix4.identity();
|
||||
late final OverlayEntry overlayEntry;
|
||||
addTearDown(
|
||||
() =>
|
||||
overlayEntry
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
|
||||
late Matrix4 paintTransform;
|
||||
|
||||
Widget buildOverlayChild(BuildContext context, OverlayChildLayoutInfo layoutInfo) {
|
||||
paintTransform = layoutInfo.childPaintTransform;
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Positioned(
|
||||
left: 10,
|
||||
top: 20,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setter) {
|
||||
setState = setter;
|
||||
return Transform(
|
||||
transform: transform,
|
||||
child: OverlayPortal.overlayChildLayoutBuilder(
|
||||
controller: controller1,
|
||||
overlayChildBuilder: buildOverlayChild,
|
||||
child: const SizedBox(width: 40, height: 50),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(paintTransform, Matrix4.translationValues(10.0, 20.0, 0.0));
|
||||
setState(() => transform = Matrix4.diagonal3Values(2.0, 4.0, 1.0));
|
||||
await tester.pump();
|
||||
expect(paintTransform, Matrix4.translationValues(10.0, 20.0, 0.0) * transform);
|
||||
});
|
||||
|
||||
testWidgets('Still works if child is null', (WidgetTester tester) async {
|
||||
late final OverlayEntry overlayEntry;
|
||||
addTearDown(
|
||||
() =>
|
||||
overlayEntry
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
|
||||
late Size regularChildSize;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Positioned(
|
||||
left: 10,
|
||||
top: 20,
|
||||
child: OverlayPortal.overlayChildLayoutBuilder(
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (BuildContext context, OverlayChildLayoutInfo layoutInfo) {
|
||||
regularChildSize = layoutInfo.childSize;
|
||||
return const SizedBox();
|
||||
},
|
||||
child: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(regularChildSize, Size.zero);
|
||||
});
|
||||
|
||||
testWidgets('Screams if RenderFollower is spotted in path', (WidgetTester tester) async {
|
||||
late final OverlayEntry overlayEntry;
|
||||
addTearDown(
|
||||
() =>
|
||||
overlayEntry
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return CompositedTransformFollower(
|
||||
link: LayerLink(),
|
||||
child: OverlayPortal.overlayChildLayoutBuilder(
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (_, _) => const SizedBox(),
|
||||
child: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
phase: EnginePhase.layout,
|
||||
);
|
||||
|
||||
expect(
|
||||
tester.takeException(),
|
||||
isA<FlutterError>().having(
|
||||
(FlutterError error) => error.message,
|
||||
'message',
|
||||
contains('RenderFollowerLayer'),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -26,11 +26,11 @@ class _ManyRelayoutBoundaries extends StatelessWidget {
|
||||
}
|
||||
|
||||
void rebuildLayoutBuilderSubtree(RenderBox descendant, WidgetTester tester) {
|
||||
assert(descendant is! RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox>);
|
||||
assert(descendant is! RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox>);
|
||||
|
||||
RenderObject? node = descendant.parent;
|
||||
while (node != null) {
|
||||
if (node is! RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox>) {
|
||||
if (node is! RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox>) {
|
||||
node = node.parent;
|
||||
} else {
|
||||
final Element layoutBuilderElement = tester.element(
|
||||
@ -2793,6 +2793,81 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'overlay child can compute the paint transform of the regular child relative to the Overlay',
|
||||
(WidgetTester tester) async {
|
||||
late StateSetter setState;
|
||||
EdgeInsets padding = const EdgeInsets.only(left: 10.0);
|
||||
late Matrix4 computedPaintTransform;
|
||||
double zOffset = 123.0;
|
||||
|
||||
late final OverlayEntry entry;
|
||||
addTearDown(() {
|
||||
entry.remove();
|
||||
entry.dispose();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
entry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setter) {
|
||||
setState = setter;
|
||||
return Transform(
|
||||
transform: Matrix4.translationValues(0.0, 0.0, zOffset),
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: OverlayPortal(
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
final RenderBox placeholderRenderBox = tester.renderObject(
|
||||
find.byType(Placeholder),
|
||||
);
|
||||
final RenderBox overlayRenderBox = tester.renderObject(
|
||||
find.byType(Overlay),
|
||||
);
|
||||
computedPaintTransform = placeholderRenderBox.getTransformTo(
|
||||
overlayRenderBox,
|
||||
);
|
||||
assert(placeholderRenderBox.hasSize);
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Placeholder(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// During the initial layout, the Padding wouldn't have computed its
|
||||
// child's offset if the overlay child was laid out via treewalk, since
|
||||
// RenderPadding.performLayout calls child.layout before computing the
|
||||
// child offset.
|
||||
expect(computedPaintTransform, Matrix4.translationValues(10.0, 0.0, 123.0));
|
||||
|
||||
setState(() {
|
||||
padding = const EdgeInsets.only(top: 20.0);
|
||||
zOffset = 321.0;
|
||||
});
|
||||
await tester.pump();
|
||||
expect(computedPaintTransform, Matrix4.translationValues(0.0, 20.0, 321.0));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class OverlayStatefulEntry extends OverlayEntry {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user