diff --git a/packages/flutter/lib/src/rendering/stack.dart b/packages/flutter/lib/src/rendering/stack.dart index dd4660aadffd..ddefa6b33ebb 100644 --- a/packages/flutter/lib/src/rendering/stack.dart +++ b/packages/flutter/lib/src/rendering/stack.dart @@ -229,6 +229,29 @@ class StackParentData extends ContainerBoxParentData { /// children in the stack. bool get isPositioned => top != null || right != null || bottom != null || left != null || width != null || height != null; + /// Computes the [BoxConstraints] the stack layout algorithm would give to + /// this child, given the [Size] of the stack. + /// + /// This method should only be called when [isPositioned] is true for the child. + BoxConstraints positionedChildConstraints(Size stackSize) { + assert(isPositioned); + final double? width = switch ((left, right)) { + (final double left?, final double right?) => stackSize.width - right - left, + (_, _) => this.width, + }; + + final double? height = switch ((top, bottom)) { + (final double top?, final double bottom?) => stackSize.height - bottom - top, + (_, _) => this.height, + }; + assert(height == null || !height.isNaN); + assert(width == null || !width.isNaN); + return BoxConstraints.tightFor( + width: width == null ? null : math.max(0.0, width), + height: height == null ? null : math.max(0.0, height), + ); + } + @override String toString() { final List values = [ @@ -354,17 +377,11 @@ class RenderStack extends RenderBox } } - Alignment? _resolvedAlignment; - - void _resolve() { - if (_resolvedAlignment != null) { - return; - } - _resolvedAlignment = alignment.resolve(textDirection); - } + Alignment get _resolvedAlignment => _resolvedAlignmentCache ??= alignment.resolve(textDirection); + Alignment? _resolvedAlignmentCache; void _markNeedResolution() { - _resolvedAlignment = null; + _resolvedAlignmentCache = null; markNeedsLayout(); } @@ -489,53 +506,59 @@ class RenderStack extends RenderBox static bool layoutPositionedChild(RenderBox child, StackParentData childParentData, Size size, Alignment alignment) { assert(childParentData.isPositioned); assert(child.parentData == childParentData); + final BoxConstraints childConstraints = childParentData.positionedChildConstraints(size); + child.layout(childConstraints, parentUsesSize: true); - bool hasVisualOverflow = false; - BoxConstraints childConstraints = const BoxConstraints(); - - if (childParentData.left != null && childParentData.right != null) { - childConstraints = childConstraints.tighten(width: size.width - childParentData.right! - childParentData.left!); - } else if (childParentData.width != null) { - childConstraints = childConstraints.tighten(width: childParentData.width); - } + final double x = switch (childParentData) { + StackParentData(:final double left?) => left, + StackParentData(:final double right?) => size.width - right - child.size.width, + StackParentData() => alignment.alongOffset(size - child.size as Offset).dx, + }; - if (childParentData.top != null && childParentData.bottom != null) { - childConstraints = childConstraints.tighten(height: size.height - childParentData.bottom! - childParentData.top!); - } else if (childParentData.height != null) { - childConstraints = childConstraints.tighten(height: childParentData.height); - } + final double y = switch (childParentData) { + StackParentData(:final double top?) => top, + StackParentData(:final double bottom?) => size.height - bottom - child.size.height, + StackParentData() => alignment.alongOffset(size - child.size as Offset).dy, + }; - child.layout(childConstraints, parentUsesSize: true); + childParentData.offset = Offset(x, y); + return x < 0.0 || x + child.size.width > size.width + || y < 0.0 || y + child.size.height > size.height; + } - final double x; - if (childParentData.left != null) { - x = childParentData.left!; - } else if (childParentData.right != null) { - x = size.width - childParentData.right! - child.size.width; - } else { - x = alignment.alongOffset(size - child.size as Offset).dx; + static double? _baselineForChild(RenderBox child, Size stackSize, BoxConstraints nonPositionedChildConstraints, Alignment alignment, TextBaseline baseline) { + final StackParentData childParentData = child.parentData! as StackParentData; + final BoxConstraints childConstraints = childParentData.isPositioned + ? childParentData.positionedChildConstraints(stackSize) + : nonPositionedChildConstraints; + final double? baselineOffset = child.getDryBaseline(childConstraints, baseline); + if (baselineOffset == null) { + return null; } + final double y = switch (childParentData) { + StackParentData(:final double top?) => top, + StackParentData(:final double bottom?) => stackSize.height - bottom - child.getDryLayout(childConstraints).height, + StackParentData() => alignment.alongOffset(stackSize - child.getDryLayout(childConstraints) as Offset).dy, + }; + return baselineOffset + y; + } - if (x < 0.0 || x + child.size.width > size.width) { - hasVisualOverflow = true; - } + @override + double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) { + final BoxConstraints nonPositionedChildConstraints = switch (fit) { + StackFit.loose => constraints.loosen(), + StackFit.expand => BoxConstraints.tight(constraints.biggest), + StackFit.passthrough => constraints, + }; - final double y; - if (childParentData.top != null) { - y = childParentData.top!; - } else if (childParentData.bottom != null) { - y = size.height - childParentData.bottom! - child.size.height; - } else { - y = alignment.alongOffset(size - child.size as Offset).dy; - } + final Alignment alignment = _resolvedAlignment; + final Size size = getDryLayout(constraints); - if (y < 0.0 || y + child.size.height > size.height) { - hasVisualOverflow = true; + BaselineOffset baselineOffset = BaselineOffset.noBaseline; + for (RenderBox? child = firstChild; child != null; child = childAfter(child)) { + baselineOffset = baselineOffset.minOf(BaselineOffset(_baselineForChild(child, size, nonPositionedChildConstraints, alignment, baseline))); } - - childParentData.offset = Offset(x, y); - - return hasVisualOverflow; + return baselineOffset.offset; } @override @@ -548,8 +571,6 @@ class RenderStack extends RenderBox } Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild}) { - _resolve(); - assert(_resolvedAlignment != null); bool hasNonPositionedChildren = false; if (childCount == 0) { return (constraints.biggest.isFinite) ? constraints.biggest : constraints.smallest; @@ -603,15 +624,15 @@ class RenderStack extends RenderBox layoutChild: ChildLayoutHelper.layoutChild, ); - assert(_resolvedAlignment != null); + final Alignment resolvedAlignment = _resolvedAlignment; RenderBox? child = firstChild; while (child != null) { final StackParentData childParentData = child.parentData! as StackParentData; if (!childParentData.isPositioned) { - childParentData.offset = _resolvedAlignment!.alongOffset(size - child.size as Offset); + childParentData.offset = resolvedAlignment.alongOffset(size - child.size as Offset); } else { - _hasVisualOverflow = layoutPositionedChild(child, childParentData, size, _resolvedAlignment!) || _hasVisualOverflow; + _hasVisualOverflow = layoutPositionedChild(child, childParentData, size, resolvedAlignment) || _hasVisualOverflow; } assert(child.parentData == childParentData); @@ -740,6 +761,25 @@ class RenderIndexedStack extends RenderStack { return offset.offset; } + @override + double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) { + final RenderBox? displayedChild = _childAtIndex(); + if (displayedChild == null) { + return null; + } + final BoxConstraints nonPositionedChildConstraints = switch (fit) { + StackFit.loose => constraints.loosen(), + StackFit.expand => BoxConstraints.tight(constraints.biggest), + StackFit.passthrough => constraints, + }; + + final Alignment alignment = _resolvedAlignment; + final Size size = getDryLayout(constraints); + + return RenderStack._baselineForChild(displayedChild, size, nonPositionedChildConstraints, alignment, baseline); + } + + @override bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { final RenderBox? displayedChild = _childAtIndex(); diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index 21c27720a73e..faf2e552c670 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:collection'; -import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -967,6 +966,35 @@ mixin _RenderTheaterMixin on RenderBox { } } + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + assert(!debugNeedsLayout); + BaselineOffset baselineOffset = BaselineOffset.noBaseline; + for (final RenderBox child in _childrenInPaintOrder()) { + assert(!child.debugNeedsLayout); + final StackParentData childParentData = child.parentData! as StackParentData; + baselineOffset = baselineOffset.minOf(BaselineOffset(child.getDistanceToActualBaseline(baseline)) + childParentData.offset.dy); + } + return baselineOffset.offset; + } + + static double? baselineForChild(RenderBox child, Size theaterSize, BoxConstraints nonPositionedChildConstraints, Alignment alignment, TextBaseline baseline) { + final StackParentData childParentData = child.parentData! as StackParentData; + final BoxConstraints childConstraints = childParentData.isPositioned + ? childParentData.positionedChildConstraints(theaterSize) + : nonPositionedChildConstraints; + final double? baselineOffset = child.getDryBaseline(childConstraints, baseline); + if (baselineOffset == null) { + return null; + } + final double y = switch (childParentData) { + StackParentData(:final double top?) => top, + StackParentData(:final double bottom?) => theaterSize.height - bottom - child.getDryLayout(childConstraints).height, + StackParentData() => alignment.alongOffset(theaterSize - child.getDryLayout(childConstraints) as Offset).dy, + }; + return baselineOffset + y; + } + void layoutChild(RenderBox child, BoxConstraints nonPositionedChildConstraints) { final StackParentData childParentData = child.parentData! as StackParentData; final Alignment alignment = theater._resolvedAlignment; @@ -1201,25 +1229,20 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin _layoutSurrogate; diff --git a/packages/flutter/test/rendering/stack_test.dart b/packages/flutter/test/rendering/stack_test.dart index bfb2e813cde4..00aabe888d01 100644 --- a/packages/flutter/test/rendering/stack_test.dart +++ b/packages/flutter/test/rendering/stack_test.dart @@ -10,6 +10,36 @@ import 'rendering_tester.dart'; void main() { TestRenderingFlutterBinding.ensureInitialized(); + test('StackParentData basic test', () { + final StackParentData parentData = StackParentData(); + const Size stackSize = Size(800.0, 600.0); + expect(parentData.isPositioned, isFalse); + + parentData.width = -100.0; + expect(parentData.isPositioned, isTrue); + expect(parentData.positionedChildConstraints(stackSize), const BoxConstraints.tightFor(width: 0.0)); + + parentData.width = 100.0; + expect(parentData.positionedChildConstraints(stackSize), const BoxConstraints.tightFor(width: 100.0)); + + parentData.left = 0.0; + parentData.right = 0.0; + expect(parentData.positionedChildConstraints(stackSize), const BoxConstraints.tightFor(width: 800.0)); + + parentData.height = -100.0; + expect(parentData.positionedChildConstraints(stackSize), const BoxConstraints.tightFor(width: 800.0, height: 0.0)); + + parentData.height = 100.0; + expect(parentData.positionedChildConstraints(stackSize), const BoxConstraints.tightFor(width: 800.0, height: 100.0)); + + parentData.top = 0.0; + parentData.bottom = 0.0; + expect(parentData.positionedChildConstraints(stackSize), const BoxConstraints.tightFor(width: 800.0, height: 600.0)); + + parentData.bottom = 1000.0; + expect(parentData.positionedChildConstraints(stackSize), const BoxConstraints.tightFor(width: 800.0, height: 0.0)); + }); + test('Stack can layout with top, right, bottom, left 0.0', () { final RenderBox size = RenderConstrainedBox( additionalConstraints: BoxConstraints.tight(const Size(100.0, 100.0)),