From fd98a2f70bc2f3454fd2476f204110061b864f56 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:37:05 -0700 Subject: [PATCH] Implements `RenderBox.computeDryBaseline` for material render boxes (#146027) `RenderChip` and `RenderInputDecorator` changes are larger so they are not included. --- .../flutter/lib/src/material/app_bar.dart | 65 ++-- .../lib/src/material/bottom_sheet.dart | 40 ++- packages/flutter/lib/src/material/button.dart | 14 + .../lib/src/material/button_style_button.dart | 14 + packages/flutter/lib/src/material/chip.dart | 12 +- .../flutter/lib/src/material/list_tile.dart | 337 +++++++++--------- .../flutter/lib/src/material/popup_menu.dart | 5 + .../lib/src/material/segmented_button.dart | 18 +- .../flutter/lib/src/material/time_picker.dart | 14 + .../lib/src/material/toggle_buttons.dart | 111 +++--- .../lib/src/rendering/shifted_box.dart | 23 +- 11 files changed, 387 insertions(+), 266 deletions(-) diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index dee365886c..45d1e2b1b2 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -2077,6 +2077,21 @@ class _RenderAppBarTitleBox extends RenderAligningShiftedBox { return constraints.constrain(childSize); } + @override + double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { + final BoxConstraints innerConstraints = constraints.copyWith(maxHeight: double.infinity); + final RenderBox? child = this.child; + if (child == null) { + return null; + } + final double? result = child.getDryBaseline(innerConstraints, baseline); + if (result == null) { + return null; + } + final Size childSize = child.getDryLayout(innerConstraints); + return result + resolvedAlignment.alongOffset(getDryLayout(constraints) - childSize as Offset).dy; + } + @override void performLayout() { final BoxConstraints innerConstraints = constraints.copyWith(maxHeight: double.infinity); @@ -2253,28 +2268,11 @@ class _RenderExpandedTitleBox extends RenderShiftedBox { return child == null ? 0.0 : child.getMinIntrinsicWidth(double.infinity) + padding.horizontal; } - Size _computeSize(BoxConstraints constraints, ChildLayouter layoutChild) { - final RenderBox? child = this.child; - if (child == null) { - return Size.zero; - } - layoutChild(child, constraints.widthConstraints().deflate(padding)); - return constraints.biggest; - } - @override - Size computeDryLayout(BoxConstraints constraints) => _computeSize(constraints, ChildLayoutHelper.dryLayoutChild); - - @override - void performLayout() { - final RenderBox? child = this.child; - if (child == null) { - this.size = constraints.smallest; - return; - } - final Size size = this.size = _computeSize(constraints, ChildLayoutHelper.layoutChild); - final Size childSize = child.size; + Size computeDryLayout(BoxConstraints constraints) => child == null ? Size.zero : constraints.biggest; + Offset _childOffsetFromSize(Size childSize, Size size) { + assert(child != null); assert(padding.isNonNegative); assert(titleAlignment.y == 1.0); // yAdjustment is the minimum additional y offset to shift the child in @@ -2284,11 +2282,34 @@ class _RenderExpandedTitleBox extends RenderShiftedBox { // top padding is basically ignored since the expanded title is // bottom-aligned). final double yAdjustment = clampDouble(childSize.height + padding.bottom - maxExtent, 0, padding.bottom); - final double offsetY = size.height - childSize.height - padding.bottom + yAdjustment; final double offsetX = (titleAlignment.x + 1) / 2 * (size.width - padding.horizontal - childSize.width) + padding.left; + final double offsetY = size.height - childSize.height - padding.bottom + yAdjustment; + return Offset(offsetX, offsetY); + } + @override + double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { + final RenderBox? child = this.child; + if (child == null) { + return null; + } + final BoxConstraints childConstraints = constraints.widthConstraints().deflate(padding); + final BaselineOffset result = BaselineOffset(child.getDryBaseline(childConstraints, baseline)) + + _childOffsetFromSize(child.getDryLayout(childConstraints), getDryLayout(constraints)).dy; + return result.offset; + } + + @override + void performLayout() { + final RenderBox? child = this.child; + if (child == null) { + size = constraints.smallest; + return; + } + size = constraints.biggest; + child.layout(constraints.widthConstraints().deflate(padding), parentUsesSize: true); final BoxParentData childParentData = child.parentData! as BoxParentData; - childParentData.offset = Offset(offsetX, offsetY); + childParentData.offset = _childOffsetFromSize(child.size, size); } } diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index e9a986aeba..ff7d73b7eb 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -615,6 +615,21 @@ class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox { return _getSize(constraints); } + @override + double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { + final RenderBox? child = this.child; + if (child == null) { + return null; + } + final BoxConstraints childConstraints = _getConstraintsForChild(constraints); + final double? result = child.getDryBaseline(childConstraints, baseline); + if (result == null) { + return null; + } + final Size childSize = childConstraints.isTight ? childConstraints.smallest : child.getDryLayout(childConstraints); + return result + _getPositionForChild(_getSize(constraints), childSize).dy; + } + BoxConstraints _getConstraintsForChild(BoxConstraints constraints) { return BoxConstraints( minWidth: constraints.maxWidth, @@ -632,18 +647,21 @@ class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox { @override void performLayout() { size = _getSize(constraints); - if (child != null) { - final BoxConstraints childConstraints = _getConstraintsForChild(constraints); - assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true)); - child!.layout(childConstraints, parentUsesSize: !childConstraints.isTight); - final BoxParentData childParentData = child!.parentData! as BoxParentData; - childParentData.offset = _getPositionForChild(size, childConstraints.isTight ? childConstraints.smallest : child!.size); - final Size childSize = childConstraints.isTight ? childConstraints.smallest : child!.size; + final RenderBox? child = this.child; + if (child == null) { + return; + } - if (_lastSize != childSize) { - _lastSize = childSize; - _onChildSizeChanged.call(_lastSize); - } + final BoxConstraints childConstraints = _getConstraintsForChild(constraints); + assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true)); + child.layout(childConstraints, parentUsesSize: !childConstraints.isTight); + final BoxParentData childParentData = child.parentData! as BoxParentData; + final Size childSize = childConstraints.isTight ? childConstraints.smallest : child.size; + childParentData.offset = _getPositionForChild(size, childSize); + + if (_lastSize != childSize) { + _lastSize = childSize; + _onChildSizeChanged.call(_lastSize); } } } diff --git a/packages/flutter/lib/src/material/button.dart b/packages/flutter/lib/src/material/button.dart index eefabb4918..6f76f336b0 100644 --- a/packages/flutter/lib/src/material/button.dart +++ b/packages/flutter/lib/src/material/button.dart @@ -515,6 +515,20 @@ class _RenderInputPadding extends RenderShiftedBox { ); } + @override + double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { + final RenderBox? child = this.child; + if (child == null) { + return null; + } + final double? result = child.getDryBaseline(constraints, baseline); + if (result == null) { + return null; + } + final Size childSize = child.getDryLayout(constraints); + return result + Alignment.center.alongOffset(getDryLayout(constraints) - childSize as Offset).dy; + } + @override void performLayout() { size = _computeSize( diff --git a/packages/flutter/lib/src/material/button_style_button.dart b/packages/flutter/lib/src/material/button_style_button.dart index 47ee2927c0..b757cc5c4b 100644 --- a/packages/flutter/lib/src/material/button_style_button.dart +++ b/packages/flutter/lib/src/material/button_style_button.dart @@ -604,6 +604,20 @@ class _RenderInputPadding extends RenderShiftedBox { ); } + @override + double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { + final RenderBox? child = this.child; + if (child == null) { + return null; + } + final double? result = child.getDryBaseline(constraints, baseline); + if (result == null) { + return null; + } + final Size childSize = child.getDryLayout(constraints); + return result + Alignment.center.alongOffset(getDryLayout(constraints) - childSize as Offset).dy; + } + @override void performLayout() { size = _computeSize( diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index e260a0c8af..8d49083431 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -1658,9 +1658,9 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip markNeedsLayout(); } - TextDirection? get textDirection => _textDirection; - TextDirection? _textDirection; - set textDirection(TextDirection? value) { + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + set textDirection(TextDirection value) { if (_textDirection == value) { return; } @@ -1855,7 +1855,7 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip tapPosition: position, chipSize: size, deleteButtonSize: deleteIcon!.size, - textDirection: textDirection!, + textDirection: textDirection, ); final RenderBox? hitTestChild = hitIsOnDeleteIcon ? (deleteIcon ?? label ?? avatar) @@ -1932,7 +1932,7 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip Offset centerLayout(Size boxSize, double x) { assert(sizes.content >= boxSize.height); - switch (textDirection!) { + switch (textDirection) { case TextDirection.rtl: x -= boxSize.width; case TextDirection.ltr: @@ -1947,7 +1947,7 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip Offset avatarOffset = Offset.zero; Offset labelOffset = Offset.zero; Offset deleteIconOffset = Offset.zero; - switch (textDirection!) { + switch (textDirection) { case TextDirection.rtl: double start = right; if (theme.showCheckmark || theme.showAvatar) { diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index b9f216c6de..bbf39bc5e7 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -25,6 +25,9 @@ import 'theme_data.dart'; // Examples can assume: // int _act = 1; +typedef _Sizes = ({ double titleY, BoxConstraints textConstraints, Size tileSize }); +typedef _PositionChild = void Function(RenderBox child, Offset offset); + /// Defines the title font used for [ListTile] descendants of a [ListTileTheme]. /// /// List tiles that appear in a [Drawer] use the theme's [TextTheme.bodyLarge] @@ -85,9 +88,12 @@ enum ListTileTitleAlignment { threeLine, /// The tops of the [ListTile.leading] and [ListTile.trailing] widgets are - /// placed 16 units below the top of the [ListTile.title] - /// if the titles' overall height is greater than 72, otherwise they're - /// centered relative to the [ListTile.title] and [ListTile.subtitle] widgets. + /// placed 16 pixels below the top of the [ListTile.title] widget, + /// if the [ListTile]'s overall height is greater than 72, otherwise the + /// [ListTile.trailing] widget is centered relative to the [ListTile.title] and + /// [ListTile.subtitle] widgets, and the [ListTile.leading] widget is 16 pixels + /// below the top of [ListTile.title], or center-aligned with [ListTile.title], + /// whichever makes the [ListTile.leading] closer to the top edge of [ListTile.title]. /// /// This is the default when [ThemeData.useMaterial3] is false. titleHeight, @@ -103,7 +109,31 @@ enum ListTileTitleAlignment { /// The bottoms of the [ListTile.leading] and [ListTile.trailing] widgets are /// placed [ListTile.minVerticalPadding] above the bottom of the [ListTile]'s /// titles. - bottom, + bottom; + + // If isLeading is true the y offset is for the leading widget, otherwise it's + // for the trailing child. + double _yOffsetFor(double childHeight, double tileHeight, _RenderListTile listTile, bool isLeading) { + return switch (this) { + ListTileTitleAlignment.threeLine => listTile.isThreeLine + ? ListTileTitleAlignment.top._yOffsetFor(childHeight, tileHeight, listTile, isLeading) + : ListTileTitleAlignment.center._yOffsetFor(childHeight, tileHeight, listTile, isLeading), + // This attempts to implement the redlines for the vertical position of the + // leading and trailing icons on the spec page: + // https://m2.material.io/components/lists#specs + // + // For large tiles (> 72dp), both leading and trailing controls should be + // a fixed distance from top. As per guidelines this is set to 16dp. + ListTileTitleAlignment.titleHeight when tileHeight > 72.0 => 16.0, + // For smaller tiles, trailing should always be centered. Leading can be + // centered or closer to the top. It should never be further than 16dp + // to the top. + ListTileTitleAlignment.titleHeight => isLeading ? math.min((tileHeight - childHeight) / 2.0, 16.0) : (tileHeight - childHeight) / 2.0, + ListTileTitleAlignment.top => listTile.minVerticalPadding, + ListTileTitleAlignment.center => (tileHeight - childHeight) / 2.0, + ListTileTitleAlignment.bottom => tileHeight - childHeight - listTile.minVerticalPadding, + }; + } } /// A single fixed-height row that typically contains some text as well as @@ -1084,18 +1114,19 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ _titleAlignment = titleAlignment; RenderBox? get leading => childForSlot(_ListTileSlot.leading); - RenderBox? get title => childForSlot(_ListTileSlot.title); + RenderBox get title => childForSlot(_ListTileSlot.title)!; RenderBox? get subtitle => childForSlot(_ListTileSlot.subtitle); RenderBox? get trailing => childForSlot(_ListTileSlot.trailing); // The returned list is ordered for hit testing. @override Iterable get children { + final RenderBox? title = childForSlot(_ListTileSlot.title); return [ if (leading != null) leading!, if (title != null) - title!, + title, if (subtitle != null) subtitle!, if (trailing != null) @@ -1248,26 +1279,23 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ + _maxWidth(trailing, height); } + // The target tile height to use if _minTileHeight is not specified. double get _defaultTileHeight { - final bool hasSubtitle = subtitle != null; - final bool isTwoLine = !isThreeLine && hasSubtitle; - final bool isOneLine = !isThreeLine && !hasSubtitle; - - final Offset baseDensity = visualDensity.baseSizeAdjustment; - if (isOneLine) { - return (isDense ? 48.0 : 56.0) + baseDensity.dy; - } - if (isTwoLine) { - return (isDense ? 64.0 : 72.0) + baseDensity.dy; - } - return (isDense ? 76.0 : 88.0) + baseDensity.dy; + final Offset baseDensity = visualDensity.baseSizeAdjustment; + return baseDensity.dy + switch ((isThreeLine, subtitle != null)) { + (true, _) => isDense ? 76.0 : 88.0, // 3 lines, + (false, true) => isDense ? 64.0 : 72.0, // 2 lines + (false, false) => isDense ? 48.0 : 56.0, // 1 line, + }; } + double get _targetTileHeight => _minTileHeight ?? _defaultTileHeight; + @override double computeMinIntrinsicHeight(double width) { return math.max( - minTileHeight ?? _defaultTileHeight, - title!.getMinIntrinsicHeight(width) + (subtitle?.getMinIntrinsicHeight(width) ?? 0.0), + _targetTileHeight, + title.getMinIntrinsicHeight(width) + (subtitle?.getMinIntrinsicHeight(width) ?? 0.0), ); } @@ -1278,187 +1306,166 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ @override double? computeDistanceToActualBaseline(TextBaseline baseline) { - assert(title != null); - final BoxParentData parentData = title!.parentData! as BoxParentData; - final BaselineOffset offset = BaselineOffset(title!.getDistanceToActualBaseline(baseline)) + final BoxParentData parentData = title.parentData! as BoxParentData; + final BaselineOffset offset = BaselineOffset(title.getDistanceToActualBaseline(baseline)) + parentData.offset.dy; return offset.offset; } - static double? _boxBaseline(RenderBox box, TextBaseline baseline) { - return box.getDistanceToBaseline(baseline); - } - - static Size _layoutBox(RenderBox? box, BoxConstraints constraints) { - if (box == null) { - return Size.zero; - } - box.layout(constraints, parentUsesSize: true); - return box.size; - } + BoxConstraints get maxIconHeightConstraint => BoxConstraints( + // One-line trailing and leading widget heights do not follow + // Material specifications, but this sizing is required to adhere + // to accessibility requirements for smallest tappable widget. + // Two- and three-line trailing widget heights are constrained + // properly according to the Material spec. + maxHeight: (isDense ? 48.0 : 56.0) + visualDensity.baseSizeAdjustment.dy, + ); static void _positionBox(RenderBox box, Offset offset) { final BoxParentData parentData = box.parentData! as BoxParentData; parentData.offset = offset; } - @override - Size computeDryLayout(BoxConstraints constraints) { - assert(debugCannotComputeDryLayout( - reason: 'Layout requires baseline metrics, which are only available after a full layout.', - )); - return Size.zero; - } - + // Implements _RenderListTile's layout algorithm. If `positionChild` is not null, + // it will be called on each child with that child's layout offset. + // // All of the dimensions below were taken from the Material Design spec: // https://material.io/design/components/lists.html#specs - @override - void performLayout() { - final BoxConstraints constraints = this.constraints; - final bool hasLeading = leading != null; - final bool hasSubtitle = subtitle != null; - final bool hasTrailing = trailing != null; - final bool isTwoLine = !isThreeLine && hasSubtitle; - final bool isOneLine = !isThreeLine && !hasSubtitle; - final Offset densityAdjustment = visualDensity.baseSizeAdjustment; - - final BoxConstraints maxIconHeightConstraint = BoxConstraints( - // One-line trailing and leading widget heights do not follow - // Material specifications, but this sizing is required to adhere - // to accessibility requirements for smallest tappable widget. - // Two- and three-line trailing widget heights are constrained - // properly according to the Material spec. - maxHeight: (isDense ? 48.0 : 56.0) + densityAdjustment.dy, - ); + _Sizes _computeSizes( + ChildBaselineGetter getBaseline, + ChildLayouter getSize, + BoxConstraints constraints, { + _PositionChild? positionChild, + }) { final BoxConstraints looseConstraints = constraints.loosen(); - final BoxConstraints iconConstraints = looseConstraints.enforce(maxIconHeightConstraint); - final double tileWidth = looseConstraints.maxWidth; - final Size leadingSize = _layoutBox(leading, iconConstraints); - final Size trailingSize = _layoutBox(trailing, iconConstraints); + final BoxConstraints iconConstraints = looseConstraints.enforce(maxIconHeightConstraint); + final RenderBox? leading = this.leading; + final RenderBox? trailing = this.trailing; + + final Size? leadingSize = leading == null ? null : getSize(leading, iconConstraints); + final Size? trailingSize = trailing == null ? null : getSize(trailing, iconConstraints); + assert( - tileWidth != leadingSize.width || tileWidth == 0.0, + tileWidth != leadingSize?.width || tileWidth == 0.0, 'Leading widget consumes entire tile width. Please use a sized widget, ' 'or consider replacing ListTile with a custom widget ' '(see https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4)', ); assert( - tileWidth != trailingSize.width || tileWidth == 0.0, + tileWidth != trailingSize?.width || tileWidth == 0.0, 'Trailing widget consumes entire tile width. Please use a sized widget, ' 'or consider replacing ListTile with a custom widget ' '(see https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4)', ); - final double titleStart = hasLeading - ? math.max(_minLeadingWidth, leadingSize.width) + _effectiveHorizontalTitleGap - : 0.0; - final double adjustedTrailingWidth = hasTrailing - ? math.max(trailingSize.width + _effectiveHorizontalTitleGap, 32.0) - : 0.0; + final double titleStart = leadingSize == null + ? 0.0 + : math.max(_minLeadingWidth, leadingSize.width) + _effectiveHorizontalTitleGap; + + final double adjustedTrailingWidth = trailingSize == null + ? 0.0 + : math.max(trailingSize.width + _effectiveHorizontalTitleGap, 32.0); + final BoxConstraints textConstraints = looseConstraints.tighten( width: tileWidth - titleStart - adjustedTrailingWidth, ); - final Size titleSize = _layoutBox(title, textConstraints); - final Size subtitleSize = _layoutBox(subtitle, textConstraints); - double? titleBaseline; - double? subtitleBaseline; - if (isTwoLine) { - titleBaseline = isDense ? 28.0 : 32.0; - subtitleBaseline = isDense ? 48.0 : 52.0; - } else if (isThreeLine) { - titleBaseline = isDense ? 22.0 : 28.0; - subtitleBaseline = isDense ? 42.0 : 48.0; - } else { - assert(isOneLine); - } + final RenderBox? subtitle = this.subtitle; + final double titleHeight = getSize(title, textConstraints).height; - double tileHeight; - double titleY; - double? subtitleY; - if (!hasSubtitle) { - tileHeight = math.max(minTileHeight ?? _defaultTileHeight, titleSize.height + 2.0 * _minVerticalPadding); - titleY = (tileHeight - titleSize.height) / 2.0; - } else { - assert(subtitleBaselineType != null); - titleY = titleBaseline! - _boxBaseline(title!, titleBaselineType)!; - subtitleY = subtitleBaseline! - _boxBaseline(subtitle!, subtitleBaselineType!)! + visualDensity.vertical * 2.0; - tileHeight = minTileHeight ?? _defaultTileHeight; - - // If the title and subtitle overlap, move the title upwards by half - // the overlap and the subtitle down by the same amount, and adjust - // tileHeight so that both titles fit. - final double titleOverlap = titleY + titleSize.height - subtitleY; - if (titleOverlap > 0.0) { - titleY -= titleOverlap / 2.0; - subtitleY += titleOverlap / 2.0; - } - - // If the title or subtitle overflow tileHeight then punt: title - // and subtitle are arranged in a column, tileHeight = column height plus - // _minVerticalPadding on top and bottom. - if (titleY < _minVerticalPadding || - (subtitleY + subtitleSize.height + _minVerticalPadding) > tileHeight) { - tileHeight = titleSize.height + subtitleSize.height + 2.0 * _minVerticalPadding; - titleY = _minVerticalPadding; - subtitleY = titleSize.height + _minVerticalPadding; - } - } - - final double leadingDiff = tileHeight - leadingSize.height; - final double trailingDiff = tileHeight - trailingSize.height; - - final (double leadingY, double trailingY) = switch (titleAlignment) { - ListTileTitleAlignment.threeLine when isThreeLine => (_minVerticalPadding, _minVerticalPadding), - ListTileTitleAlignment.threeLine => (leadingDiff / 2.0, trailingDiff / 2.0), - // This attempts to implement the redlines for the vertical position of the - // leading and trailing icons on the spec page: - // https://m2.material.io/components/lists#specs - // - // For large tiles (> 72dp), both leading and trailing controls should be - // a fixed distance from top. As per guidelines this is set to 16dp. - ListTileTitleAlignment.titleHeight when tileHeight > 72.0 => (16.0, 16.0), - // For smaller tiles, trailing should always be centered. Leading can be - // centered or closer to the top. It should never be further than 16dp - // to the top. - ListTileTitleAlignment.titleHeight => (math.min(leadingDiff / 2.0, 16.0), trailingDiff / 2.0), - ListTileTitleAlignment.top => (_minVerticalPadding, _minVerticalPadding), - ListTileTitleAlignment.center => (leadingDiff / 2.0, trailingDiff / 2.0), - ListTileTitleAlignment.bottom => (leadingDiff - _minVerticalPadding, trailingDiff - _minVerticalPadding), + final bool isLTR = switch (textDirection) { + TextDirection.ltr => true, + TextDirection.rtl => false, }; - switch (textDirection) { - case TextDirection.rtl: { - if (hasLeading) { - _positionBox(leading!, Offset(tileWidth - leadingSize.width, leadingY)); - } - _positionBox(title!, Offset(adjustedTrailingWidth, titleY)); - if (hasSubtitle) { - _positionBox(subtitle!, Offset(adjustedTrailingWidth, subtitleY!)); - } - if (hasTrailing) { - _positionBox(trailing!, Offset(0.0, trailingY)); - } - break; + final double titleY; + final double tileHeight; + if (subtitle == null) { + tileHeight = math.max(_targetTileHeight, titleHeight + 2.0 * _minVerticalPadding); + titleY = (tileHeight - titleHeight) / 2.0; + } else { + final double subtitleHeight = getSize(subtitle, textConstraints).height; + final double titleBaseline = getBaseline(title, textConstraints, titleBaselineType) ?? titleHeight; + final double subtitleBaseline = getBaseline(subtitle, textConstraints, subtitleBaselineType!) ?? subtitleHeight; + + final double targetTitleY = (isThreeLine ? (isDense ? 22.0 : 28.0) : (isDense ? 28.0 : 32.0)) - titleBaseline; + final double targetSubtitleY = (isThreeLine ? (isDense ? 42.0 : 48.0) : (isDense ? 48.0 : 52.0)) + visualDensity.vertical * 2.0 - subtitleBaseline; + // Prevent the title and the subtitle from overlapping by moving them away from + // each other by the same distance. + final double halfOverlap = math.max(targetTitleY + titleHeight - targetSubtitleY, 0) / 2; + final double idealTitleY = targetTitleY - halfOverlap; + final double idealSubtitleY = targetSubtitleY + halfOverlap; + // However if either component can't maintain the minimal padding from the top/bottom edges, the ListTile enters "compat mode". + final bool compact = idealTitleY < minVerticalPadding || idealSubtitleY + subtitleHeight + minVerticalPadding > _targetTileHeight; + + // Position subtitle. + positionChild?.call(subtitle, Offset( + isLTR ? titleStart : adjustedTrailingWidth, + compact ? minVerticalPadding + titleHeight : idealSubtitleY, + )); + tileHeight = compact ? 2 * _minVerticalPadding + titleHeight + subtitleHeight : _targetTileHeight; + titleY = compact ? minVerticalPadding : idealTitleY; + } + + if (positionChild != null) { + positionChild(title, Offset( + isLTR ? titleStart : adjustedTrailingWidth, + titleY, + )); + + if (leading != null && leadingSize != null) { + positionChild(leading, Offset( + isLTR ? 0.0 : tileWidth - leadingSize.width, + titleAlignment._yOffsetFor(leadingSize.height, tileHeight, this, true), + )); } - case TextDirection.ltr: { - if (hasLeading) { - _positionBox(leading!, Offset(0.0, leadingY)); - } - _positionBox(title!, Offset(titleStart, titleY)); - if (hasSubtitle) { - _positionBox(subtitle!, Offset(titleStart, subtitleY!)); - } - if (hasTrailing) { - _positionBox(trailing!, Offset(tileWidth - trailingSize.width, trailingY)); - } - break; + + if (trailing != null && trailingSize != null) { + positionChild(trailing, Offset( + isLTR ? tileWidth - trailingSize.width : 0.0, + titleAlignment._yOffsetFor(trailingSize.height, tileHeight, this, false), + )); } } - size = constraints.constrain(Size(tileWidth, tileHeight)); - assert(size.width == constraints.constrainWidth(tileWidth)); - assert(size.height == constraints.constrainHeight(tileHeight)); + return (titleY: titleY, textConstraints: textConstraints, tileSize: Size(tileWidth, tileHeight)); + } + + @override + double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { + final _Sizes sizes = _computeSizes( + ChildLayoutHelper.getDryBaseline, + ChildLayoutHelper.dryLayoutChild, + constraints, + ); + final BaselineOffset titleBaseline = BaselineOffset(title.getDryBaseline(sizes.textConstraints, baseline)) + sizes.titleY; + return titleBaseline.offset; + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + return constraints.constrain( + _computeSizes( + ChildLayoutHelper.getDryBaseline, + ChildLayoutHelper.dryLayoutChild, + constraints, + ).tileSize, + ); + } + + @override + void performLayout() { + final Size tileSize = _computeSizes( + ChildLayoutHelper.getBaseline, + ChildLayoutHelper.layoutChild, + constraints, + positionChild: _positionBox, + ).tileSize; + + size = constraints.constrain(tileSize); + assert(size.width == constraints.constrainWidth(tileSize.width)); + assert(size.height == constraints.constrainHeight(tileSize.height)); } @override diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index fa73e2e233..b10bda281c 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -159,6 +159,11 @@ class _RenderMenuItem extends RenderShiftedBox { return child?.getDryLayout(constraints) ?? Size.zero; } + @override + double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { + return child?.getDryBaseline(constraints, baseline); + } + @override void performLayout() { if (child == null) { diff --git a/packages/flutter/lib/src/material/segmented_button.dart b/packages/flutter/lib/src/material/segmented_button.dart index 540513e82c..8eacecd593 100644 --- a/packages/flutter/lib/src/material/segmented_button.dart +++ b/packages/flutter/lib/src/material/segmented_button.dart @@ -796,6 +796,18 @@ class _RenderSegmentedButton extends RenderBox with return _computeOverallSizeFromChildSize(childSize); } + @override + double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { + final Size childSize = _calculateChildSize(constraints); + final BoxConstraints childConstraints = BoxConstraints.tight(childSize); + + BaselineOffset baselineOffset = BaselineOffset.noBaseline; + for (RenderBox? child = firstChild; child != null; child = childAfter(child)) { + baselineOffset = baselineOffset.minOf(BaselineOffset(child.getDryBaseline(childConstraints, baseline))); + } + return baselineOffset.offset; + } + @override void performLayout() { final BoxConstraints constraints = this.constraints; @@ -851,9 +863,9 @@ class _RenderSegmentedButton extends RenderBox with context.canvas.restore(); // Compute a clip rect for the outer border of the child. - late final double segmentLeft; - late final double segmentRight; - late final double dividerPos; + final double segmentLeft; + final double segmentRight; + final double dividerPos; final double borderOutset = math.max(enabledBorder.side.strokeOutset, disabledBorder.side.strokeOutset); switch (textDirection) { case TextDirection.rtl: diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index 1fe8d502c4..da2d5f0525 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -821,6 +821,20 @@ class _RenderInputPadding extends RenderShiftedBox { ); } + @override + double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { + final RenderBox? child = this.child; + if (child == null) { + return null; + } + final double? result = child.getDryBaseline(constraints, baseline); + if (result == null) { + return null; + } + final Size childSize = child.getDryLayout(constraints); + return result + Alignment.center.alongOffset(getDryLayout(constraints) - childSize as Offset).dy; + } + @override void performLayout() { size = _computeSize( diff --git a/packages/flutter/lib/src/material/toggle_buttons.dart b/packages/flutter/lib/src/material/toggle_buttons.dart index 2525693cfa..557612905c 100644 --- a/packages/flutter/lib/src/material/toggle_buttons.dart +++ b/packages/flutter/lib/src/material/toggle_buttons.dart @@ -1159,19 +1159,19 @@ class _SelectToggleButtonRenderObject extends RenderShiftedBox { } static double _maxHeight(RenderBox? box, double width) { - return box == null ? 0.0 : box.getMaxIntrinsicHeight(width); + return box?.getMaxIntrinsicHeight(width) ?? 0.0; } static double _minHeight(RenderBox? box, double width) { - return box == null ? 0.0 : box.getMinIntrinsicHeight(width); + return box?.getMinIntrinsicHeight(width) ?? 0.0; } static double _minWidth(RenderBox? box, double height) { - return box == null ? 0.0 : box.getMinIntrinsicWidth(height); + return box?.getMinIntrinsicWidth(height) ?? 0.0; } static double _maxWidth(RenderBox? box, double height) { - return box == null ? 0.0 : box.getMaxIntrinsicWidth(height); + return box?.getMaxIntrinsicWidth(height) ?? 0.0; } @override @@ -1220,6 +1220,42 @@ class _SelectToggleButtonRenderObject extends RenderShiftedBox { ); } + EdgeInsetsDirectional get _childPadding { + assert(child != null); + // It does not matter what [textDirection] or [verticalDirection] is, + // since deflating the size constraints horizontally/vertically + // and the returned size accounts for the width of both sides. + return switch (direction) { + Axis.horizontal => EdgeInsetsDirectional.only( + start: leadingBorderSide.width, + end: trailingBorderSide.width, + top: borderSide.width, + bottom: borderSide.width, + ), + Axis.vertical => EdgeInsetsDirectional.only( + start: borderSide.width, + end: borderSide.width, + top: leadingBorderSide.width, + bottom: trailingBorderSide.width, + ), + }; + } + + @override + double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) { + final double? childBaseline = child?.getDryBaseline(constraints.deflate(_childPadding), baseline); + if (childBaseline == null) { + return null; + } + return childBaseline + switch (direction) { + Axis.horizontal => borderSide.width, + Axis.vertical => switch (verticalDirection) { + VerticalDirection.down => leadingBorderSide.width, + VerticalDirection.up => trailingBorderSide.width, + }, + }; + } + @override void performLayout() { size = _computeSize( @@ -1244,53 +1280,18 @@ class _SelectToggleButtonRenderObject extends RenderShiftedBox { } Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild}) { + final RenderBox? child = this.child; if (child == null) { - if (direction == Axis.horizontal) { - return constraints.constrain(Size( - leadingBorderSide.width + trailingBorderSide.width, - borderSide.width * 2.0, - )); - } else { - return constraints.constrain(Size( - borderSide.width * 2.0, - leadingBorderSide.width + trailingBorderSide.width, - )); - } + final Size horizontalSize = Size(leadingBorderSide.width + trailingBorderSide.width, borderSide.width * 2.0); + return switch (direction) { + Axis.horizontal => constraints.constrain(horizontalSize), + Axis.vertical => constraints.constrain(horizontalSize.flipped), + }; } - final double leftConstraint; - final double rightConstraint; - final double topConstraint; - final double bottomConstraint; - - // It does not matter what [textDirection] or [verticalDirection] is, - // since deflating the size constraints horizontally/vertically - // and the returned size accounts for the width of both sides. - if (direction == Axis.horizontal) { - rightConstraint = trailingBorderSide.width; - leftConstraint = leadingBorderSide.width; - topConstraint = borderSide.width; - bottomConstraint = borderSide.width; - } else { - rightConstraint = borderSide.width; - leftConstraint = borderSide.width; - topConstraint = leadingBorderSide.width; - bottomConstraint = trailingBorderSide.width; - } - final BoxConstraints innerConstraints = constraints.deflate( - EdgeInsets.only( - left: leftConstraint, - top: topConstraint, - right: rightConstraint, - bottom: bottomConstraint, - ), - ); - final Size childSize = layoutChild(child!, innerConstraints); - - return constraints.constrain(Size( - leftConstraint + childSize.width + rightConstraint, - topConstraint + childSize.height + bottomConstraint, - )); + final EdgeInsetsDirectional childPadding = _childPadding; + final BoxConstraints innerConstraints = constraints.deflate(childPadding); + return constraints.constrain(childPadding.inflateSize(layoutChild(child, innerConstraints))); } @override @@ -1621,6 +1622,20 @@ class _RenderInputPadding extends RenderShiftedBox { ); } + @override + double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { + final RenderBox? child = this.child; + if (child == null) { + return null; + } + final double? result = child.getDryBaseline(constraints, baseline); + if (result == null) { + return null; + } + final Size childSize = child.getDryLayout(constraints); + return result + Alignment.center.alongOffset(getDryLayout(constraints) - childSize as Offset).dy; + } + @override void performLayout() { size = _computeSize( diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart index a409c6f039..659eb17ab1 100644 --- a/packages/flutter/lib/src/rendering/shifted_box.dart +++ b/packages/flutter/lib/src/rendering/shifted_box.dart @@ -54,8 +54,9 @@ abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixi double? computeDistanceToActualBaseline(TextBaseline baseline) { double? result; final RenderBox? child = this.child; + assert(!debugNeedsLayout); if (child != null) { - assert(!debugNeedsLayout); + assert(!child.debugNeedsLayout); result = child.getDistanceToActualBaseline(baseline); final BoxParentData childParentData = child.parentData! as BoxParentData; if (result != null) { @@ -279,15 +280,17 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { _textDirection = textDirection, super(child); + /// The [Alignment] to use for aligning the child. + /// + /// This is the [alignment] resolved against [textDirection]. Subclasses should + /// use [resolvedAlignment] instead of [alignment] directly, for computing the + /// child's offset. + /// + /// The [performLayout] method will be called when the value changes. + @protected + Alignment get resolvedAlignment => _resolvedAlignment ??= alignment.resolve(textDirection); Alignment? _resolvedAlignment; - void _resolve() { - if (_resolvedAlignment != null) { - return; - } - _resolvedAlignment = alignment.resolve(textDirection); - } - void _markNeedResolution() { _resolvedAlignment = null; markNeedsLayout(); @@ -340,14 +343,12 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { /// this object's own size has been set. @protected void alignChild() { - _resolve(); assert(child != null); assert(!child!.debugNeedsLayout); assert(child!.hasSize); assert(hasSize); - assert(_resolvedAlignment != null); final BoxParentData childParentData = child!.parentData! as BoxParentData; - childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset); + childParentData.offset = resolvedAlignment.alongOffset(size - child!.size as Offset); } @override