diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb205e..1f7be00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0 +### Enhancement +* [Wrapped items](https://github.com/letsar/overflow_view/issues/2) + ## 0.5.0 ### Changed * Update Flutter constraints. diff --git a/example/lib/main.dart b/example/lib/main.dart index 4e99f40..49934c8 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( diff --git a/example/pubspec.lock b/example/pubspec.lock index e6d4f72..159b2b3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,50 +5,58 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.13.0" + version: "2.8.2" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.0" characters: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.3.1" clock: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.0" collection: dependency: transitive description: name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" source: hosted - version: "1.19.1" + version: "0.1.3" fake_async: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.3.3" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -59,146 +67,102 @@ packages: description: flutter source: sdk version: "0.0.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" - url: "https://pub.dev" - source: hosted - version: "10.0.9" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.dev" - source: hosted - version: "3.0.9" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.12.17" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.11.1" + version: "0.1.4" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.16.0" + version: "1.7.0" overflow_view: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.5.0" + version: "0.4.0" path: dependency: transitive description: name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.9.1" + version: "1.8.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.0" + version: "0.0.99" source_span: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.10.1" + version: "1.8.2" stack_trace: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.12.1" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.4.1" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "1.2.0" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.7.4" + version: "0.4.9" value_layout_builder: dependency: transitive description: name: value_layout_builder - sha256: ab4b7d98bac8cefeb9713154d43ee0477490183f5aa23bb4ffa5103d9bbf6275 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.5.0" + version: "0.3.1" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 - url: "https://pub.dev" - source: hosted - version: "15.0.0" + version: "2.1.2" sdks: - dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.32.0" + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=1.24.0-10.2.pre" diff --git a/lib/src/rendering/overflow_view.dart b/lib/src/rendering/overflow_view.dart index 9d45af0..fdf56f0 100644 --- a/lib/src/rendering/overflow_view.dart +++ b/lib/src/rendering/overflow_view.dart @@ -5,12 +5,58 @@ import 'dart:math' as math; /// Parent data for use with [RenderOverflowView]. class OverflowViewParentData extends ContainerBoxParentData { + int _runIndex = 0; bool? offstage; } +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 +66,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 == null || 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 +153,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 == null || 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 +317,138 @@ 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: - return child.size.height; + 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 child.size.width; + return computeDryLayout(BoxConstraints(maxHeight: height)).width; } } - double _getMainSize(RenderBox child) { - switch (_direction) { + @override + double computeMaxIntrinsicWidth(double height) { + switch (direction) { case Axis.horizontal: - return child.size.width; + 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.height; + return computeDryLayout(BoxConstraints(maxHeight: height)).width; } } - bool _hasOverflow = false; + @override + double computeMinIntrinsicHeight(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 = + math.max(height, child.getMinIntrinsicHeight(double.infinity)); + child = childAfter(child); + } + return height; + } + } + + @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(firstChild != null); + assert(_debugHasNecessaryDirections); + _hasVisualOverflow = false; + + final BoxConstraints constraints = this.constraints; + + if (childCount == 1) { + size = constraints.smallest; + return; + } + 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,16 +461,16 @@ class RenderOverflowView extends RenderBox } void performFixedLayout() { - RenderBox child = firstChild!; 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); - 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 +560,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 +594,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 +603,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 +612,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 +651,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 +683,478 @@ class RenderOverflowView extends RenderBox size = constraints.constrain(idealSize); } + void performWrapLayout() { + 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 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 currentRunChildCount = 0; + int runIndex = 0; + + bool showOverflowIndicator = false; + Offset currentChildOffset = Offset.zero; + + RenderBox? child = firstChild; + child?.layout(childConstraints, parentUsesSize: true); + + 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); + + if (currentRunChildCount > 1) { + currentRunMainAxisExtent += spacing; + } + currentRunMainAxisExtent += childMainAxisExtent; + + currentRunCrossAxisExtent = math.max( + currentRunCrossAxisExtent, + childCrossAxisExtent, + ); + + // Prepare [Offset.dy] of the next child + double nextSiblingVerticalDistance = currentChildOffset.dy; + + // 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: currentRunChildCount, + )); + + // Update the extent of this widget in main axis and cross axis + mainAxisExtent = math.max(mainAxisExtent, currentRunMainAxisExtent); + + if (runMetrics.length > 1) { + crossAxisExtent += runSpacing; + } + + crossAxisExtent += currentRunCrossAxisExtent; + + // Update [Offset.dy] of the next child + nextSiblingVerticalDistance = crossAxisExtent + runSpacing; + + // 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; + } + + // Go to the next child + + final double nextSiblingHorizontalDistance = spacing + currentRunMainAxisExtent; + final nextChildOffset = Offset(nextSiblingHorizontalDistance, nextSiblingVerticalDistance); + currentChildOffset = nextChildOffset; + + child = nextSibling; + } + + if (runMetrics.isEmpty && childCount > 1) { // why > 1, because one for the Overflow indicator + assert(!showOverflowIndicator); + + // This is when the maxRun == 1 + + mainAxisExtent = currentRunMainAxisExtent; + crossAxisExtent = currentRunCrossAxisExtent; + + runMetrics.add(_RunMetrics( + mainAxisExtent: currentRunMainAxisExtent, + crossAxisExtent: currentRunCrossAxisExtent, + childCount: currentRunChildCount, + )); + } + + // 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; + + final lastVisibleChild = renderBoxes.last; + final OverflowViewParentData childParentData = lastVisibleChild.parentData as OverflowViewParentData; + + return childParentData.offset == currentChildOffset; + }()); + + if (showOverflowIndicator) { + // 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); + + Offset overflowIndicatorOffset = currentChildOffset; + + // 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; + + // Save offset of the indicator + overflowIndicatorOffset = Offset( + removedChildHorizontalDistance, + removedChildParentData.offset.dy, + ); + + final double overflowIndicatorMainAxisLimit = mainAxisExtent - lastRunMainAxisExtent; + + if (overflowIndicatorMainAxisLimit >= overflowIndicatorMainAxisExtent) { + break; + } + + // The indicator need more space to show... + + if (lastMetrics.hasNoChild) { + // but there are no child. + break; + } + + // Prepare to remove the next child + unRenderedChildCount++; + + // 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); + } + + _positionChildrenInWrapLayout( + flipMainAxis: flipMainAxis, + flipCrossAxis: flipCrossAxis, + mainAxisExtent: mainAxisExtent, + crossAxisExtent: crossAxisExtent, + runMetrics: runMetrics, + ); + } + + void _positionChildrenInWrapLayout({ + required bool flipMainAxis, + required bool flipCrossAxis, + required double mainAxisExtent, + required double crossAxisExtent, + required List<_RunMetrics> runMetrics, + }) { + final int runCount = runMetrics.length; + assert(runCount > 0); + + 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 +1168,37 @@ 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 +1206,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..0c203f5 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,69 @@ 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, + 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, + maxItemPerRun: maxItemPerRun, + 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, + 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 +140,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 +290,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 +310,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 { diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..77efde6 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,154 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + 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.7.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" + value_layout_builder: + dependency: "direct main" + description: + name: value_layout_builder + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=1.24.0-10.2.pre" diff --git a/pubspec.yaml b/pubspec.yaml index ca8ee6a..05bafaa 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.5.0 +version: 0.6.0 homepage: https://github.com/letsar/overflow_view environment: