From 9e082f6ce9c8ed17cad783104adb15edef513021 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Tue, 22 Jun 2021 17:01:23 -0700 Subject: [PATCH] Add OverflowBar.alignment property (#85050) --- .../flutter/lib/src/widgets/overflow_bar.dart | 90 +++++++++++++++++-- .../test/widgets/overflow_bar_test.dart | 70 +++++++++++++++ 2 files changed, 154 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/widgets/overflow_bar.dart b/packages/flutter/lib/src/widgets/overflow_bar.dart index acf6f912d9a..e76e6d4f13d 100644 --- a/packages/flutter/lib/src/widgets/overflow_bar.dart +++ b/packages/flutter/lib/src/widgets/overflow_bar.dart @@ -103,6 +103,7 @@ class OverflowBar extends MultiChildRenderObjectWidget { OverflowBar({ Key? key, this.spacing = 0.0, + this.alignment, this.overflowSpacing = 0.0, this.overflowAlignment = OverflowBarAlignment.start, this.overflowDirection = VerticalDirection.down, @@ -125,6 +126,33 @@ class OverflowBar extends MultiChildRenderObjectWidget { /// Defaults to 0.0. final double spacing; + /// Defines the [children]'s horizontal layout according to the same + /// rules as for [Row.mainAxisAlignment]. + /// + /// If this property is non-null, and the [children], separated by + /// [spacing], fit within the available width, then the overflow + /// bar will be as wide as possible. If the children do not fit + /// within the available width, then this property is ignored and + /// [overflowAlignment] applies instead. + /// + /// If this property is null (the default) then the overflow bar + /// will be no wider than needed to layout the [children] separated + /// by [spacing], modulo the incoming constraints. + /// + /// If [alignment] is one of [MainAxisAlignment.spaceAround], + /// [MainAxisAlignment.spaceBetween], or + /// [MainAxisAlignment.spaceEvenly], then the [spacing] parameter is + /// only used to see if the horizontal layout will overflow. + /// + /// Defaults to null. + /// + /// See also: + /// + /// * [overflowAlignment], the horizontal alignment of the [children] within + /// the vertical "overflow" layout. + /// + final MainAxisAlignment? alignment; + /// The height of the gap between [children] in the vertical /// "overflow" layout. /// @@ -163,6 +191,9 @@ class OverflowBar extends MultiChildRenderObjectWidget { /// /// See also: /// + /// * [alignment], which defines the [children]'s horizontal layout + /// (according to the same rules as for [Row.mainAxisAlignment]) when + /// the children, separated by [spacing], fit within the available space. /// * [overflowDirection], which defines the order that the /// [OverflowBar]'s children appear in, if the horizontal layout /// overflows. @@ -221,6 +252,7 @@ class OverflowBar extends MultiChildRenderObjectWidget { RenderObject createRenderObject(BuildContext context) { return _RenderOverflowBar( spacing: spacing, + alignment: alignment, overflowSpacing: overflowSpacing, overflowAlignment: overflowAlignment, overflowDirection: overflowDirection, @@ -233,6 +265,7 @@ class OverflowBar extends MultiChildRenderObjectWidget { void updateRenderObject(BuildContext context, RenderObject renderObject) { (renderObject as _RenderOverflowBar) ..spacing = spacing + ..alignment = alignment ..overflowSpacing = overflowSpacing ..overflowAlignment = overflowAlignment ..overflowDirection = overflowDirection @@ -244,6 +277,7 @@ class OverflowBar extends MultiChildRenderObjectWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DoubleProperty('spacing', spacing, defaultValue: 0)); + properties.add(EnumProperty('alignment', alignment, defaultValue: null)); properties.add(DoubleProperty('overflowSpacing', overflowSpacing, defaultValue: 0)); properties.add(EnumProperty('overflowAlignment', overflowAlignment, defaultValue: OverflowBarAlignment.start)); properties.add(EnumProperty('overflowDirection', overflowDirection, defaultValue: VerticalDirection.down)); @@ -259,6 +293,7 @@ class _RenderOverflowBar extends RenderBox _RenderOverflowBar({ List? children, double spacing = 0.0, + MainAxisAlignment? alignment, double overflowSpacing = 0.0, OverflowBarAlignment overflowAlignment = OverflowBarAlignment.start, VerticalDirection overflowDirection = VerticalDirection.down, @@ -270,6 +305,7 @@ class _RenderOverflowBar extends RenderBox assert(textDirection != null), assert(clipBehavior != null), _spacing = spacing, + _alignment = alignment, _overflowSpacing = overflowSpacing, _overflowAlignment = overflowAlignment, _overflowDirection = overflowDirection, @@ -288,6 +324,15 @@ class _RenderOverflowBar extends RenderBox markNeedsLayout(); } + MainAxisAlignment? get alignment => _alignment; + MainAxisAlignment? _alignment; + set alignment (MainAxisAlignment? value) { + if (_alignment == value) + return; + _alignment = value; + markNeedsLayout(); + } + double get overflowSpacing => _overflowSpacing; double _overflowSpacing; set overflowSpacing (double value) { @@ -456,7 +501,8 @@ class _RenderOverflowBar extends RenderBox if (actualWidth > constraints.maxWidth) { return constraints.constrain(Size(constraints.maxWidth, y - overflowSpacing)); } else { - return constraints.constrain(Size(actualWidth, maxChildHeight)); + final double overallWidth = alignment == null ? actualWidth : constraints.maxWidth; + return constraints.constrain(Size(overallWidth, maxChildHeight)); } } @@ -510,10 +556,42 @@ class _RenderOverflowBar extends RenderBox } size = constraints.constrain(Size(constraints.maxWidth, y - overflowSpacing)); } else { - // Default horizontal layout. - size = constraints.constrain(Size(actualWidth, maxChildHeight)); + // Default horizontal layout child = firstChild; - double x = rtl ? size.width - child!.size.width : 0; + final double firstChildWidth = child!.size.width; + final double overallWidth = alignment == null ? actualWidth : constraints.maxWidth; + size = constraints.constrain(Size(overallWidth, maxChildHeight)); + + late double x; // initial value: origin of the first child + double layoutSpacing = spacing; // space between children + switch (alignment) { + case null: + x = rtl ? size.width - firstChildWidth : 0; + break; + case MainAxisAlignment.start: + x = rtl ? size.width - firstChildWidth : 0; + break; + case MainAxisAlignment.center: + final double halfRemainingWidth = (size.width - actualWidth) / 2; + x = rtl ? size.width - halfRemainingWidth - firstChildWidth : halfRemainingWidth; + break; + case MainAxisAlignment.end: + x = rtl ? actualWidth - firstChildWidth : size.width - actualWidth; + break; + case MainAxisAlignment.spaceBetween: + layoutSpacing = (size.width - childrenWidth) / (childCount - 1); + x = rtl ? size.width - firstChildWidth : 0; + break; + case MainAxisAlignment.spaceAround: + layoutSpacing = childCount > 0 ? (size.width - childrenWidth) / childCount : 0; + x = rtl ? size.width - layoutSpacing / 2 - firstChildWidth : layoutSpacing / 2; + break; + case MainAxisAlignment.spaceEvenly: + layoutSpacing = (size.width - childrenWidth) / (childCount + 1); + x = rtl ? size.width - layoutSpacing - firstChildWidth : layoutSpacing; + break; + } + while (child != null) { final _OverflowBarParentData childParentData = child.parentData! as _OverflowBarParentData; childParentData.offset = Offset(x, (maxChildHeight - child.size.height) / 2); @@ -522,11 +600,11 @@ class _RenderOverflowBar extends RenderBox // the origin of the next child for RTL: subtract the width of the next // child (if there is one). if (!rtl) { - x += child.size.width + spacing; + x += child.size.width + layoutSpacing; } child = childAfter(child); if (rtl && child != null) { - x -= child.size.width + spacing; + x -= child.size.width + layoutSpacing; } } } diff --git a/packages/flutter/test/widgets/overflow_bar_test.dart b/packages/flutter/test/widgets/overflow_bar_test.dart index 77bcaead21e..31ffbae12a6 100644 --- a/packages/flutter/test/widgets/overflow_bar_test.dart +++ b/packages/flutter/test/widgets/overflow_bar_test.dart @@ -9,6 +9,7 @@ void main() { testWidgets('OverflowBar documented defaults', (WidgetTester tester) async { final OverflowBar bar = OverflowBar(); expect(bar.spacing, 0); + expect(bar.alignment, null); expect(bar.overflowSpacing, 0); expect(bar.overflowDirection, VerticalDirection.down); expect(bar.textDirection, null); @@ -271,4 +272,73 @@ void main() { expect(tester.getTopLeft(find.byKey(key1)).dx, 680); expect(tester.getTopLeft(find.byKey(key2)).dx, 600); }); + + testWidgets('OverflowBar with alignment should match Row with mainAxisAlignment', (WidgetTester tester) async { + final Key key0 = UniqueKey(); + final Key key1 = UniqueKey(); + final Key key2 = UniqueKey(); + + // This list of children appears in a Row and an OverflowBar, so each + // find.byKey() for key0, key1, key2 returns two widgets. + final List children = [ + SizedBox(key: key0, width: 50, height: 50), + SizedBox(key: key1, width: 70, height: 50), + SizedBox(key: key2, width: 80, height: 50), + ]; + + const List allAlignments = [ + MainAxisAlignment.start, + MainAxisAlignment.center, + MainAxisAlignment.end, + MainAxisAlignment.spaceBetween, + MainAxisAlignment.spaceAround, + MainAxisAlignment.spaceEvenly, + ]; + + const List allTextDirections = [ + TextDirection.ltr, + TextDirection.rtl, + ]; + + Widget buildFrame(MainAxisAlignment alignment, TextDirection textDirection) { + return Directionality( + textDirection: textDirection, + child: Column( + children: [ + OverflowBar( + alignment: alignment, + children: children, + ), + Row( + mainAxisAlignment: alignment, + children: children, + ), + ], + ), + ); + } + + // Each key from key0, key1, key2 maps to one child in the OverflowBar + // and a matching child in the Row. We expect the children to be the + // same size and for their left and right edges to align. + void testLayout() { + expect(tester.getSize(find.byType(OverflowBar)), const Size(800, 50)); + for (final Key key in [key0, key1, key2]) { + final Finder matchingChildren = find.byKey(key); + expect(matchingChildren.evaluate().length, 2); + final Rect rect0 = tester.getRect(matchingChildren.first); + final Rect rect1 = tester.getRect(matchingChildren.last); + expect(rect0.size, rect1.size); + expect(rect0.left, rect1.left); + expect(rect0.right, rect1.right); + } + } + + for (final MainAxisAlignment alignment in allAlignments) { + for (final TextDirection textDirection in allTextDirections) { + await tester.pumpWidget(buildFrame(alignment, textDirection)); + testLayout(); + } + } + }); }