From d2f07fb4b87e5121f487f42b587e259f7f372f4f Mon Sep 17 00:00:00 2001 From: thanhle1547 Date: Mon, 13 Jun 2022 17:10:26 +0700 Subject: [PATCH 01/10] update pubspec.lock files --- example/pubspec.lock | 38 +++++++++++++++++++------------------- pubspec.lock | 38 +++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 82a8f5e..4842ad1 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,14 +21,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -42,7 +42,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" cupertino_icons: dependency: "direct main" description: @@ -56,7 +56,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -73,14 +73,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" overflow_view: dependency: "direct main" description: @@ -94,7 +101,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" sky_engine: dependency: transitive description: flutter @@ -106,7 +113,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -141,14 +148,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" + version: "0.4.9" value_layout_builder: dependency: transitive description: @@ -162,7 +162,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=1.24.0-10.2.pre" diff --git a/pubspec.lock b/pubspec.lock index ae81523..77efde6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,14 +21,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -42,14 +42,14 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -66,21 +66,28 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" sky_engine: dependency: transitive description: flutter @@ -92,7 +99,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -127,14 +134,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" + version: "0.4.9" value_layout_builder: dependency: "direct main" description: @@ -148,7 +148,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=1.24.0-10.2.pre" From b594c0fbe12bbff8533cdce23d04ad89451b28e4 Mon Sep 17 00:00:00 2001 From: thanhle1547 Date: Sat, 18 Jun 2022 13:54:56 +0700 Subject: [PATCH 02/10] Implement wrapped items --- example/lib/main.dart | 157 ++-- lib/src/rendering/overflow_view.dart | 1069 ++++++++++++++++++++++++-- lib/src/widgets/overflow_view.dart | 252 +++++- 3 files changed, 1360 insertions(+), 118 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index dc1d991..0dff1fe 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -98,66 +98,115 @@ class _MyHomePageState extends State { ), SizedBox(height: 20), Expanded( - child: OverflowView( - direction: Axis.vertical, - spacing: 4, - children: [ - for (int i = 0; i < _counter; i++) - AvatarWidget( - text: avatars[i].initials, - color: avatars[i].color, - ) - ], - builder: (context, remaining) { - return SizedBox( - height: 80, - width: 80, - child: Stack( - fit: StackFit.expand, - children: [ - if (remaining > 0) - AvatarOverview( - position: 0, - remaining: remaining, - counter: _counter, - ), - if (remaining > 1) - AvatarOverview( - position: 1, - remaining: remaining, - counter: _counter, - ), - if (remaining > 2) - AvatarOverview( - position: 2, - remaining: remaining, - counter: _counter, - ), - if (remaining > 3) - AvatarOverview( - position: 3, - remaining: remaining, - counter: _counter, - ), - Positioned.fill( - child: Center( - child: FractionallySizedBox( - alignment: Alignment.center, - widthFactor: 0.5, - heightFactor: 0.5, - child: FittedBox( - child: AvatarWidget( - text: '+$remaining', - color: Colors.black.withOpacity(0.9), + child: Row( + children: [ + OverflowView( + direction: Axis.vertical, + spacing: 4, + children: [ + for (int i = 0; i < _counter; i++) + AvatarWidget( + text: avatars[i].initials, + color: avatars[i].color, + ) + ], + builder: (context, remaining) { + return SizedBox( + height: 80, + width: 80, + child: Stack( + fit: StackFit.expand, + children: [ + if (remaining > 0) + AvatarOverview( + position: 0, + remaining: remaining, + counter: _counter, + ), + if (remaining > 1) + AvatarOverview( + position: 1, + remaining: remaining, + counter: _counter, + ), + if (remaining > 2) + AvatarOverview( + position: 2, + remaining: remaining, + counter: _counter, + ), + if (remaining > 3) + AvatarOverview( + position: 3, + remaining: remaining, + counter: _counter, + ), + Positioned.fill( + child: Center( + child: FractionallySizedBox( + alignment: Alignment.center, + widthFactor: 0.5, + heightFactor: 0.5, + child: FittedBox( + child: AvatarWidget( + text: '+$remaining', + color: Colors.black.withOpacity(0.9), + ), + ), ), ), ), + ], + ), + ); + }, + ), + SizedBox(width: 40), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "People's names", + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 16), + Container( + width: 190, + height: 80, + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + border: Border.all( + color: Colors.blue.shade100, + width: 2, + ), + borderRadius: BorderRadius.circular(18), + ), + child: OverflowView.wrap( + maxRun: 3, + builder: (context, remainingItemCount) => Chip( + label: Text("+$remainingItemCount"), + backgroundColor: Colors.red, + ), + children: [ + for (int i = 0; i < _counter; i++) + Chip( + label: Text( + avatars[i].initials, + style: TextStyle( + color: Colors.white, + ), + ), + backgroundColor: avatars[i].color, + ), + ], ), ), ], ), - ); - }, + ) + ], ), ), // Slider( @@ -231,7 +280,7 @@ class AvatarOverview extends StatelessWidget { class AvatarWidget extends StatelessWidget { const AvatarWidget({ - Key? key, + Key? key, required this.text, required this.color, }) : super(key: key); diff --git a/lib/src/rendering/overflow_view.dart b/lib/src/rendering/overflow_view.dart index 9d45af0..ac341f7 100644 --- a/lib/src/rendering/overflow_view.dart +++ b/lib/src/rendering/overflow_view.dart @@ -5,12 +5,59 @@ import 'dart:math' as math; /// Parent data for use with [RenderOverflowView]. class OverflowViewParentData extends ContainerBoxParentData { + int _runIndex = 0; bool? offstage; + bool _isLastElement = false; } +class _RunMetrics { + _RunMetrics({ + required this.mainAxisExtent, + required this.crossAxisExtent, + required this.childCount, + }); + + final double mainAxisExtent; + final double crossAxisExtent; + final int childCount; + + bool get isSingleChild => childCount == 1; + bool get hasNoChild => childCount == 0; + + _RunMetrics copyWith({ + double? mainAxisExtent, + double? crossAxisExtent, + int? childCount, + }) => + _RunMetrics( + mainAxisExtent: mainAxisExtent ?? this.mainAxisExtent, + crossAxisExtent: crossAxisExtent ?? this.crossAxisExtent, + childCount: childCount ?? this.childCount, + ); +} + +/// Used with [OverflowView] to define how it should constrain all children and +/// displays them. enum OverflowViewLayoutBehavior { + /// All the children will be constrained to have the same size + /// as the first one. + /// + /// Places the children in one line. + /// + /// This can be used for an avatar list for example. fixed, + + /// Let all children to determine their own size. + /// + /// Places the children in one line. + /// + /// This can be used for a menu bar for example. flexible, + + /// Let all children to determine their own size. + /// + /// Displays its children in multiple horizontal or vertical runs. + wrap, } class RenderOverflowView extends RenderBox @@ -20,26 +67,83 @@ class RenderOverflowView extends RenderBox RenderOverflowView({ List? children, required Axis direction, + required WrapAlignment alignment, required double spacing, + required WrapAlignment runAlignment, + required double runSpacing, + required WrapCrossAlignment crossAxisAlignment, + required int maxRun, + required int? maxItemPerRun, + TextDirection? textDirection, + required VerticalDirection verticalDirection, required OverflowViewLayoutBehavior layoutBehavior, }) : assert(spacing > double.negativeInfinity && spacing < double.infinity), + assert(maxRun > 0), + assert(maxItemPerRun == null || maxItemPerRun > 0), _direction = direction, + _alignment = alignment, _spacing = spacing, - _layoutBehavior = layoutBehavior, - _isHorizontal = direction == Axis.horizontal { + _runAlignment = runAlignment, + _runSpacing = runSpacing, + _crossAxisAlignment = crossAxisAlignment, + _maxRun = maxRun, + _maxItemPerRun = maxItemPerRun, + _textDirection = textDirection, + _verticalDirection = verticalDirection, + _layoutBehavior = layoutBehavior { addAll(children); } + /// The direction to use as the main axis. + /// + /// For example, if [direction] is [Axis.horizontal], the default, the + /// children are placed adjacent to one another in a horizontal run until the + /// available horizontal space is consumed, at which point a subsequent + /// children are placed in a new run vertically adjacent to the previous run. Axis get direction => _direction; Axis _direction; set direction(Axis value) { if (_direction != value) { _direction = value; - _isHorizontal = direction == Axis.horizontal; markNeedsLayout(); } } + bool get _isHorizontal => direction == Axis.horizontal; + + /// How the children within a run should be placed in the main axis. + /// + /// For example, if [alignment] is [WrapAlignment.center], the children in + /// each run are grouped together in the center of their run in the main axis. + /// + /// Defaults to [WrapAlignment.start]. + /// + /// See also: + /// + /// * [runAlignment], which controls how the runs are placed relative to each + /// other in the cross axis. + /// * [crossAxisAlignment], which controls how the children within each run + /// are placed relative to each other in the cross axis. + WrapAlignment get alignment => _alignment; + WrapAlignment _alignment; + set alignment(WrapAlignment value) { + if (_alignment == value) return; + + _alignment = value; + markNeedsLayout(); + } + + /// How much space to place between children in a run in the main axis. + /// + /// For example, if [spacing] is 10.0, the children will be spaced at least + /// 10.0 logical pixels apart in the main axis. + /// + /// If there is additional free space in a run (e.g., because the wrap has a + /// minimum size that is not filled or because some runs are longer than + /// others), the additional free space will be allocated according to the + /// [alignment]. + /// + /// Defaults to 0.0. double get spacing => _spacing; double _spacing; set spacing(double value) { @@ -50,6 +154,161 @@ class RenderOverflowView extends RenderBox } } + /// How the runs themselves should be placed in the cross axis. + /// + /// For example, if [runAlignment] is [WrapAlignment.center], the runs are + /// grouped together in the center of the overall [RenderOverflowView] in the cross + /// axis. + /// + /// Defaults to [WrapAlignment.start]. + /// + /// See also: + /// + /// * [alignment], which controls how the children within each run are placed + /// relative to each other in the main axis. + /// * [crossAxisAlignment], which controls how the children within each run + /// are placed relative to each other in the cross axis. + WrapAlignment get runAlignment => _runAlignment; + WrapAlignment _runAlignment; + set runAlignment(WrapAlignment value) { + if (_runAlignment == value) return; + + _runAlignment = value; + markNeedsLayout(); + } + + /// How much space to place between the runs themselves in the cross axis. + /// + /// For example, if [runSpacing] is 10.0, the runs will be spaced at least + /// 10.0 logical pixels apart in the cross axis. + /// + /// If there is additional free space in the overall [RenderOverflowView] (e.g., + /// because the wrap has a minimum size that is not filled), the additional + /// free space will be allocated according to the [runAlignment]. + /// + /// Defaults to 0.0. + double get runSpacing => _runSpacing; + double _runSpacing; + set runSpacing(double value) { + assert(value >= 0 && value < double.infinity); + + if (_runSpacing == value) return; + + _runSpacing = value; + markNeedsLayout(); + } + + /// How the children within a run should be aligned relative to each other in + /// the cross axis. + /// + /// For example, if this is set to [WrapCrossAlignment.end], and the + /// [direction] is [Axis.horizontal], then the children within each + /// run will have their bottom edges aligned to the bottom edge of the run. + /// + /// Defaults to [WrapCrossAlignment.start]. + /// + /// See also: + /// + /// * [alignment], which controls how the children within each run are placed + /// relative to each other in the main axis. + /// * [runAlignment], which controls how the runs are placed relative to each + /// other in the cross axis. + WrapCrossAlignment get crossAxisAlignment => _crossAxisAlignment; + WrapCrossAlignment _crossAxisAlignment; + set crossAxisAlignment(WrapCrossAlignment value) { + if (_crossAxisAlignment == value) return; + + _crossAxisAlignment = value; + markNeedsLayout(); + } + + /// A maximum number of rows (the runs). + int get maxRun => _maxRun; + int _maxRun; + set maxRun(int value) { + assert(value > 0); + + if (_maxRun == value) return; + + _maxRun = value; + markNeedsLayout(); + } + + /// A maximum number of columns (the item in each run). + int? get maxItemPerRun => _maxItemPerRun; + int? _maxItemPerRun; + set maxItemPerRun(int? value) { + assert(value == null || value > 0); + + if (_maxItemPerRun == value) return; + + _maxItemPerRun = value; + markNeedsLayout(); + } + + /// Determines the order to lay children out horizontally and how to interpret + /// `start` and `end` in the horizontal direction. + /// + /// If the [direction] is [Axis.horizontal], this controls the order in which + /// children are positioned (left-to-right or right-to-left), and the meaning + /// of the [alignment] property's [WrapAlignment.start] and + /// [WrapAlignment.end] values. + /// + /// If the [direction] is [Axis.horizontal], and either the + /// [alignment] is either [WrapAlignment.start] or [WrapAlignment.end], or + /// there's more than one child, then the [textDirection] must not be null. + /// + /// If the [direction] is [Axis.vertical], this controls the order in + /// which runs are positioned, the meaning of the [runAlignment] property's + /// [WrapAlignment.start] and [WrapAlignment.end] values, as well as the + /// [crossAxisAlignment] property's [WrapCrossAlignment.start] and + /// [WrapCrossAlignment.end] values. + /// + /// If the [direction] is [Axis.vertical], and either the + /// [runAlignment] is either [WrapAlignment.start] or [WrapAlignment.end], the + /// [crossAxisAlignment] is either [WrapCrossAlignment.start] or + /// [WrapCrossAlignment.end], or there's more than one child, then the + /// [textDirection] must not be null. + TextDirection? get textDirection => _textDirection; + TextDirection? _textDirection; + set textDirection(TextDirection? value) { + if (_textDirection == value) return; + + _textDirection = value; + markNeedsLayout(); + } + + /// Determines the order to lay children out vertically and how to interpret + /// `start` and `end` in the vertical direction. + /// + /// If the [direction] is [Axis.vertical], this controls which order children + /// are painted in (down or up), the meaning of the [alignment] property's + /// [WrapAlignment.start] and [WrapAlignment.end] values. + /// + /// If the [direction] is [Axis.vertical], and either the [alignment] + /// is either [WrapAlignment.start] or [WrapAlignment.end], or there's + /// more than one child, then the [verticalDirection] must not be null. + /// + /// If the [direction] is [Axis.horizontal], this controls the order in which + /// runs are positioned, the meaning of the [runAlignment] property's + /// [WrapAlignment.start] and [WrapAlignment.end] values, as well as the + /// [crossAxisAlignment] property's [WrapCrossAlignment.start] and + /// [WrapCrossAlignment.end] values. + /// + /// If the [direction] is [Axis.horizontal], and either the + /// [runAlignment] is either [WrapAlignment.start] or [WrapAlignment.end], the + /// [crossAxisAlignment] is either [WrapCrossAlignment.start] or + /// [WrapCrossAlignment.end], or there's more than one child, then the + /// [verticalDirection] must not be null. + VerticalDirection get verticalDirection => _verticalDirection; + VerticalDirection _verticalDirection; + set verticalDirection(VerticalDirection value) { + if (_verticalDirection != value) { + _verticalDirection = value; + markNeedsLayout(); + } + } + OverflowViewLayoutBehavior get layoutBehavior => _layoutBehavior; OverflowViewLayoutBehavior _layoutBehavior; set layoutBehavior(OverflowViewLayoutBehavior value) { @@ -59,42 +318,131 @@ class RenderOverflowView extends RenderBox } } - bool _isHorizontal; + bool get _debugHasNecessaryDirections { + if (firstChild != null && lastChild != firstChild) { + // i.e. there's more than one child + if (direction == Axis.horizontal) { + assert(textDirection != null, + 'Horizontal $runtimeType with multiple children has a null textDirection, so the layout order is undefined.'); + } + } + if (alignment == WrapAlignment.start || alignment == WrapAlignment.end) { + if (direction == Axis.horizontal) { + assert(textDirection != null, + 'Horizontal $runtimeType with alignment $alignment has a null textDirection, so the alignment cannot be resolved.'); + } + } + if (runAlignment == WrapAlignment.start || + runAlignment == WrapAlignment.end) { + if (direction == Axis.vertical) { + assert(textDirection != null, + 'Vertical $runtimeType with runAlignment $runAlignment has a null textDirection, so the alignment cannot be resolved.'); + } + } + if (crossAxisAlignment == WrapCrossAlignment.start || + crossAxisAlignment == WrapCrossAlignment.end) { + if (direction == Axis.vertical) { + assert(textDirection != null, + 'Vertical $runtimeType with crossAxisAlignment $crossAxisAlignment has a null textDirection, so the alignment cannot be resolved.'); + } + } + return true; + } + @override void setupParentData(RenderBox child) { if (child.parentData is! OverflowViewParentData) child.parentData = OverflowViewParentData(); } - double _getCrossSize(RenderBox child) { - switch (_direction) { + @override + double computeMinIntrinsicWidth(double height) { + switch (direction) { + case Axis.horizontal: + double width = 0.0; + RenderBox? child = firstChild; + while (child != null) { + width = math.max(width, child.getMinIntrinsicWidth(double.infinity)); + child = childAfter(child); + } + return width; + case Axis.vertical: + return computeDryLayout(BoxConstraints(maxHeight: height)).width; + } + } + + @override + double computeMaxIntrinsicWidth(double height) { + switch (direction) { case Axis.horizontal: - return child.size.height; + double width = 0.0; + RenderBox? child = firstChild; + while (child != null) { + width += child.getMaxIntrinsicWidth(double.infinity); + child = childAfter(child); + } + return width; case Axis.vertical: - return child.size.width; + return computeDryLayout(BoxConstraints(maxHeight: height)).width; } } - double _getMainSize(RenderBox child) { - switch (_direction) { + @override + double computeMinIntrinsicHeight(double width) { + switch (direction) { case Axis.horizontal: - return child.size.width; + return computeDryLayout(BoxConstraints(maxWidth: width)).height; case Axis.vertical: - return child.size.height; + double height = 0.0; + RenderBox? child = firstChild; + while (child != null) { + height = + math.max(height, child.getMinIntrinsicHeight(double.infinity)); + child = childAfter(child); + } + return height; } } - bool _hasOverflow = false; + @override + double computeMaxIntrinsicHeight(double width) { + switch (direction) { + case Axis.horizontal: + return computeDryLayout(BoxConstraints(maxWidth: width)).height; + case Axis.vertical: + double height = 0.0; + RenderBox? child = firstChild; + while (child != null) { + height += child.getMaxIntrinsicHeight(double.infinity); + child = childAfter(child); + } + return height; + } + } + + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + return defaultComputeDistanceToHighestActualBaseline(baseline); + } + + bool _hasVisualOverflow = false; @override void performLayout() { - _hasOverflow = false; + assert(_debugHasNecessaryDirections); + _hasVisualOverflow = false; assert(firstChild != null); resetOffstage(); - if (layoutBehavior == OverflowViewLayoutBehavior.fixed) { - performFixedLayout(); - } else { - performFlexibleLayout(); + switch (layoutBehavior) { + case OverflowViewLayoutBehavior.fixed: + performFixedLayout(); + break; + case OverflowViewLayoutBehavior.flexible: + performFlexibleLayout(); + break; + case OverflowViewLayoutBehavior.wrap: + performWrapLayout(); + break; } } @@ -107,7 +455,14 @@ class RenderOverflowView extends RenderBox } void performFixedLayout() { - RenderBox child = firstChild!; + final BoxConstraints constraints = this.constraints; + + RenderBox? child = firstChild; + if (child == null) { + size = constraints.smallest; + return; + } + final BoxConstraints childConstraints = constraints.loosen(); final double maxExtent = _isHorizontal ? constraints.maxWidth : constraints.maxHeight; @@ -115,8 +470,8 @@ class RenderOverflowView extends RenderBox OverflowViewParentData childParentData = child.parentData as OverflowViewParentData; child.layout(childConstraints, parentUsesSize: true); - final double childExtent = child.size.getMainExtent(direction); - final double crossExtent = child.size.getCrossExtent(direction); + final double childExtent = _getMainAxisExtent(child.size); + final double crossExtent = _getCrossAxisExtent(child.size); final BoxConstraints otherChildConstraints = _isHorizontal ? childConstraints.tighten(width: childExtent, height: crossExtent) : childConstraints.tighten(height: childExtent, width: crossExtent); @@ -206,7 +561,7 @@ class RenderOverflowView extends RenderBox child.layout(childConstraints, parentUsesSize: true); - final double childMainSize = _getMainSize(child); + final double childMainSize = _getMainAxisExtent(child.size); if (childMainSize <= availableExtent) { // We have room to paint this child. @@ -240,7 +595,7 @@ class RenderOverflowView extends RenderBox parentUsesSize: true, ); - final double childMainSize = _getMainSize(overflowIndicator); + final double childMainSize = _getMainAxisExtent(overflowIndicator.size); // We need to remove the children that prevent the overflowIndicator // to paint. @@ -249,7 +604,7 @@ class RenderOverflowView extends RenderBox final OverflowViewParentData childParentData = child.parentData as OverflowViewParentData; childParentData.offstage = true; - final double childStride = _getMainSize(child) + spacing; + final double childStride = _getMainAxisExtent(child.size) + spacing; availableExtent += childStride; unRenderedChildCount++; @@ -258,7 +613,7 @@ class RenderOverflowView extends RenderBox if (childMainSize > availableExtent) { // We cannot paint any child because there is not enough space. - _hasOverflow = true; + _hasVisualOverflow = true; } if (overflowIndicatorConstraints.value != unRenderedChildCount) { @@ -297,21 +652,21 @@ class RenderOverflowView extends RenderBox // Because the overflow indicator will be paint outside of the screen, // we need to say that there is an overflow. - _hasOverflow = true; + _hasVisualOverflow = true; } final double crossSize = renderBoxes.fold( 0, (previousValue, element) => math.max( previousValue, - _getCrossSize(element), + _getCrossAxisExtent(element.size), ), ); // By default we center all children in the cross-axis. for (final child in renderBoxes) { final double childCrossPosition = - crossSize / 2.0 - _getCrossSize(child) / 2.0; + crossSize / 2.0 - _getCrossAxisExtent(child.size) / 2.0; final OverflowViewParentData childParentData = child.parentData as OverflowViewParentData; childParentData.offset = _isHorizontal @@ -329,6 +684,572 @@ class RenderOverflowView extends RenderBox size = constraints.constrain(idealSize); } + void performWrapLayout() { + final BoxConstraints constraints = this.constraints; + + RenderBox? child = firstChild; + if (child == null) { + size = constraints.smallest; + return; + } + + final BoxConstraints childConstraints; + double mainAxisLimit = 0.0; + double crossAxisLimit = 0.0; + bool flipMainAxis = false; + bool flipCrossAxis = false; + + switch (direction) { + case Axis.horizontal: + childConstraints = BoxConstraints(maxWidth: constraints.maxWidth); + mainAxisLimit = constraints.maxWidth; + crossAxisLimit = constraints.maxHeight; + if (textDirection == TextDirection.rtl) flipMainAxis = true; + if (verticalDirection == VerticalDirection.up) flipCrossAxis = true; + break; + case Axis.vertical: + childConstraints = BoxConstraints(maxHeight: constraints.maxHeight); + mainAxisLimit = constraints.maxHeight; + crossAxisLimit = constraints.maxWidth; + if (verticalDirection == VerticalDirection.up) flipMainAxis = true; + if (textDirection == TextDirection.rtl) flipCrossAxis = true; + break; + } + + List renderBoxes = []; + int unRenderedChildCount = this.childCount - 1; + + final double spacing = this.spacing; + final double runSpacing = this.runSpacing; + final List<_RunMetrics> runMetrics = <_RunMetrics>[]; + double mainAxisExtent = 0.0; + double crossAxisExtent = 0.0; + double currentRunMainAxisExtent = 0.0; + double currentRunCrossAxisExtent = 0.0; + int childCount = 0; + int runCount = 0; + int itemCountPerRun = 0; + + bool showOverflowIndicator = false; + Offset currentChildOffset = Offset.zero; + + OverflowViewParentData? previousChildParentData; + + while (child != lastChild) { + child!.layout(childConstraints, parentUsesSize: true); + + final OverflowViewParentData childParentData = + child.parentData as OverflowViewParentData; + childParentData.offset = currentChildOffset; + + final double childMainAxisExtent = _getMainAxisExtent(child.size); + final double childCrossAxisExtent = _getCrossAxisExtent(child.size); + + final double childMainAxisStride = spacing + childMainAxisExtent; + + double childCrossAxisStride = currentChildOffset.dy; + + if (childCount > 0 && + (itemCountPerRun == maxItemPerRun || + currentRunMainAxisExtent + childMainAxisStride > mainAxisLimit)) { + mainAxisExtent = math.max(mainAxisExtent, currentRunMainAxisExtent); + crossAxisExtent += currentRunCrossAxisExtent; + if (runMetrics.isNotEmpty) { + crossAxisExtent += runSpacing; + } + + childCrossAxisStride = crossAxisExtent; + + runMetrics.add(_RunMetrics( + mainAxisExtent: currentRunMainAxisExtent, + crossAxisExtent: currentRunCrossAxisExtent, + childCount: childCount, + )); + + runCount = runMetrics.length; + + if (previousChildParentData != null) { + previousChildParentData._isLastElement = true; + } + + childCount = 0; + itemCountPerRun = 0; + + if (runCount == maxRun || + crossAxisExtent + childCrossAxisExtent + runSpacing > + crossAxisLimit) { + // We have no room to paint any further child. + showOverflowIndicator = true; + break; + } + + currentRunMainAxisExtent = 0.0; + currentRunCrossAxisExtent = 0.0; + } + + currentChildOffset = Offset(childMainAxisStride, childCrossAxisStride); + + currentRunMainAxisExtent += childMainAxisExtent; + + if (childCount > 0) { + currentRunMainAxisExtent += spacing; + } + + currentRunCrossAxisExtent = + math.max(currentRunCrossAxisExtent, childCrossAxisExtent); + childCount += 1; + itemCountPerRun += 1; + unRenderedChildCount--; + + childParentData.offstage = false; + childParentData._runIndex = runMetrics.length; + renderBoxes.add(child); + + child = childParentData.nextSibling!; + + previousChildParentData = childParentData; + } + + if (childCount != 0) { + mainAxisExtent = math.max(mainAxisExtent, currentRunMainAxisExtent); + crossAxisExtent += currentRunCrossAxisExtent; + if (runMetrics.isNotEmpty) { + crossAxisExtent += runSpacing; + } + runMetrics.add(_RunMetrics( + mainAxisExtent: currentRunMainAxisExtent, + crossAxisExtent: currentRunCrossAxisExtent, + childCount: childCount, + )); + } + + runCount = runMetrics.length; + assert(runCount > 0); + + double overflowIndicatorMainAxisLimit = currentRunMainAxisExtent == 0.0 + ? 0.0 + : mainAxisLimit - currentRunMainAxisExtent; + + if (showOverflowIndicator) { + // We didn't layout all the children. + final RenderBox overflowIndicator = lastChild!; + final BoxValueConstraints overflowIndicatorConstraints = + BoxValueConstraints( + value: unRenderedChildCount, + constraints: childConstraints, + ); + overflowIndicator.layout( + overflowIndicatorConstraints, + parentUsesSize: true, + ); + + double overflowIndicatorMainAxisExtent = + _getMainAxisExtent(overflowIndicator.size); + double overflowIndicatorCrossAxisExtent = + _getCrossAxisExtent(overflowIndicator.size); + + Offset overflowIndicatorOffset = currentChildOffset; + int overflowIndicatorRunIndex = runMetrics.length - 1; + bool isLastElement = false; + + double overflowIndicatorMainAxisStride = + spacing + overflowIndicatorMainAxisExtent; + + if (overflowIndicatorMainAxisStride + runMetrics.last.mainAxisExtent < + mainAxisLimit && + (maxItemPerRun == null || + runMetrics.last.childCount < maxItemPerRun!)) { + overflowIndicatorOffset = Offset( + overflowIndicatorMainAxisStride, + math.max( + runMetrics.last.crossAxisExtent, + overflowIndicatorCrossAxisExtent, + ), + ); + isLastElement = true; + + _RunMetrics oldMetrics = runMetrics.removeLast(); + runMetrics.add( + _RunMetrics( + mainAxisExtent: oldMetrics.mainAxisExtent, + crossAxisExtent: math.max( + oldMetrics.crossAxisExtent, + overflowIndicatorOffset.dy, + ), + childCount: oldMetrics.childCount + 1, + ), + ); + } else { + // We need to remove the children that prevent the overflowIndicator + // to paint. + while (overflowIndicatorConstraints.value != unRenderedChildCount || + runMetrics.last.childCount == maxItemPerRun || + (overflowIndicatorMainAxisStride > overflowIndicatorMainAxisLimit && + renderBoxes.isNotEmpty)) { + if (overflowIndicatorConstraints.value.toString().length != + unRenderedChildCount.toString().length) { + // The number of unrendered child drastically changed + // (like from 9 to 10), we have to layout the indicator another time. + overflowIndicator.layout( + BoxValueConstraints( + value: unRenderedChildCount, + constraints: childConstraints, + ), + parentUsesSize: true, + ); + + overflowIndicatorMainAxisExtent = + _getMainAxisExtent(overflowIndicator.size); + overflowIndicatorCrossAxisExtent = + _getCrossAxisExtent(overflowIndicator.size); + } else if (overflowIndicatorMainAxisStride <= + overflowIndicatorMainAxisLimit && + (maxItemPerRun == null || + runMetrics.last.childCount < maxItemPerRun!)) { + break; + } + + final RenderBox child = renderBoxes.removeLast(); + final OverflowViewParentData childParentData = + child.parentData as OverflowViewParentData; + childParentData.offstage = true; + + final _RunMetrics oldMetrics = + runMetrics.removeAt(childParentData._runIndex); + final int runMetricsChildCount = oldMetrics.childCount; + + unRenderedChildCount++; + + final double childMainAxisStride = childParentData.offset.dx; + overflowIndicatorOffset = Offset( + childMainAxisStride, + childParentData.offset.dy, + ); + + if (childParentData._isLastElement) { + if (runMetricsChildCount > 1) { + overflowIndicatorMainAxisLimit -= spacing; + } + overflowIndicatorMainAxisLimit = oldMetrics.isSingleChild + ? mainAxisLimit + : mainAxisLimit - childMainAxisStride; + overflowIndicatorMainAxisStride = overflowIndicatorMainAxisExtent; + + isLastElement = true; + } else { + overflowIndicatorMainAxisLimit += childMainAxisStride; + if (runMetricsChildCount > 1) { + overflowIndicatorMainAxisLimit += spacing; + } + overflowIndicatorRunIndex = childParentData._runIndex; + overflowIndicatorMainAxisStride = + spacing + overflowIndicatorMainAxisExtent; + + isLastElement = false; + } + + runMetrics.insert( + childParentData._runIndex, + oldMetrics.copyWith( + mainAxisExtent: oldMetrics.isSingleChild + ? overflowIndicatorMainAxisExtent + : oldMetrics.mainAxisExtent - childMainAxisStride, + childCount: oldMetrics.childCount - 1, + ), + ); + + final _RunMetrics refreshedMetrics = + runMetrics[childParentData._runIndex]; + if (refreshedMetrics.hasNoChild && childParentData._runIndex > 0) { + final _RunMetrics newMetrics = + runMetrics[childParentData._runIndex - 1]; + + if (maxItemPerRun == null && + overflowIndicatorMainAxisStride + newMetrics.mainAxisExtent < + mainAxisLimit) { + // We can bring the overflowIndicator to the previous run + // + // ┌────────┐ ┌────────┐ + // │╔══╗┌──┐│ │╔══╗╔══╗│ + // │╚══╝╘═↑╛│ ==> │╚══╝╚══╝│ + // │╔══╗ _↑ │ ==> │ │ + // │╚══╝ │ │ │ + // └────────┘ └────────┘ + overflowIndicatorOffset = Offset( + newMetrics.mainAxisExtent + spacing, + refreshedMetrics.crossAxisExtent - newMetrics.crossAxisExtent, + ); + isLastElement = true; + + runMetrics.removeLast(); + + _RunMetrics oldMetrics = runMetrics.removeLast(); + runMetrics.add( + oldMetrics.copyWith( + childCount: oldMetrics.childCount + 1, + ), + ); + + overflowIndicatorMainAxisLimit = + mainAxisLimit - oldMetrics.mainAxisExtent; + overflowIndicatorRunIndex = runMetrics.length - 1; + overflowIndicatorMainAxisStride = + spacing + overflowIndicatorMainAxisExtent; + } + } + } + } + + if (overflowIndicatorMainAxisExtent > overflowIndicatorMainAxisLimit) { + // We cannot paint any child because there is not enough space. + _hasVisualOverflow = true; + } + + if (overflowIndicatorConstraints.value != unRenderedChildCount) { + // The number of unrendered child changed, we have to layout the + // indicator another time. + overflowIndicator.layout( + BoxValueConstraints( + value: unRenderedChildCount, + constraints: childConstraints, + ), + parentUsesSize: true, + ); + } + + final OverflowViewParentData overflowIndicatorParentData = + overflowIndicator.parentData as OverflowViewParentData; + overflowIndicatorParentData.offset = overflowIndicatorOffset; + overflowIndicatorParentData.offstage = false; + overflowIndicatorParentData._isLastElement = isLastElement; + overflowIndicatorParentData._runIndex = overflowIndicatorRunIndex; + _RunMetrics oldMetrics = runMetrics.removeAt(overflowIndicatorRunIndex); + runMetrics.insert( + overflowIndicatorRunIndex, + _RunMetrics( + mainAxisExtent: oldMetrics.hasNoChild + ? oldMetrics.mainAxisExtent + : oldMetrics.mainAxisExtent + overflowIndicatorMainAxisStride, + crossAxisExtent: math.max( + overflowIndicatorCrossAxisExtent, + oldMetrics.crossAxisExtent, + ), + childCount: oldMetrics.childCount + 1, + ), + ); + + currentChildOffset = overflowIndicatorOffset; + mainAxisExtent = math.max(mainAxisExtent, runMetrics.last.mainAxisExtent); + } else { + // We need to layout the overflowIndicator because we may have already + // laid it out with parentUsesSize: true before. + // When unmounting a _LayoutBuilderElement, it calls markNeedsLayout + // a last time, and can cause error. + lastChild?.layout( + BoxValueConstraints( + value: unRenderedChildCount, + constraints: childConstraints, + ), + ); + } + + runCount = runMetrics.length; + assert(runCount > 0); + + _performWrapLayout( + flipMainAxis: flipMainAxis, + flipCrossAxis: flipCrossAxis, + mainAxisExtent: mainAxisExtent, + crossAxisExtent: crossAxisExtent, + runCount: runCount, + runMetrics: runMetrics, + ); + } + + void _performWrapLayout({ + required bool flipMainAxis, + required bool flipCrossAxis, + required double mainAxisExtent, + required double crossAxisExtent, + required int runCount, + required List<_RunMetrics> runMetrics, + }) { + double containerMainAxisExtent = 0.0; + double containerCrossAxisExtent = 0.0; + + switch (direction) { + case Axis.horizontal: + size = constraints.constrain(Size(mainAxisExtent, crossAxisExtent)); + containerMainAxisExtent = size.width; + containerCrossAxisExtent = size.height; + break; + case Axis.vertical: + size = constraints.constrain(Size(crossAxisExtent, mainAxisExtent)); + containerMainAxisExtent = size.height; + containerCrossAxisExtent = size.width; + break; + } + + _hasVisualOverflow = containerMainAxisExtent < mainAxisExtent || + containerCrossAxisExtent < crossAxisExtent; + + final double crossAxisFreeSpace = + math.max(0.0, containerCrossAxisExtent - crossAxisExtent); + double runLeadingSpace = 0.0; + double runBetweenSpace = 0.0; + switch (runAlignment) { + case WrapAlignment.start: + break; + case WrapAlignment.end: + runLeadingSpace = crossAxisFreeSpace; + break; + case WrapAlignment.center: + runLeadingSpace = crossAxisFreeSpace / 2.0; + break; + case WrapAlignment.spaceBetween: + runBetweenSpace = + runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0; + break; + case WrapAlignment.spaceAround: + runBetweenSpace = crossAxisFreeSpace / runCount; + runLeadingSpace = runBetweenSpace / 2.0; + break; + case WrapAlignment.spaceEvenly: + runBetweenSpace = crossAxisFreeSpace / (runCount + 1); + runLeadingSpace = runBetweenSpace; + break; + } + + runBetweenSpace += runSpacing; + double crossAxisOffset = flipCrossAxis + ? containerCrossAxisExtent - runLeadingSpace + : runLeadingSpace; + + RenderBox? child = firstChild; + for (int i = 0; i < runCount; ++i) { + final _RunMetrics metrics = runMetrics[i]; + final double runMainAxisExtent = metrics.mainAxisExtent; + final double runCrossAxisExtent = metrics.crossAxisExtent; + final int childCount = metrics.childCount; + + final double mainAxisFreeSpace = + math.max(0.0, containerMainAxisExtent - runMainAxisExtent); + double childLeadingSpace = 0.0; + double childBetweenSpace = 0.0; + + switch (alignment) { + case WrapAlignment.start: + break; + case WrapAlignment.end: + childLeadingSpace = mainAxisFreeSpace; + break; + case WrapAlignment.center: + childLeadingSpace = mainAxisFreeSpace / 2.0; + break; + case WrapAlignment.spaceBetween: + childBetweenSpace = + childCount > 1 ? mainAxisFreeSpace / (childCount - 1) : 0.0; + break; + case WrapAlignment.spaceAround: + childBetweenSpace = mainAxisFreeSpace / childCount; + childLeadingSpace = childBetweenSpace / 2.0; + break; + case WrapAlignment.spaceEvenly: + childBetweenSpace = mainAxisFreeSpace / (childCount + 1); + childLeadingSpace = childBetweenSpace; + break; + } + + childBetweenSpace += spacing; + double childMainPosition = flipMainAxis + ? containerMainAxisExtent - childLeadingSpace + : childLeadingSpace; + + if (flipCrossAxis) crossAxisOffset -= runCrossAxisExtent; + + while (child != null) { + final OverflowViewParentData childParentData = + child.parentData! as OverflowViewParentData; + + if (childParentData._runIndex != i && + childParentData.offstage != null) { + break; + } + + if (childParentData.offstage != false) { + child = childParentData.nextSibling; + continue; + } + + final double childMainAxisExtent = _getMainAxisExtent(child.size); + final double childCrossAxisExtent = _getCrossAxisExtent(child.size); + final double childCrossAxisOffset = _getChildCrossAxisOffset( + flipCrossAxis, + runCrossAxisExtent, + childCrossAxisExtent, + ); + if (flipMainAxis) childMainPosition -= childMainAxisExtent; + childParentData.offstage = false; + childParentData.offset = _getOffset( + childMainPosition, + crossAxisOffset + childCrossAxisOffset, + ); + if (flipMainAxis) { + childMainPosition -= childBetweenSpace; + } else { + childMainPosition += childMainAxisExtent + childBetweenSpace; + } + child = childParentData.nextSibling; + } + + if (flipCrossAxis) { + crossAxisOffset -= runBetweenSpace; + } else { + crossAxisOffset += runCrossAxisExtent + runBetweenSpace; + } + } + } + + double _getMainAxisExtent(Size childSize) { + switch (direction) { + case Axis.horizontal: + return childSize.width; + case Axis.vertical: + return childSize.height; + } + } + + double _getCrossAxisExtent(Size childSize) { + switch (direction) { + case Axis.horizontal: + return childSize.height; + case Axis.vertical: + return childSize.width; + } + } + + Offset _getOffset(double mainAxisOffset, double crossAxisOffset) { + switch (direction) { + case Axis.horizontal: + return Offset(mainAxisOffset, crossAxisOffset); + case Axis.vertical: + return Offset(crossAxisOffset, mainAxisOffset); + } + } + + double _getChildCrossAxisOffset(bool flipCrossAxis, double runCrossAxisExtent, + double childCrossAxisExtent) { + final double freeSpace = runCrossAxisExtent - childCrossAxisExtent; + switch (crossAxisAlignment) { + case WrapCrossAlignment.start: + return flipCrossAxis ? freeSpace : 0.0; + case WrapCrossAlignment.end: + return flipCrossAxis ? 0.0 : freeSpace; + case WrapCrossAlignment.center: + return freeSpace / 2.0; + } + } + void visitOnlyOnStageChildren(RenderObjectVisitor visitor) { visitChildren((child) { if (child.isOnstage) { @@ -342,36 +1263,38 @@ class RenderOverflowView extends RenderBox visitOnlyOnStageChildren(visitor); } + final LayerHandle _clipRectLayer = + LayerHandle(); + @override void paint(PaintingContext context, Offset offset) { - void paintChild(RenderObject child) { - final OverflowViewParentData childParentData = - child.parentData as OverflowViewParentData; - if (childParentData.offstage == false) { - context.paintChild(child, childParentData.offset + offset); - } else { - // We paint it outside the box. - context.paintChild(child, size.bottomRight(Offset.zero)); - } - } - - void defaultPaint(PaintingContext context, Offset offset) { - visitOnlyOnStageChildren(paintChild); - } - - if (_hasOverflow) { - context.pushClipRect( + if (_hasVisualOverflow) { + _clipRectLayer.layer = context.pushClipRect( needsCompositing, offset, Offset.zero & size, defaultPaint, clipBehavior: Clip.hardEdge, + oldLayer: _clipRectLayer.layer, ); } else { + _clipRectLayer.layer = null; defaultPaint(context, offset); } } + @override + void defaultPaint(PaintingContext context, Offset offset) { + visitOnlyOnStageChildren((RenderObject child) { + // Paint the child + final OverflowViewParentData childParentData = + child.parentData as OverflowViewParentData; + if (childParentData.offstage == false) { + context.paintChild(child, childParentData.offset + offset); + } + }); + } + @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { // The x, y parameters have the top left of the node's box as the origin. @@ -379,27 +1302,55 @@ class RenderOverflowView extends RenderBox final RenderBox child = renderObject as RenderBox; final OverflowViewParentData childParentData = child.parentData as OverflowViewParentData; - result.addWithPaintOffset( - offset: childParentData.offset, - position: position, - hitTest: (BoxHitTestResult result, Offset transformed) { - assert(transformed == position - childParentData.offset); - return child.hitTest(result, position: transformed); - }, - ); + if (child.hasSize && childParentData.offstage == false) { + result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - childParentData.offset); + return child.hitTest(result, position: transformed); + }, + ); + } }); return false; } -} -extension on Size { - double getMainExtent(Axis axis) { - return axis == Axis.horizontal ? width : height; + @override + void dispose() { + _clipRectLayer.layer = null; + super.dispose(); } - double getCrossExtent(Axis axis) { - return axis == Axis.horizontal ? height : width; + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('direction', direction)); + properties.add(EnumProperty('alignment', alignment)); + properties.add(DoubleProperty('spacing', spacing)); + properties.add(EnumProperty('runAlignment', runAlignment)); + properties.add(DoubleProperty('runSpacing', runSpacing)); + properties.add(DoubleProperty('crossAxisAlignment', runSpacing)); + properties.add(IntProperty('maxRun', maxRun)); + properties.add(IntProperty( + 'maxItemPerRun', + maxItemPerRun, + defaultValue: null, + )); + properties.add(EnumProperty( + 'textDirection', + textDirection, + defaultValue: null, + )); + properties.add(EnumProperty( + 'verticalDirection', + verticalDirection, + )); + properties.add(EnumProperty( + 'layoutBehavior', + layoutBehavior, + )); } } diff --git a/lib/src/widgets/overflow_view.dart b/lib/src/widgets/overflow_view.dart index 4136745..0378627 100644 --- a/lib/src/widgets/overflow_view.dart +++ b/lib/src/widgets/overflow_view.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:overflow_view/src/rendering/overflow_view.dart'; import 'package:value_layout_builder/value_layout_builder.dart'; @@ -19,7 +20,7 @@ class OverflowView extends MultiChildRenderObjectWidget { /// /// All children will have the same size has the first child. /// - /// The [spacing] argument must also be positive and finite. + /// The [spacing] argument must also be finite. OverflowView({ Key? key, required OverflowIndicatorBuilder builder, @@ -31,7 +32,13 @@ class OverflowView extends MultiChildRenderObjectWidget { builder: builder, direction: direction, children: children, + alignment: WrapAlignment.start, spacing: spacing, + runAlignment: WrapAlignment.start, + runSpacing: 0, + crossAxisAlignment: WrapCrossAlignment.start, + maxRun: 1, + verticalDirection: VerticalDirection.down, layoutBehavior: OverflowViewLayoutBehavior.fixed, ); @@ -39,7 +46,7 @@ class OverflowView extends MultiChildRenderObjectWidget { /// /// All children can have their own size. /// - /// The [spacing] argument must also be positive and finite. + /// The [spacing] argument must also be finite. OverflowView.flexible({ Key? key, required OverflowIndicatorBuilder builder, @@ -51,19 +58,68 @@ class OverflowView extends MultiChildRenderObjectWidget { builder: builder, direction: direction, children: children, + alignment: WrapAlignment.start, spacing: spacing, + runAlignment: WrapAlignment.start, + runSpacing: 0, + crossAxisAlignment: WrapCrossAlignment.start, + maxRun: 1, + verticalDirection: VerticalDirection.down, layoutBehavior: OverflowViewLayoutBehavior.flexible, ); + /// Creates a flexible [OverflowView]. + /// + /// All children can have their own size. + /// + /// The [spacing] argument must also be finite. + OverflowView.wrap({ + Key? key, + required OverflowIndicatorBuilder builder, + Axis direction = Axis.horizontal, + required List children, + WrapAlignment alignment = WrapAlignment.start, + double spacing = 0, + WrapAlignment runAlignment = WrapAlignment.start, + double runSpacing = 0.0, + WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start, + int maxRun = 1, + int? maxItemPerRun, + TextDirection? textDirection, + VerticalDirection verticalDirection = VerticalDirection.down, + }) : this._all( + key: key, + builder: builder, + direction: direction, + children: children, + alignment: alignment, + spacing: spacing, + runAlignment: runAlignment, + runSpacing: runSpacing, + crossAxisAlignment: crossAxisAlignment, + maxRun: maxRun, + textDirection: textDirection, + verticalDirection: verticalDirection, + layoutBehavior: OverflowViewLayoutBehavior.wrap, + ); + OverflowView._all({ Key? key, required OverflowIndicatorBuilder builder, this.direction = Axis.horizontal, required List children, + this.alignment = WrapAlignment.start, this.spacing = 0, + this.runAlignment = WrapAlignment.start, + this.runSpacing = 0.0, + this.crossAxisAlignment = WrapCrossAlignment.start, + this.maxRun = 1, + this.maxItemPerRun, + this.textDirection, + this.verticalDirection = VerticalDirection.down, required OverflowViewLayoutBehavior layoutBehavior, - }) : assert(spacing > double.negativeInfinity && - spacing < double.infinity), + }) : assert(spacing > double.negativeInfinity && spacing < double.infinity), + assert(maxItemPerRun == null || maxItemPerRun > 0), _layoutBehavior = layoutBehavior, super( key: key, @@ -83,9 +139,145 @@ class OverflowView extends MultiChildRenderObjectWidget { /// children are placed adjacent to one another as in a [Row]. final Axis direction; - /// The amount of space between successive children. + /// How the children within a run should be placed in the main axis. + /// + /// For example, if [alignment] is [WrapAlignment.center], the children in + /// each run are grouped together in the center of their run in the main axis. + /// + /// Defaults to [WrapAlignment.start]. + /// + /// See also: + /// + /// * [runAlignment], which controls how the runs are placed relative to each + /// other in the cross axis. + /// * [crossAxisAlignment], which controls how the children within each run + /// are placed relative to each other in the cross axis. + final WrapAlignment alignment; + + /// * If [_layoutBehavior] is [OverflowViewLayoutBehavior.fixed], + /// [OverflowViewLayoutBehavior.flexible]: + /// + /// It's the amount of space between successive children. + /// + /// * If [_layoutBehavior] is [OverflowViewLayoutBehavior.wrap]: + /// + /// How much space to place between children in a run in the main axis. + /// + /// For example, if [spacing] is 10.0, the children will be spaced at least + /// 10.0 logical pixels apart in the main axis. + /// + /// If there is additional free space in a run (e.g., because the wrap has a + /// minimum size that is not filled or because some runs are longer than + /// others), the additional free space will be allocated according to the + /// [alignment]. + /// + /// Defaults to 0.0. final double spacing; + /// How the runs themselves should be placed in the cross axis. + /// + /// For example, if [runAlignment] is [WrapAlignment.center], the runs are + /// grouped together in the center of the overall [OverflowView] in the cross axis. + /// + /// Defaults to [WrapAlignment.start]. + /// + /// See also: + /// + /// * [alignment], which controls how the children within each run are placed + /// relative to each other in the main axis. + /// * [crossAxisAlignment], which controls how the children within each run + /// are placed relative to each other in the cross axis. + final WrapAlignment runAlignment; + + /// How much space to place between the runs themselves in the cross axis. + /// + /// For example, if [runSpacing] is 10.0, the runs will be spaced at least + /// 10.0 logical pixels apart in the cross axis. + /// + /// If there is additional free space in the overall [OverflowView] (e.g., because + /// the wrap has a minimum size that is not filled), the additional free space + /// will be allocated according to the [runAlignment]. + /// + /// Defaults to 0.0. + final double runSpacing; + + /// How the children within a run should be aligned relative to each other in + /// the cross axis. + /// + /// For example, if this is set to [WrapCrossAlignment.end], and the + /// [direction] is [Axis.horizontal], then the children within each + /// run will have their bottom edges aligned to the bottom edge of the run. + /// + /// Defaults to [WrapCrossAlignment.start]. + /// + /// See also: + /// + /// * [alignment], which controls how the children within each run are placed + /// relative to each other in the main axis. + /// * [runAlignment], which controls how the runs are placed relative to each + /// other in the cross axis. + final WrapCrossAlignment crossAxisAlignment; + + /// A maximum number of rows (the runs). + final int maxRun; + + /// A maximum number of columns (the item in each run). + final int? maxItemPerRun; + + /// Determines the order to lay children out horizontally and how to interpret + /// `start` and `end` in the horizontal direction. + /// + /// Defaults to the ambient [Directionality]. + /// + /// If the [direction] is [Axis.horizontal], this controls order in which the + /// children are positioned (left-to-right or right-to-left), and the meaning + /// of the [alignment] property's [WrapAlignment.start] and + /// [WrapAlignment.end] values. + /// + /// If the [direction] is [Axis.horizontal], and either the + /// [alignment] is either [WrapAlignment.start] or [WrapAlignment.end], or + /// there's more than one child, then the [textDirection] (or the ambient + /// [Directionality]) must not be null. + /// + /// If the [direction] is [Axis.vertical], this controls the order in which + /// runs are positioned, the meaning of the [runAlignment] property's + /// [WrapAlignment.start] and [WrapAlignment.end] values, as well as the + /// [crossAxisAlignment] property's [WrapCrossAlignment.start] and + /// [WrapCrossAlignment.end] values. + /// + /// If the [direction] is [Axis.vertical], and either the + /// [runAlignment] is either [WrapAlignment.start] or [WrapAlignment.end], the + /// [crossAxisAlignment] is either [WrapCrossAlignment.start] or + /// [WrapCrossAlignment.end], or there's more than one child, then the + /// [textDirection] (or the ambient [Directionality]) must not be null. + final TextDirection? textDirection; + + /// Determines the order to lay children out vertically and how to interpret + /// `start` and `end` in the vertical direction. + /// + /// If the [direction] is [Axis.vertical], this controls which order children + /// are painted in (down or up), the meaning of the [alignment] property's + /// [WrapAlignment.start] and [WrapAlignment.end] values. + /// + /// If the [direction] is [Axis.vertical], and either the [alignment] + /// is either [WrapAlignment.start] or [WrapAlignment.end], or there's + /// more than one child, then the [verticalDirection] must not be null. + /// + /// If the [direction] is [Axis.horizontal], this controls the order in which + /// runs are positioned, the meaning of the [runAlignment] property's + /// [WrapAlignment.start] and [WrapAlignment.end] values, as well as the + /// [crossAxisAlignment] property's [WrapCrossAlignment.start] and + /// [WrapCrossAlignment.end] values. + /// + /// If the [direction] is [Axis.horizontal], and either the + /// [runAlignment] is either [WrapAlignment.start] or [WrapAlignment.end], the + /// [crossAxisAlignment] is either [WrapCrossAlignment.start] or + /// [WrapCrossAlignment.end], or there's more than one child, then the + /// [verticalDirection] must not be null. + final VerticalDirection verticalDirection; + + /// Defines whether a [OverflowView] should constrain all children and + /// displays them. final OverflowViewLayoutBehavior _layoutBehavior; @override @@ -97,7 +289,15 @@ class OverflowView extends MultiChildRenderObjectWidget { RenderOverflowView createRenderObject(BuildContext context) { return RenderOverflowView( direction: direction, + alignment: alignment, spacing: spacing, + runAlignment: runAlignment, + runSpacing: runSpacing, + crossAxisAlignment: crossAxisAlignment, + maxRun: maxRun, + maxItemPerRun: maxItemPerRun, + textDirection: textDirection ?? Directionality.maybeOf(context), + verticalDirection: verticalDirection, layoutBehavior: _layoutBehavior, ); } @@ -109,9 +309,51 @@ class OverflowView extends MultiChildRenderObjectWidget { ) { renderObject ..direction = direction + ..alignment = alignment ..spacing = spacing + ..runAlignment = runAlignment + ..runSpacing = runSpacing + ..crossAxisAlignment = crossAxisAlignment + ..maxRun = maxRun + ..maxItemPerRun = maxItemPerRun + ..textDirection = textDirection ?? Directionality.maybeOf(context) + ..verticalDirection = verticalDirection ..layoutBehavior = _layoutBehavior; } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('direction', direction)); + properties.add(EnumProperty('alignment', alignment)); + properties.add(DoubleProperty('spacing', spacing)); + properties.add(EnumProperty('runAlignment', runAlignment)); + properties.add(DoubleProperty('runSpacing', runSpacing)); + properties.add(EnumProperty( + 'crossAxisAlignment', + crossAxisAlignment, + )); + properties.add(IntProperty('maxRun', maxRun)); + properties.add(IntProperty( + 'maxItemPerRun', + maxItemPerRun, + defaultValue: null, + )); + properties.add(EnumProperty( + 'textDirection', + textDirection, + defaultValue: null, + )); + properties.add(EnumProperty( + 'verticalDirection', + verticalDirection, + defaultValue: VerticalDirection.down, + )); + properties.add(EnumProperty( + 'layoutBehavior', + _layoutBehavior, + )); + } } class _OverflowViewElement extends MultiChildRenderObjectElement { From 1bb42ec3f63031b0d89343ef7c9b408bbb78be4e Mon Sep 17 00:00:00 2001 From: thanhle1547 Date: Sat, 18 Jun 2022 14:41:08 +0700 Subject: [PATCH 03/10] fix missing maxItemPerRun argument --- lib/src/widgets/overflow_view.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/widgets/overflow_view.dart b/lib/src/widgets/overflow_view.dart index 0378627..2484b8b 100644 --- a/lib/src/widgets/overflow_view.dart +++ b/lib/src/widgets/overflow_view.dart @@ -98,6 +98,7 @@ class OverflowView extends MultiChildRenderObjectWidget { runSpacing: runSpacing, crossAxisAlignment: crossAxisAlignment, maxRun: maxRun, + maxItemPerRun: maxItemPerRun, textDirection: textDirection, verticalDirection: verticalDirection, layoutBehavior: OverflowViewLayoutBehavior.wrap, From 466f99557a8a20b3d64e5740875d16da7d7003ac Mon Sep 17 00:00:00 2001 From: thanhle1547 Date: Wed, 29 Jun 2022 18:01:38 +0700 Subject: [PATCH 04/10] change performLayout condition --- lib/src/rendering/overflow_view.dart | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/src/rendering/overflow_view.dart b/lib/src/rendering/overflow_view.dart index ac341f7..b1b7932 100644 --- a/lib/src/rendering/overflow_view.dart +++ b/lib/src/rendering/overflow_view.dart @@ -431,7 +431,14 @@ class RenderOverflowView extends RenderBox void performLayout() { assert(_debugHasNecessaryDirections); _hasVisualOverflow = false; - assert(firstChild != null); + + final BoxConstraints constraints = this.constraints; + + if (childCount == 1) { + size = constraints.smallest; + return; + } + resetOffstage(); switch (layoutBehavior) { case OverflowViewLayoutBehavior.fixed: @@ -455,18 +462,11 @@ class RenderOverflowView extends RenderBox } void performFixedLayout() { - final BoxConstraints constraints = this.constraints; - - RenderBox? child = firstChild; - if (child == null) { - size = constraints.smallest; - return; - } - final BoxConstraints childConstraints = constraints.loosen(); final double maxExtent = _isHorizontal ? constraints.maxWidth : constraints.maxHeight; + RenderBox child = firstChild!; OverflowViewParentData childParentData = child.parentData as OverflowViewParentData; child.layout(childConstraints, parentUsesSize: true); @@ -685,14 +685,6 @@ class RenderOverflowView extends RenderBox } void performWrapLayout() { - final BoxConstraints constraints = this.constraints; - - RenderBox? child = firstChild; - if (child == null) { - size = constraints.smallest; - return; - } - final BoxConstraints childConstraints; double mainAxisLimit = 0.0; double crossAxisLimit = 0.0; @@ -735,6 +727,7 @@ class RenderOverflowView extends RenderBox OverflowViewParentData? previousChildParentData; + RenderBox? child = firstChild; while (child != lastChild) { child!.layout(childConstraints, parentUsesSize: true); From 218e7b8d8067401048230de436952d3be589012a Mon Sep 17 00:00:00 2001 From: thanhle1547 Date: Tue, 28 Jun 2022 15:37:46 +0700 Subject: [PATCH 05/10] Update changelog file and versioning --- CHANGELOG.md | 4 ++++ example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b4aba2..53c7603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0 +### Enhancement +* [Wrapped items](https://github.com/letsar/overflow_view/issues/2) + ## 0.3.1 ### Changed * Small formatting issues. diff --git a/example/pubspec.lock b/example/pubspec.lock index 4842ad1..159b2b3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -94,7 +94,7 @@ packages: path: ".." relative: true source: path - version: "0.3.1" + version: "0.4.0" path: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 52520aa..494e2c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: overflow_view description: A widget displaying children in a line with an overflow indicator at the end if there is not enough space. -version: 0.3.1 +version: 0.4.0 homepage: https://github.com/letsar/overflow_view environment: From bc81db201ef620f770164e932388fd9d7c25ce6a Mon Sep 17 00:00:00 2001 From: thanhle1547 Date: Sun, 23 Oct 2022 22:06:23 +0700 Subject: [PATCH 06/10] make maxRun argument of OverflowView.wrap nullable --- lib/src/rendering/overflow_view.dart | 12 ++++++------ lib/src/widgets/overflow_view.dart | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/src/rendering/overflow_view.dart b/lib/src/rendering/overflow_view.dart index b1b7932..5ce5313 100644 --- a/lib/src/rendering/overflow_view.dart +++ b/lib/src/rendering/overflow_view.dart @@ -72,13 +72,13 @@ class RenderOverflowView extends RenderBox required WrapAlignment runAlignment, required double runSpacing, required WrapCrossAlignment crossAxisAlignment, - required int maxRun, + required int? maxRun, required int? maxItemPerRun, TextDirection? textDirection, required VerticalDirection verticalDirection, required OverflowViewLayoutBehavior layoutBehavior, }) : assert(spacing > double.negativeInfinity && spacing < double.infinity), - assert(maxRun > 0), + assert(maxRun == null || maxRun > 0), assert(maxItemPerRun == null || maxItemPerRun > 0), _direction = direction, _alignment = alignment, @@ -223,10 +223,10 @@ class RenderOverflowView extends RenderBox } /// A maximum number of rows (the runs). - int get maxRun => _maxRun; - int _maxRun; - set maxRun(int value) { - assert(value > 0); + int? get maxRun => _maxRun; + int? _maxRun; + set maxRun(int? value) { + assert(value == null || value > 0); if (_maxRun == value) return; diff --git a/lib/src/widgets/overflow_view.dart b/lib/src/widgets/overflow_view.dart index 2484b8b..26109a2 100644 --- a/lib/src/widgets/overflow_view.dart +++ b/lib/src/widgets/overflow_view.dart @@ -83,7 +83,7 @@ class OverflowView extends MultiChildRenderObjectWidget { WrapAlignment runAlignment = WrapAlignment.start, double runSpacing = 0.0, WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start, - int maxRun = 1, + int? maxRun = 1, int? maxItemPerRun, TextDirection? textDirection, VerticalDirection verticalDirection = VerticalDirection.down, @@ -220,7 +220,7 @@ class OverflowView extends MultiChildRenderObjectWidget { final WrapCrossAlignment crossAxisAlignment; /// A maximum number of rows (the runs). - final int maxRun; + final int? maxRun; /// A maximum number of columns (the item in each run). final int? maxItemPerRun; From 72469c7e50542f429135ac99f35d0ecd572f4137 Mon Sep 17 00:00:00 2001 From: thanhle1547 Date: Sun, 23 Oct 2022 22:10:01 +0700 Subject: [PATCH 07/10] change the maxRun argument default value of OverflowView.wrap to null --- lib/src/widgets/overflow_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/overflow_view.dart b/lib/src/widgets/overflow_view.dart index 26109a2..0c203f5 100644 --- a/lib/src/widgets/overflow_view.dart +++ b/lib/src/widgets/overflow_view.dart @@ -83,7 +83,7 @@ class OverflowView extends MultiChildRenderObjectWidget { WrapAlignment runAlignment = WrapAlignment.start, double runSpacing = 0.0, WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start, - int? maxRun = 1, + int? maxRun, int? maxItemPerRun, TextDirection? textDirection, VerticalDirection verticalDirection = VerticalDirection.down, @@ -114,7 +114,7 @@ class OverflowView extends MultiChildRenderObjectWidget { this.runAlignment = WrapAlignment.start, this.runSpacing = 0.0, this.crossAxisAlignment = WrapCrossAlignment.start, - this.maxRun = 1, + this.maxRun, this.maxItemPerRun, this.textDirection, this.verticalDirection = VerticalDirection.down, From 2979bedcf78882a7647c15c83eaddbdbdaeb570e Mon Sep 17 00:00:00 2001 From: thanhle1547 Date: Fri, 12 Sep 2025 22:54:42 +0700 Subject: [PATCH 08/10] rewrite wrapped items implementation --- lib/src/rendering/overflow_view.dart | 474 +++++++++++---------------- 1 file changed, 196 insertions(+), 278 deletions(-) diff --git a/lib/src/rendering/overflow_view.dart b/lib/src/rendering/overflow_view.dart index 5ce5313..08f8e5e 100644 --- a/lib/src/rendering/overflow_view.dart +++ b/lib/src/rendering/overflow_view.dart @@ -7,7 +7,6 @@ import 'dart:math' as math; class OverflowViewParentData extends ContainerBoxParentData { int _runIndex = 0; bool? offstage; - bool _isLastElement = false; } class _RunMetrics { @@ -713,348 +712,272 @@ class RenderOverflowView extends RenderBox final double spacing = this.spacing; final double runSpacing = this.runSpacing; + final int? maxItemPerRun = this.maxItemPerRun; final List<_RunMetrics> runMetrics = <_RunMetrics>[]; double mainAxisExtent = 0.0; double crossAxisExtent = 0.0; double currentRunMainAxisExtent = 0.0; double currentRunCrossAxisExtent = 0.0; - int childCount = 0; - int runCount = 0; - int itemCountPerRun = 0; + int currentRunChildCount = 0; + int runIndex = 0; bool showOverflowIndicator = false; Offset currentChildOffset = Offset.zero; - OverflowViewParentData? previousChildParentData; - RenderBox? child = firstChild; - while (child != lastChild) { - child!.layout(childConstraints, parentUsesSize: true); + child?.layout(childConstraints, parentUsesSize: true); - final OverflowViewParentData childParentData = - child.parentData as OverflowViewParentData; + while (child != lastChild) { // the last child is the Overflow indicator, which will be laid out later + final OverflowViewParentData childParentData = child!.parentData as OverflowViewParentData; childParentData.offset = currentChildOffset; + // mark the child is not hidden, which means visible + childParentData.offstage = false; + childParentData._runIndex = runIndex; + + currentRunChildCount++; + unRenderedChildCount--; + + renderBoxes.add(child); + + // Calculate the extent of the current run (row) in main axis and cross axis final double childMainAxisExtent = _getMainAxisExtent(child.size); final double childCrossAxisExtent = _getCrossAxisExtent(child.size); - final double childMainAxisStride = spacing + childMainAxisExtent; + if (currentRunChildCount > 1) { + currentRunMainAxisExtent += spacing; + } + currentRunMainAxisExtent += childMainAxisExtent; - double childCrossAxisStride = currentChildOffset.dy; + currentRunCrossAxisExtent = math.max( + currentRunCrossAxisExtent, + childCrossAxisExtent, + ); - if (childCount > 0 && - (itemCountPerRun == maxItemPerRun || - currentRunMainAxisExtent + childMainAxisStride > mainAxisLimit)) { - mainAxisExtent = math.max(mainAxisExtent, currentRunMainAxisExtent); - crossAxisExtent += currentRunCrossAxisExtent; - if (runMetrics.isNotEmpty) { - crossAxisExtent += runSpacing; - } + // Prepare [Offset.dy] of the next child + double nextSiblingVerticalDistance = currentChildOffset.dy; - childCrossAxisStride = crossAxisExtent; + // Layout next child to get its extent in main axis, + // to prepare for the next run (row) + final RenderBox? nextSibling = childParentData.nextSibling; + double nextSiblingMainAxisExtent = 0.0; + if (nextSibling != null && nextSibling != lastChild) { + // The next child isn't the overflow indicator, + // which (as the last child) will be laid out later. + nextSibling.layout(childConstraints, parentUsesSize: true); + nextSiblingMainAxisExtent = _getMainAxisExtent(nextSibling.size); + } + + if ((maxItemPerRun != null && currentRunChildCount + 1 > maxItemPerRun) || + currentRunMainAxisExtent + nextSiblingMainAxisExtent > mainAxisLimit) { + // Save information of current run runMetrics.add(_RunMetrics( mainAxisExtent: currentRunMainAxisExtent, crossAxisExtent: currentRunCrossAxisExtent, - childCount: childCount, + childCount: currentRunChildCount, )); - runCount = runMetrics.length; + // Update the extent of this widget in main axis and cross axis + mainAxisExtent = math.max(mainAxisExtent, currentRunMainAxisExtent); - if (previousChildParentData != null) { - previousChildParentData._isLastElement = true; + if (runMetrics.length > 1) { + crossAxisExtent += runSpacing; } - childCount = 0; - itemCountPerRun = 0; + crossAxisExtent += currentRunCrossAxisExtent; + + // Update [Offset.dy] of the next child + nextSiblingVerticalDistance = crossAxisExtent + runSpacing; - if (runCount == maxRun || - crossAxisExtent + childCrossAxisExtent + runSpacing > - crossAxisLimit) { - // We have no room to paint any further child. - showOverflowIndicator = true; + // If we reach the maximum number of runs + // or the maximum extent in the cross axis, + // we need to stop laying out the remaining children + // and prepare to layout the Overflow indicator. + + if (runMetrics.length == maxRun) { + // When the maxRun == 1 and maxItemPerRun == 1, + // we don't need to show the Overflow indicator + showOverflowIndicator = nextSibling != lastChild; break; } + if (nextSibling != null && nextSibling != lastChild) { + // The next child isn't the overflow indicator, + // which (as the last child) will be laid out later. + final double nextSiblingCrossAxisExtent = _getCrossAxisExtent(nextSibling.size); + + if (crossAxisExtent + runSpacing + nextSiblingCrossAxisExtent > crossAxisLimit) { + // We have no room to paint any further child. + showOverflowIndicator = true; + break; + } + } + + runIndex += 1; + + // Reset current run information for the next run calculation + currentRunMainAxisExtent = 0.0; currentRunCrossAxisExtent = 0.0; + currentRunChildCount = 0; } - currentChildOffset = Offset(childMainAxisStride, childCrossAxisStride); + // Go to the next child - currentRunMainAxisExtent += childMainAxisExtent; - - if (childCount > 0) { - currentRunMainAxisExtent += spacing; - } + final double nextSiblingHorizontalDistance = spacing + currentRunMainAxisExtent; + final nextChildOffset = Offset(nextSiblingHorizontalDistance, nextSiblingVerticalDistance); + currentChildOffset = nextChildOffset; - currentRunCrossAxisExtent = - math.max(currentRunCrossAxisExtent, childCrossAxisExtent); - childCount += 1; - itemCountPerRun += 1; - unRenderedChildCount--; + child = nextSibling; + } - childParentData.offstage = false; - childParentData._runIndex = runMetrics.length; - renderBoxes.add(child); + if (runMetrics.isEmpty && childCount > 1) { // why > 1, because one for the Overflow indicator + assert(!showOverflowIndicator); - child = childParentData.nextSibling!; + // This is when the maxRun == 1 - previousChildParentData = childParentData; - } + mainAxisExtent = currentRunMainAxisExtent; + crossAxisExtent = currentRunCrossAxisExtent; - if (childCount != 0) { - mainAxisExtent = math.max(mainAxisExtent, currentRunMainAxisExtent); - crossAxisExtent += currentRunCrossAxisExtent; - if (runMetrics.isNotEmpty) { - crossAxisExtent += runSpacing; - } runMetrics.add(_RunMetrics( mainAxisExtent: currentRunMainAxisExtent, crossAxisExtent: currentRunCrossAxisExtent, - childCount: childCount, + childCount: currentRunChildCount, )); } - runCount = runMetrics.length; - assert(runCount > 0); + // Now, if showOverflowIndicator == true, + // - runIndex is the last index of all the runs + assert(!showOverflowIndicator || runIndex == runMetrics.length - 1); + // - currentChildOffset is the offset of the last visible child, + // so when we need to show the Overflow indicator, + // we can use this offset to replace that child + // (the last visible child will be mark as invisible). + assert(() { + if (!showOverflowIndicator) return true; + + if (renderBoxes.isEmpty) return false; - double overflowIndicatorMainAxisLimit = currentRunMainAxisExtent == 0.0 - ? 0.0 - : mainAxisLimit - currentRunMainAxisExtent; + final lastVisibleChild = renderBoxes.last; + final OverflowViewParentData childParentData = lastVisibleChild.parentData as OverflowViewParentData; + + return childParentData.offset == currentChildOffset; + }()); if (showOverflowIndicator) { - // We didn't layout all the children. - final RenderBox overflowIndicator = lastChild!; - final BoxValueConstraints overflowIndicatorConstraints = - BoxValueConstraints( + // About to remove the last visible child + unRenderedChildCount++; + // to replace it with the Overflow indicator. + + final RenderBox overflowIndicator = lastChild!; // this is the Overflow indicator. + final BoxValueConstraints overflowIndicatorConstraints = BoxValueConstraints( value: unRenderedChildCount, constraints: childConstraints, ); - overflowIndicator.layout( - overflowIndicatorConstraints, - parentUsesSize: true, - ); - double overflowIndicatorMainAxisExtent = - _getMainAxisExtent(overflowIndicator.size); - double overflowIndicatorCrossAxisExtent = - _getCrossAxisExtent(overflowIndicator.size); + overflowIndicator.layout(overflowIndicatorConstraints, parentUsesSize: true); + + double overflowIndicatorMainAxisExtent = _getMainAxisExtent(overflowIndicator.size); + double overflowIndicatorCrossAxisExtent = _getCrossAxisExtent(overflowIndicator.size); Offset overflowIndicatorOffset = currentChildOffset; - int overflowIndicatorRunIndex = runMetrics.length - 1; - bool isLastElement = false; - double overflowIndicatorMainAxisStride = - spacing + overflowIndicatorMainAxisExtent; + // Remove the last run metrics to make changes, + // because the metrics are immutable. + _RunMetrics lastMetrics = runMetrics.removeLast(); + double lastRunMainAxisExtent = lastMetrics.mainAxisExtent; + int lastMetricsChildCount = lastMetrics.childCount; + + while(lastMetricsChildCount > 0) { + // Remove the last visible child + final RenderBox removedChild = renderBoxes.removeLast(); + final OverflowViewParentData removedChildParentData = removedChild.parentData as OverflowViewParentData; + removedChildParentData.offstage = true; + lastMetricsChildCount--; + + // Re-calculate the extent in the main axis of the last run + final double removedChildMainAxisExtent = _getMainAxisExtent(removedChild.size); + + lastRunMainAxisExtent -= removedChildMainAxisExtent; + if (lastMetricsChildCount > 0) { + lastRunMainAxisExtent -= spacing; + } + + // Use the [Offset.dx] of removed child as the [Offset.dx] of the indicator + final double removedChildHorizontalDistance = removedChildParentData.offset.dx; - if (overflowIndicatorMainAxisStride + runMetrics.last.mainAxisExtent < - mainAxisLimit && - (maxItemPerRun == null || - runMetrics.last.childCount < maxItemPerRun!)) { + // Save offset of the indicator overflowIndicatorOffset = Offset( - overflowIndicatorMainAxisStride, - math.max( - runMetrics.last.crossAxisExtent, - overflowIndicatorCrossAxisExtent, - ), - ); - isLastElement = true; - - _RunMetrics oldMetrics = runMetrics.removeLast(); - runMetrics.add( - _RunMetrics( - mainAxisExtent: oldMetrics.mainAxisExtent, - crossAxisExtent: math.max( - oldMetrics.crossAxisExtent, - overflowIndicatorOffset.dy, - ), - childCount: oldMetrics.childCount + 1, - ), + removedChildHorizontalDistance, + removedChildParentData.offset.dy, ); - } else { - // We need to remove the children that prevent the overflowIndicator - // to paint. - while (overflowIndicatorConstraints.value != unRenderedChildCount || - runMetrics.last.childCount == maxItemPerRun || - (overflowIndicatorMainAxisStride > overflowIndicatorMainAxisLimit && - renderBoxes.isNotEmpty)) { - if (overflowIndicatorConstraints.value.toString().length != - unRenderedChildCount.toString().length) { - // The number of unrendered child drastically changed - // (like from 9 to 10), we have to layout the indicator another time. - overflowIndicator.layout( - BoxValueConstraints( - value: unRenderedChildCount, - constraints: childConstraints, - ), - parentUsesSize: true, - ); - - overflowIndicatorMainAxisExtent = - _getMainAxisExtent(overflowIndicator.size); - overflowIndicatorCrossAxisExtent = - _getCrossAxisExtent(overflowIndicator.size); - } else if (overflowIndicatorMainAxisStride <= - overflowIndicatorMainAxisLimit && - (maxItemPerRun == null || - runMetrics.last.childCount < maxItemPerRun!)) { - break; - } - final RenderBox child = renderBoxes.removeLast(); - final OverflowViewParentData childParentData = - child.parentData as OverflowViewParentData; - childParentData.offstage = true; - - final _RunMetrics oldMetrics = - runMetrics.removeAt(childParentData._runIndex); - final int runMetricsChildCount = oldMetrics.childCount; - - unRenderedChildCount++; - - final double childMainAxisStride = childParentData.offset.dx; - overflowIndicatorOffset = Offset( - childMainAxisStride, - childParentData.offset.dy, - ); - - if (childParentData._isLastElement) { - if (runMetricsChildCount > 1) { - overflowIndicatorMainAxisLimit -= spacing; - } - overflowIndicatorMainAxisLimit = oldMetrics.isSingleChild - ? mainAxisLimit - : mainAxisLimit - childMainAxisStride; - overflowIndicatorMainAxisStride = overflowIndicatorMainAxisExtent; - - isLastElement = true; - } else { - overflowIndicatorMainAxisLimit += childMainAxisStride; - if (runMetricsChildCount > 1) { - overflowIndicatorMainAxisLimit += spacing; - } - overflowIndicatorRunIndex = childParentData._runIndex; - overflowIndicatorMainAxisStride = - spacing + overflowIndicatorMainAxisExtent; - - isLastElement = false; - } + final double overflowIndicatorMainAxisLimit = mainAxisExtent - lastRunMainAxisExtent; - runMetrics.insert( - childParentData._runIndex, - oldMetrics.copyWith( - mainAxisExtent: oldMetrics.isSingleChild - ? overflowIndicatorMainAxisExtent - : oldMetrics.mainAxisExtent - childMainAxisStride, - childCount: oldMetrics.childCount - 1, - ), - ); - - final _RunMetrics refreshedMetrics = - runMetrics[childParentData._runIndex]; - if (refreshedMetrics.hasNoChild && childParentData._runIndex > 0) { - final _RunMetrics newMetrics = - runMetrics[childParentData._runIndex - 1]; - - if (maxItemPerRun == null && - overflowIndicatorMainAxisStride + newMetrics.mainAxisExtent < - mainAxisLimit) { - // We can bring the overflowIndicator to the previous run - // - // ┌────────┐ ┌────────┐ - // │╔══╗┌──┐│ │╔══╗╔══╗│ - // │╚══╝╘═↑╛│ ==> │╚══╝╚══╝│ - // │╔══╗ _↑ │ ==> │ │ - // │╚══╝ │ │ │ - // └────────┘ └────────┘ - overflowIndicatorOffset = Offset( - newMetrics.mainAxisExtent + spacing, - refreshedMetrics.crossAxisExtent - newMetrics.crossAxisExtent, - ); - isLastElement = true; - - runMetrics.removeLast(); - - _RunMetrics oldMetrics = runMetrics.removeLast(); - runMetrics.add( - oldMetrics.copyWith( - childCount: oldMetrics.childCount + 1, - ), - ); - - overflowIndicatorMainAxisLimit = - mainAxisLimit - oldMetrics.mainAxisExtent; - overflowIndicatorRunIndex = runMetrics.length - 1; - overflowIndicatorMainAxisStride = - spacing + overflowIndicatorMainAxisExtent; - } - } + if (overflowIndicatorMainAxisLimit >= overflowIndicatorMainAxisExtent) { + break; } - } - if (overflowIndicatorMainAxisExtent > overflowIndicatorMainAxisLimit) { - // We cannot paint any child because there is not enough space. - _hasVisualOverflow = true; - } + // The indicator need more space to show... - if (overflowIndicatorConstraints.value != unRenderedChildCount) { - // The number of unrendered child changed, we have to layout the - // indicator another time. - overflowIndicator.layout( - BoxValueConstraints( - value: unRenderedChildCount, - constraints: childConstraints, - ), - parentUsesSize: true, - ); - } + if (lastMetrics.hasNoChild) { + // but there are no child. + // We cannot paint any child because there is not enough space. + // Mark the visual is overflow. + _hasVisualOverflow = true; + break; + } - final OverflowViewParentData overflowIndicatorParentData = - overflowIndicator.parentData as OverflowViewParentData; - overflowIndicatorParentData.offset = overflowIndicatorOffset; - overflowIndicatorParentData.offstage = false; - overflowIndicatorParentData._isLastElement = isLastElement; - overflowIndicatorParentData._runIndex = overflowIndicatorRunIndex; - _RunMetrics oldMetrics = runMetrics.removeAt(overflowIndicatorRunIndex); - runMetrics.insert( - overflowIndicatorRunIndex, - _RunMetrics( - mainAxisExtent: oldMetrics.hasNoChild - ? oldMetrics.mainAxisExtent - : oldMetrics.mainAxisExtent + overflowIndicatorMainAxisStride, - crossAxisExtent: math.max( - overflowIndicatorCrossAxisExtent, - oldMetrics.crossAxisExtent, - ), - childCount: oldMetrics.childCount + 1, - ), - ); + // Prepare to remove the next child + unRenderedChildCount++; - currentChildOffset = overflowIndicatorOffset; - mainAxisExtent = math.max(mainAxisExtent, runMetrics.last.mainAxisExtent); - } else { - // We need to layout the overflowIndicator because we may have already - // laid it out with parentUsesSize: true before. - // When unmounting a _LayoutBuilderElement, it calls markNeedsLayout - // a last time, and can cause error. - lastChild?.layout( - BoxValueConstraints( + // Relayout the indicator with the new number of hidden children + final BoxValueConstraints overflowIndicatorConstraints = BoxValueConstraints( value: unRenderedChildCount, constraints: childConstraints, + ); + + overflowIndicator.layout(overflowIndicatorConstraints, parentUsesSize: true); + + overflowIndicatorMainAxisExtent = _getMainAxisExtent(overflowIndicator.size); + overflowIndicatorCrossAxisExtent = _getCrossAxisExtent(overflowIndicator.size); + } + + if (lastMetricsChildCount > 0) { + lastRunMainAxisExtent += spacing; + } + lastRunMainAxisExtent += overflowIndicatorMainAxisExtent; + + lastMetrics = lastMetrics.copyWith( + mainAxisExtent: lastRunMainAxisExtent, + crossAxisExtent: math.max( + lastMetrics.crossAxisExtent, + overflowIndicatorCrossAxisExtent, ), + childCount: lastMetricsChildCount + 1, // 1 is for the indicator ); + + runMetrics.add(lastMetrics); + + final OverflowViewParentData overflowIndicatorParentData = overflowIndicator.parentData as OverflowViewParentData; + overflowIndicatorParentData.offset = overflowIndicatorOffset; + overflowIndicatorParentData.offstage = false; + overflowIndicatorParentData._runIndex = runMetrics.length - 1; + + mainAxisExtent = math.max(mainAxisExtent, lastMetrics.mainAxisExtent); + crossAxisExtent = math.max(crossAxisExtent, lastMetrics.crossAxisExtent); } - runCount = runMetrics.length; - assert(runCount > 0); + if (crossAxisExtent > crossAxisLimit) { + _hasVisualOverflow = true; + } _performWrapLayout( flipMainAxis: flipMainAxis, flipCrossAxis: flipCrossAxis, mainAxisExtent: mainAxisExtent, crossAxisExtent: crossAxisExtent, - runCount: runCount, runMetrics: runMetrics, ); } @@ -1064,9 +987,11 @@ class RenderOverflowView extends RenderBox required bool flipCrossAxis, required double mainAxisExtent, required double crossAxisExtent, - required int runCount, required List<_RunMetrics> runMetrics, }) { + final int runCount = runMetrics.length; + assert(runCount > 0); + double containerMainAxisExtent = 0.0; double containerCrossAxisExtent = 0.0; @@ -1086,8 +1011,7 @@ class RenderOverflowView extends RenderBox _hasVisualOverflow = containerMainAxisExtent < mainAxisExtent || containerCrossAxisExtent < crossAxisExtent; - final double crossAxisFreeSpace = - math.max(0.0, containerCrossAxisExtent - crossAxisExtent); + final double crossAxisFreeSpace = math.max(0.0, containerCrossAxisExtent - crossAxisExtent); double runLeadingSpace = 0.0; double runBetweenSpace = 0.0; switch (runAlignment) { @@ -1100,8 +1024,7 @@ class RenderOverflowView extends RenderBox runLeadingSpace = crossAxisFreeSpace / 2.0; break; case WrapAlignment.spaceBetween: - runBetweenSpace = - runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0; + runBetweenSpace = runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0; break; case WrapAlignment.spaceAround: runBetweenSpace = crossAxisFreeSpace / runCount; @@ -1125,8 +1048,7 @@ class RenderOverflowView extends RenderBox final double runCrossAxisExtent = metrics.crossAxisExtent; final int childCount = metrics.childCount; - final double mainAxisFreeSpace = - math.max(0.0, containerMainAxisExtent - runMainAxisExtent); + final double mainAxisFreeSpace = math.max(0.0, containerMainAxisExtent - runMainAxisExtent); double childLeadingSpace = 0.0; double childBetweenSpace = 0.0; @@ -1140,8 +1062,7 @@ class RenderOverflowView extends RenderBox childLeadingSpace = mainAxisFreeSpace / 2.0; break; case WrapAlignment.spaceBetween: - childBetweenSpace = - childCount > 1 ? mainAxisFreeSpace / (childCount - 1) : 0.0; + childBetweenSpace = childCount > 1 ? mainAxisFreeSpace / (childCount - 1) : 0.0; break; case WrapAlignment.spaceAround: childBetweenSpace = mainAxisFreeSpace / childCount; @@ -1161,11 +1082,9 @@ class RenderOverflowView extends RenderBox if (flipCrossAxis) crossAxisOffset -= runCrossAxisExtent; while (child != null) { - final OverflowViewParentData childParentData = - child.parentData! as OverflowViewParentData; + final OverflowViewParentData childParentData = child.parentData! as OverflowViewParentData; - if (childParentData._runIndex != i && - childParentData.offstage != null) { + if (childParentData._runIndex != i && childParentData.offstage != null) { break; } @@ -1256,8 +1175,7 @@ class RenderOverflowView extends RenderBox visitOnlyOnStageChildren(visitor); } - final LayerHandle _clipRectLayer = - LayerHandle(); + final LayerHandle _clipRectLayer = LayerHandle(); @override void paint(PaintingContext context, Offset offset) { From eec89fb665dcf69d6a0ba22754146c426dd9f185 Mon Sep 17 00:00:00 2001 From: thanhle1547 Date: Sat, 13 Sep 2025 00:15:42 +0700 Subject: [PATCH 09/10] rename `RenderOverflowView._performWrapLayout` to `_positionChildrenInWrapLayout` --- lib/src/rendering/overflow_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/rendering/overflow_view.dart b/lib/src/rendering/overflow_view.dart index 08f8e5e..0445f47 100644 --- a/lib/src/rendering/overflow_view.dart +++ b/lib/src/rendering/overflow_view.dart @@ -973,7 +973,7 @@ class RenderOverflowView extends RenderBox _hasVisualOverflow = true; } - _performWrapLayout( + _positionChildrenInWrapLayout( flipMainAxis: flipMainAxis, flipCrossAxis: flipCrossAxis, mainAxisExtent: mainAxisExtent, @@ -982,7 +982,7 @@ class RenderOverflowView extends RenderBox ); } - void _performWrapLayout({ + void _positionChildrenInWrapLayout({ required bool flipMainAxis, required bool flipCrossAxis, required double mainAxisExtent, From 6cf2e3b00748f4c08b07f72a649b43fc9b002cf2 Mon Sep 17 00:00:00 2001 From: thanhle1547 Date: Sat, 13 Sep 2025 00:16:43 +0700 Subject: [PATCH 10/10] remove unnecessary set `_hasVisualOverflow` to true --- lib/src/rendering/overflow_view.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/src/rendering/overflow_view.dart b/lib/src/rendering/overflow_view.dart index 0445f47..fdf56f0 100644 --- a/lib/src/rendering/overflow_view.dart +++ b/lib/src/rendering/overflow_view.dart @@ -923,9 +923,6 @@ class RenderOverflowView extends RenderBox if (lastMetrics.hasNoChild) { // but there are no child. - // We cannot paint any child because there is not enough space. - // Mark the visual is overflow. - _hasVisualOverflow = true; break; } @@ -969,10 +966,6 @@ class RenderOverflowView extends RenderBox crossAxisExtent = math.max(crossAxisExtent, lastMetrics.crossAxisExtent); } - if (crossAxisExtent > crossAxisLimit) { - _hasVisualOverflow = true; - } - _positionChildrenInWrapLayout( flipMainAxis: flipMainAxis, flipCrossAxis: flipCrossAxis,