diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index beb96e7..f9514ec 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -11,7 +11,9 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Flutter - uses: kuhnroyal/flutter-fvm-config-action/setup@v3 + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.22.1" - name: Get packages run: flutter pub get - name: Analyze diff --git a/.gitignore b/.gitignore index 8b0c0a4..96486fd 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,3 @@ migrate_working_dir/ .dart_tool/ .packages build/ - -# FVM Version Cache -.fvm/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 59d6856..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "example", - "cwd": "example", - "request": "launch", - "type": "dart" - }, - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f9fd97..8fbb77c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,3 @@ -## 4.0.0-beta.1 - -- Rewrite the Controller/Container logic to allow Flutter to handle the initial layout of all widgets, updating the rendered sizes in the controller after the first frame. This _shouldn't_ have any major impact to the API, but it does introduce the use of Timers, which could affect tests. -- Rename `ResizableController.sizes` to `ResizableController.pixels` to more clearly indicate its value. -- Store, expose, and utilize the current list of `ResizableSize` values in the controller. This enables the values to be used even after manually updating them. - - ## 3.0.3 - Reinstate the removed `ResizableControllerManager#setChildren` method. This method was removed because the method it targets on the controller was made public. However, the package version was incorrectly bumped since this could be a breaking change. This patch reinstates the method, fixing the breaking change, but adds a deprecation warning in favor of the public controller method. diff --git a/example/lib/screens/controller_listen/controller_listen_example_screen.dart b/example/lib/screens/controller_listen/controller_listen_example_screen.dart index 8e2cdcb..e3dee7e 100644 --- a/example/lib/screens/controller_listen/controller_listen_example_screen.dart +++ b/example/lib/screens/controller_listen/controller_listen_example_screen.dart @@ -24,7 +24,7 @@ class _ControllerListenExampleScreenState void initState() { super.initState(); controller.addListener(() { - final sizes = controller.pixels; + final sizes = controller.sizes; setState(() { leftWidth = sizes.first; rightWidth = sizes.last; diff --git a/example/pubspec.lock b/example/pubspec.lock index 6c2b1ca..784d123 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -84,7 +84,7 @@ packages: path: ".." relative: true source: path - version: "4.0.0-beta.1" + version: "3.0.3" flutter_test: dependency: "direct dev" description: flutter diff --git a/lib/flutter_resizable_container.dart b/lib/flutter_resizable_container.dart index ac1cfa3..14533f1 100644 --- a/lib/flutter_resizable_container.dart +++ b/lib/flutter_resizable_container.dart @@ -1,4 +1,4 @@ -library; +library flutter_resizable_container; export 'src/resizable_container.dart'; export 'src/resizable_controller.dart' show ResizableController; diff --git a/lib/src/resizable_container.dart b/lib/src/resizable_container.dart index 9e61829..89c0a7d 100644 --- a/lib/src/resizable_container.dart +++ b/lib/src/resizable_container.dart @@ -1,10 +1,10 @@ +import 'dart:async'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_resizable_container/flutter_resizable_container.dart'; import 'package:flutter_resizable_container/src/extensions/box_constraints_ext.dart'; -import 'package:flutter_resizable_container/src/extensions/iterable_ext.dart'; import 'package:flutter_resizable_container/src/resizable_container_divider.dart'; import 'package:flutter_resizable_container/src/resizable_controller.dart'; @@ -46,12 +46,13 @@ class _ResizableContainerState extends State { late final controller = widget.controller ?? ResizableController(); late final isDefaultController = widget.controller == null; late final manager = ResizableControllerManager(controller); - late var keys = _generateKeys(); + late List keys = List.generate( + widget.children.length, + (_) => GlobalKey(), + ); - List _generateKeys() => List.generate( - widget.children.length, - (_) => GlobalKey(), - ); + var initialized = false; + var initScheduled = false; @override void initState() { @@ -62,16 +63,15 @@ class _ResizableContainerState extends State { @override void didUpdateWidget(covariant ResizableContainer oldWidget) { - final didChildrenChange = !listEquals(oldWidget.children, widget.children); - final didDirectionChange = widget.direction != oldWidget.direction; - final hasChanges = didChildrenChange || didDirectionChange; - - if (didChildrenChange) { - controller.setChildren(widget.children); - } + final hasChanges = !listEquals(oldWidget.children, widget.children) + || oldWidget.direction != widget.direction; if (hasChanges) { - keys = _generateKeys(); + manager.updateChildren(widget.children); + keys = List.generate( + widget.children.length, + (_) => GlobalKey(), + ); } super.didUpdateWidget(oldWidget); @@ -96,62 +96,82 @@ class _ResizableContainerState extends State { return AnimatedBuilder( animation: controller, builder: (context, _) { - if (controller.needsLayout) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _readSizesAfterLayout(); - }); - - return PreLayout( - availableSpace: availableSpace, - children: widget.children, - direction: widget.direction, - divider: widget.divider, - keys: keys, - sizes: controller.sizes, - ); - } else { - return Flex( - crossAxisAlignment: CrossAxisAlignment.stretch, - direction: widget.direction, - children: [ - for (var i = 0; i < widget.children.length; i++) ...[ - Builder( - builder: (context) { - final child = widget.children[i].child; - - final height = _getChildSize( - index: i, - direction: Axis.vertical, - constraints: constraints, - ); + final hasFlexOrShrink = widget.children.any( + (child) => child.size.isShrink || child.size.isExpand, + ); - final width = _getChildSize( - index: i, - direction: Axis.horizontal, - constraints: constraints, + return Flex( + crossAxisAlignment: CrossAxisAlignment.stretch, + direction: widget.direction, + children: [ + for (var i = 0; i < widget.children.length; i++) ...[ + Builder( + builder: (context) { + final child = widget.children[i].child; + final size = widget.children[i].size; + final key = keys[i]; + + final scheduleInit = hasFlexOrShrink && !initScheduled; + final shrink = !initialized && size.isShrink; + final expand = !initialized && size.isExpand; + + if (scheduleInit) { + Timer.run(_sizeInit); + initScheduled = true; + } + + if (shrink) { + // Use UnconstrainedBox to allow the child to shrink + // to its minimum size. + return UnconstrainedBox( + key: key, + child: child, ); - - return SizedBox( - height: height, - width: width, + } + + if (expand) { + // Use Expanded to allow the child to expand to fill + // the available space, mediated by its "flex" value. + return Expanded( + key: key, + flex: size.value.toInt(), child: child, ); - }, - ), - if (i < widget.children.length - 1) ...[ - ResizableContainerDivider( - config: widget.divider, - direction: widget.direction, - onResizeUpdate: (delta) => manager.adjustChildSize( - index: i, - delta: delta, - ), + } + + final height = _getChildSize( + index: i, + direction: Axis.vertical, + constraints: constraints, + ); + + final width = _getChildSize( + index: i, + direction: Axis.horizontal, + constraints: constraints, + ); + + return SizedBox( + key: key, + height: height, + width: width, + child: child, + ); + }, + ), + if (i < widget.children.length - 1) ...[ + ResizableContainerDivider( + config: widget.divider, + direction: widget.direction, + onResizeUpdate: (delta) => manager.adjustChildSize( + index: i, + delta: delta, ), - ], + ), ], ], - ); - } + ], + ); }, ); }, @@ -174,11 +194,15 @@ class _ResizableContainerState extends State { if (direction != direction) { return constraints.maxForDirection(direction); } else { - return controller.pixels[index]; + var size = controller.sizes[index]; + final child = widget.children[index]; + size = min(size, child.maxSize ?? double.infinity); + size = max(size, child.minSize ?? 0); + return size; } } - void _readSizesAfterLayout() { + void _sizeInit() { final sizes = keys.map((key) { final size = _getRenderBoxSize(key); @@ -192,7 +216,8 @@ class _ResizableContainerState extends State { }; }); - manager.setRenderedSizes(sizes.toList()); + manager.setSizes(sizes.toList()); + initialized = true; } Size? _getRenderBoxSize(GlobalKey key) { @@ -200,107 +225,3 @@ class _ResizableContainerState extends State { return renderBox?.size; } } - -class PreLayout extends StatelessWidget { - const PreLayout({ - super.key, - required this.availableSpace, - required this.children, - required this.direction, - required this.divider, - required this.keys, - required this.sizes, - }); - - final double availableSpace; - final List children; - final Axis direction; - final ResizableDivider divider; - final List keys; - final List sizes; - - @override - Widget build(BuildContext context) { - final totalPixels = - sizes.where((size) => size.isPixels).sum((size) => size.value); - - return Flex( - crossAxisAlignment: CrossAxisAlignment.stretch, - direction: direction, - children: [ - for (var i = 0; i < children.length; i++) ...[ - Builder(builder: (context) { - final size = sizes[i]; - final value = size.value; - - if (size.isPixels) { - final constrained = _getConstrainedSize( - value: value, - minimum: children[i].minSize, - maximum: children[i].maxSize, - ); - - return SizedBox( - key: keys[i], - height: direction == Axis.horizontal ? null : constrained, - width: direction == Axis.horizontal ? constrained : null, - child: children[i].child, - ); - } - - if (size.isRatio) { - final size = (availableSpace - totalPixels) * value; - final constrained = _getConstrainedSize( - value: size, - minimum: children[i].minSize, - maximum: children[i].maxSize, - ); - - return SizedBox( - key: keys[i], - height: direction == Axis.horizontal ? null : constrained, - width: direction == Axis.horizontal ? constrained : null, - child: children[i].child, - ); - } - - if (size.isShrink) { - return UnconstrainedBox( - key: keys[i], - child: children[i].child, - ); - } - - return Expanded( - key: keys[i], - flex: value.toInt(), - child: children[i].child, - ); - }), - if (i < children.length - 1) ...[ - ResizableContainerDivider( - config: divider, - direction: direction, - onResizeUpdate: (_) {}, - ), - ], - ], - ], - ); - } - - double _getConstrainedSize({ - required double value, - required double? minimum, - required double? maximum, - }) { - if (minimum == null && maximum == null) { - return value; - } - - var adjustedSize = min(value, maximum ?? double.infinity); - adjustedSize = max(adjustedSize, 0); - - return adjustedSize; - } -} diff --git a/lib/src/resizable_controller.dart b/lib/src/resizable_controller.dart index 63e2be4..532aca6 100644 --- a/lib/src/resizable_controller.dart +++ b/lib/src/resizable_controller.dart @@ -1,59 +1,82 @@ -import "dart:async"; import "dart:collection"; import "dart:math"; import 'package:flutter/material.dart'; import "package:flutter_resizable_container/flutter_resizable_container.dart"; +import "package:flutter_resizable_container/src/extensions/iterable_ext.dart"; +import "package:flutter_resizable_container/src/resizable_size.dart"; /// A controller to provide a programmatic interface to a [ResizableContainer]. class ResizableController with ChangeNotifier { double _availableSpace = -1; - List _pixels = []; - List _sizes = const []; + List _sizes = []; List _children = const []; - bool _needsLayout = false; - - bool get needsLayout => _needsLayout; /// The size, in pixels, of each child. - UnmodifiableListView get pixels => UnmodifiableListView(_pixels); - - UnmodifiableListView get sizes => UnmodifiableListView(_sizes); + UnmodifiableListView get sizes => UnmodifiableListView(_sizes); /// A list of ratios (proportion of total available space taken) for each child. UnmodifiableListView get ratios { return UnmodifiableListView([ - for (final size in pixels) ...[ + for (final size in sizes) ...[ size / _availableSpace, ], ]); } - void setSizes(List sizes) { - if (sizes.length != _children.length) { - throw ArgumentError('Must contain a value for every child'); - } - - final totalPixels = sizes - .where((size) => size.isPixels) - .fold(0.0, (sum, curr) => sum + curr.value); - - if (totalPixels > _availableSpace) { + /// Programmatically set the sizes of the children. + /// + /// Each child must have a corresponding index in the [values] list. + /// The value at each index may be a pixel value, a ratio, or "expand". + /// + /// Sizes are allocated based on the following hierarchy: + /// 1. ResizeSize.pixels + /// 2. ResizeSize.ratio + /// 3. ResizeSize.expand + /// + /// * If the value is a ResizeSize.pixels, the child will be given that size + /// in logical pixels + /// * If the value is a ResizeSize.ratio, the child will be given that portion + /// of the remaining available space, after all ResizeSize.pixel values have + /// been allocated + /// * If the value is a ResizeSize.expand, the child will be given the + /// remaining available space, after all pixel and ratio values have been + /// allocated + /// * If there are multiple "expand" values, each child will be given an equal + /// portion of the remaining available space, after all pixel and ratio values + /// have been allocated + /// + /// For example, + /// + /// ```dart + /// controller.setSizes(const [ + /// ResizableSize.pixels(100), + /// ResizableSize.ratio(0.5), + /// ResizableSize.expand(), + /// ]); + /// ``` + /// + /// In this scenario: + /// * the first child will be given 100 logical pixels of space + /// * the second child will be given 50% of the remaining available space + /// * the third child will be given whatever remaining space is left + /// + /// This method throws an `ArgumentError` in any of the following scenarios: + /// * The length of [values] is different from the length of [children] + /// * The total amount of pixels is greater than the total available space + /// * The sum of all ratio values exceeds 1.0 + void setSizes(List values) { + if (values.length != _children.length) { throw ArgumentError( - 'Total pixels must be less than or equal to available space', + 'Must contain a value for every child. Children: ${_children.length}, Sizes: ${values.length}', ); } - final totalRatio = sizes - .where((size) => size.isRatio) - .fold(0.0, (sum, curr) => sum + curr.value); - - if (totalRatio > 1.0) { - throw ArgumentError('Total ratio must be less than or equal to 1.0'); - } + _sizes = _mapSizesToAvailableSpace( + resizableSizes: values, + availableSpace: _availableSpace, + ); - _sizes = sizes; - _needsLayout = true; notifyListeners(); } @@ -65,216 +88,126 @@ class ResizableController with ChangeNotifier { ? _getAdjustedReducingDelta(index: index, delta: delta) : _getAdjustedIncreasingDelta(index: index, delta: delta); - _pixels[index] += adjustedDelta; - _pixels[index + 1] -= adjustedDelta; - notifyListeners(); - } - - void setChildren(List children) { - _children = children; - _sizes = children.map((child) => child.size).toList(); - _pixels = List.filled(children.length, 0); - _needsLayout = true; + _sizes[index] += adjustedDelta; + _sizes[index + 1] -= adjustedDelta; notifyListeners(); } - void _setRenderedSizes(List pixels) { - _pixels = pixels; - _needsLayout = false; - Timer.run(notifyListeners); - } - void _setAvailableSpace(double availableSpace) { - if (_availableSpace == -1) { - _needsLayout = true; - _availableSpace = availableSpace; - return; - } - if (availableSpace == _availableSpace) { return; } - // Adjust the sizes of all children based on the new available space. - // - // Prioritize adjusting "expand" children first. Any remaining change in - // available space (if the "expand" children have reached 0 or a size - // constraint) should be uniformly distributed among the remaining - // non-shrink children, taking into account their minimum & maximum size - // constraints. - final delta = _getDelta(availableSpace); - - if (delta == 0.0) { + if (_availableSpace == -1) { + // Initialize the child sizes and available space, but do not notify + // listeners; this step only occurs during the initial build, so we do + // not need to trigger another build + _initializeChildSizes(availableSpace); _availableSpace = availableSpace; - return; - } - - final distributed = _distributeDelta( - delta: delta, - sizes: _pixels, - ); - - for (var i = 0; i < sizes.length; i++) { - _pixels[i] += distributed[i]; + } else { + // Update the child sizes to the new space and notify listeners + _updateChildSizes(availableSpace); + _availableSpace = availableSpace; + notifyListeners(); } - - _availableSpace = availableSpace; } - double _getDelta(double availableSpace) { - var delta = availableSpace - _availableSpace; - - if (delta == 0.0) { - return 0.0; - } - - if (delta > 0) { - final minimumNecessarySize = _getMinimumNecessarySize(); - - if (minimumNecessarySize >= availableSpace) { - return 0.0; - } + void setChildren(List children) { + _children = children; + } - delta = min(delta, availableSpace - minimumNecessarySize); - } + void _updateChildren(List children) { + _children = children; + _initializeChildSizes(_availableSpace); + } - return delta; + void _initializeChildSizes(double availableSpace) { + _sizes = _getInitialChildSizes(availableSpace); } - double _getMinimumNecessarySize() { - final minimums = _children.map((child) => child.minSize ?? 0.0).toList(); - return minimums.fold(0.0, (sum, curr) => sum + curr); + List _getInitialChildSizes(double availableSpace) { + return _mapSizesToAvailableSpace( + resizableSizes: _children.map((child) => child.size), + availableSpace: availableSpace, + ); } - List _distributeDelta({ - required double delta, - required List sizes, + List _mapSizesToAvailableSpace({ + required Iterable resizableSizes, + required double availableSpace, }) { - final indices = List.generate(_children.length, (i) => i); - final changeableIndices = _getChangeableIndices(delta < 0 ? -1 : 1, sizes); + final totalPixels = resizableSizes.totalPixels; + final totalRatio = resizableSizes.totalRatio; + final flexCount = resizableSizes.flexCount; - if (changeableIndices.isEmpty) { - return List.filled(sizes.length, 0.0); + if (resizableSizes.totalPixels > availableSpace) { + throw ArgumentError('Size cannot exceed total available space.'); } - final changePerItem = delta / changeableIndices.length; - - final maximums = indices.map((i) { - if (changeableIndices.contains(i)) { - return _getAllowableChange(delta: delta, index: i, sizes: sizes); - } - - return 0.0; - }).toList(); - - final changes = indices.map((index) { - if (!changeableIndices.contains(index)) { - return 0.0; - } - - final max = maximums[index]; - - if (max.abs() < changePerItem.abs()) { - return max; - } - - return changePerItem; - }).toList(); + if (resizableSizes.totalRatio > 1.01) { + throw ArgumentError('Ratios cannot exceed 1.0'); + } - final changesSum = changes.fold(0.0, (sum, curr) => sum + curr); - final remainingChange = delta - changesSum; + final remainingSpace = availableSpace - totalPixels; + final ratioSpace = remainingSpace * totalRatio; + final totalFlexSpace = remainingSpace - ratioSpace; + final flexUnitSpace = totalFlexSpace / max(1, flexCount); + + final sizes = resizableSizes.map( + (size) => switch (size.type) { + SizeType.pixels => size.value, + SizeType.ratio => remainingSpace * size.value, + SizeType.expand => flexUnitSpace * size.value, + SizeType.shrink => 0.0, + }, + ); - if (remainingChange.abs() > 0) { - final adjustedSizes = indices.map( - (index) => sizes[index] + changes[index], - ); + return sizes.toList(); + } - final redistributed = _distributeDelta( - delta: remainingChange, - sizes: adjustedSizes.toList(), + void _updateChildSizes(double availableSpace) { + final flexCount = _children.map((child) => child.size).flexCount; + if (flexCount > 0) { + // If any children are set to expand, adjust them instead of any + // statically-sized children + _flexExpandableChildren( + availableSpace: availableSpace, + flexCount: flexCount, ); - - for (var i = 0; i < changes.length; i++) { - changes[i] += redistributed[i]; - } + } else { + // If no children are set to expand, then scale each child uniformly + _adjustChildrenUniformly(availableSpace); } - - return changes; } - double _getAllowableChange({ - required double delta, - required int index, - required List sizes, + void _flexExpandableChildren({ + required double availableSpace, + required int flexCount, }) { - final targetSize = sizes[index] + delta; + final delta = availableSpace - _availableSpace; + final deltaPerExpandable = delta / flexCount; - if (delta < 0) { - final minimumSize = _children[index].minSize ?? 0; - - if (targetSize <= minimumSize) { - return minimumSize - sizes[index]; + for (var i = 0; i < _children.length; i++) { + if (_children[i].size.isExpand) { + _sizes[i] += deltaPerExpandable * _children[i].size.value; } - - return delta; - } - - final maximumSize = _children[index].maxSize ?? double.infinity; - - if (targetSize >= maximumSize) { - return maximumSize - sizes[index]; } - - return delta; } - List _getChangeableIndices(int direction, List sizes) { - final indices = List.generate(_children.length, (i) => i); - final List changeableIndices = []; - - bool shouldAdd(index) { - final minSize = _children[index].minSize ?? 0.0; - final maxSize = _children[index].maxSize ?? double.infinity; - - if (direction < 0 && sizes[index] > minSize) { - return true; - } else if (direction > 0 && sizes[index] < maxSize) { - return true; - } else { - return false; - } + void _adjustChildrenUniformly(double availableSpace) { + for (var i = 0; i < _children.length; i++) { + final currentRatio = _sizes[i] / _availableSpace; + _sizes[i] = currentRatio * availableSpace; } - - for (final index in indices) { - if (!_children[index].size.isExpand) { - continue; - } - - if (shouldAdd(index)) { - changeableIndices.add(index); - } - } - - if (changeableIndices.isNotEmpty) { - return changeableIndices; - } - - for (final index in indices) { - if (shouldAdd(index)) { - changeableIndices.add(index); - } - } - - return changeableIndices; } double _getAdjustedReducingDelta({ required int index, required double delta, }) { - final currentSize = pixels[index]; + final currentSize = sizes[index]; final minCurrentSize = _children[index].minSize ?? 0; - final adjacentSize = pixels[index + 1]; + final adjacentSize = sizes[index + 1]; final maxAdjacentSize = _children[index + 1].maxSize ?? double.infinity; final maxCurrentDelta = currentSize - minCurrentSize; final maxAdjacentDelta = maxAdjacentSize - adjacentSize; @@ -291,9 +224,9 @@ class ResizableController with ChangeNotifier { required int index, required double delta, }) { - final currentSize = pixels[index]; + final currentSize = sizes[index]; final maxCurrentSize = _children[index].maxSize ?? double.infinity; - final adjacentSize = pixels[index + 1]; + final adjacentSize = sizes[index + 1]; final minAdjacentSize = _children[index + 1].minSize ?? 0; final maxAvailableSpace = min(maxCurrentSize, _availableSpace); final maxCurrentDelta = maxAvailableSpace - currentSize; @@ -306,6 +239,10 @@ class ResizableController with ChangeNotifier { return delta; } + + void _notify() { + notifyListeners(); + } } final class ResizableControllerManager { @@ -313,16 +250,34 @@ final class ResizableControllerManager { final ResizableController _controller; - void adjustChildSize({required int index, required double delta}) { - _controller._adjustChildSize(index: index, delta: delta); + void setAvailableSpace(double availableSpace) { + _controller._setAvailableSpace(availableSpace); } - void setRenderedSizes(List sizes) { - _controller._setRenderedSizes(sizes); + void updateChildren(List children) { + _controller._updateChildren(children); } - void setAvailableSpace(double availableSpace) { - _controller._setAvailableSpace(availableSpace); + @Deprecated( + 'This method is deprecated and will be removed in the next major version. Use ResizableController#setChildren instead.', + ) + void setChildren(List children) { + _controller.setChildren(children); + } + + void adjustChildSize({ + required int index, + required double delta, + }) { + _controller._adjustChildSize(index: index, delta: delta); + } + + void setSizes(List sizes) { + for (var i = 0; i < sizes.length; i++) { + _controller._sizes[i] = sizes[i]; + } + + _controller._notify(); } } diff --git a/pubspec.yaml b/pubspec.yaml index fd80b2c..bd575ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_resizable_container description: Add nestable, resizable containers to your Flutter app with ease. -version: 4.0.0-beta.1 +version: 3.0.3 homepage: https://github.com/andyhorn/flutter_resizable_container environment: @@ -16,4 +16,3 @@ dev_dependencies: sdk: flutter flutter_lints: ">=3.0.1 <6.0.0" mocktail: ^1.0.3 - fake_async: ^1.3.1 diff --git a/test/resizable_container_test.dart b/test/resizable_container_test.dart index 44ed2ce..3eb3eb3 100644 --- a/test/resizable_container_test.dart +++ b/test/resizable_container_test.dart @@ -55,8 +55,8 @@ void main() { final boxASize = tester.getSize(find.byKey(const Key('BoxA'))); final boxBSize = tester.getSize(find.byKey(const Key('BoxB'))); - expect(boxASize.width, equals(controller.pixels.first)); - expect(boxBSize.width, equals(controller.pixels.last)); + expect(boxASize.width, equals(controller.sizes.first)); + expect(boxBSize.width, equals(controller.sizes.last)); }); testWidgets('can resize using the controller', (tester) async { @@ -102,13 +102,12 @@ void main() { ResizableSize.ratio(0.6), ResizableSize.ratio(0.4), ]); - await tester.pump(); - await tester.pumpAndSettle(); final boxASize = tester.getSize(find.byKey(const Key('BoxA'))); final boxBSize = tester.getSize(find.byKey(const Key('BoxB'))); + // The sizes are not exactly their ratio because the divider width is 2.0 expect(boxASize, const Size(availableSpace * 0.6, 1000)); expect(boxBSize, const Size(availableSpace * 0.4, 1000)); }); @@ -253,7 +252,6 @@ void main() { await tester.tap(find.byKey(const Key('ToggleSwitch'))); await tester.pump(); - await tester.pumpAndSettle(); expect(find.byKey(const Key('ChildA')), findsOneWidget); expect(find.byKey(const Key('ChildB')), findsNothing); @@ -310,310 +308,8 @@ void main() { expect(boxASize.width, moreOrLessEquals(800, epsilon: 2)); expect(boxBSize.width, moreOrLessEquals(200, epsilon: 2)); - expect(controller.pixels.first, moreOrLessEquals(800, epsilon: 2)); - expect(controller.pixels.last, moreOrLessEquals(200, epsilon: 2)); - }); - - group('when changing the screen size', () { - group('with a shrink and expand child', () { - testWidgets( - 'delta is distributed to the expand', - (tester) async { - final controller = ResizableController(); - await tester.binding.setSurfaceSize(const Size(1000, 1000)); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ResizableContainer( - controller: controller, - direction: Axis.horizontal, - divider: const ResizableDivider( - thickness: 1, - padding: 0, - ), - children: const [ - ResizableChild( - size: ResizableSize.shrink(), - child: SizedBox( - width: 200, - key: Key('BoxA'), - ), - ), - ResizableChild( - size: ResizableSize.expand(), - child: SizedBox.expand( - key: Key('BoxB'), - ), - ), - ], - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - final boxAFinder = find.byKey(const Key('BoxA')); - final boxBFinder = find.byKey(const Key('BoxB')); - - expect(boxAFinder, findsOneWidget); - expect(boxBFinder, findsOneWidget); - - final boxASize = tester.getSize(boxAFinder); - final boxBSize = tester.getSize(boxBFinder); - - expect(boxASize.width, moreOrLessEquals(200, epsilon: 2)); - expect(boxBSize.width, moreOrLessEquals(800, epsilon: 2)); - - await tester.binding.setSurfaceSize(const Size(1100, 1000)); - await tester.pumpAndSettle(); - - final newBoxASize = tester.getSize(boxAFinder); - final newBoxBSize = tester.getSize(boxBFinder); - - expect(newBoxASize.width, moreOrLessEquals(200, epsilon: 2)); - expect(newBoxBSize.width, moreOrLessEquals(900, epsilon: 2)); - }, - ); - - testWidgets( - 'if the expand reaches 0, shrink will receive remaining delta', - (tester) async { - final controller = ResizableController(); - await tester.binding.setSurfaceSize(const Size(1000, 1000)); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ResizableContainer( - controller: controller, - direction: Axis.horizontal, - divider: const ResizableDivider( - thickness: 1, - padding: 0, - ), - children: const [ - ResizableChild( - size: ResizableSize.shrink(), - child: SizedBox( - width: 200, - key: Key('BoxA'), - ), - ), - ResizableChild( - size: ResizableSize.expand(), - child: SizedBox( - key: Key('BoxB'), - ), - ), - ], - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - final boxAFinder = find.byKey(const Key('BoxA')); - final boxBFinder = find.byKey(const Key('BoxB')); - - expect(boxAFinder, findsOneWidget); - expect(boxBFinder, findsOneWidget); - - final boxASize = tester.getSize(boxAFinder); - final boxBSize = tester.getSize(boxBFinder); - - expect(boxASize.width, moreOrLessEquals(200, epsilon: 2)); - expect(boxBSize.width, moreOrLessEquals(800, epsilon: 2)); - - await tester.binding.setSurfaceSize(const Size(150, 1000)); - await tester.pumpAndSettle(); - - final newBoxASize = tester.getSize(boxAFinder); - final newBoxBSize = tester.getSize(boxBFinder); - - expect(newBoxASize.width, moreOrLessEquals(150, epsilon: 2)); - expect(newBoxBSize.width, moreOrLessEquals(0, epsilon: 2)); - }, - ); - - testWidgets( - 'if the expand reaches a max constraint, shrink will receive remaining delta', - (tester) async { - final controller = ResizableController(); - await tester.binding.setSurfaceSize(const Size(1000, 1000)); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ResizableContainer( - controller: controller, - direction: Axis.horizontal, - divider: const ResizableDivider( - thickness: 1, - padding: 0, - ), - children: const [ - ResizableChild( - size: ResizableSize.shrink(), - child: SizedBox( - width: 200, - key: Key('BoxA'), - ), - ), - ResizableChild( - size: ResizableSize.expand(), - maxSize: 850, - child: SizedBox( - key: Key('BoxB'), - ), - ), - ], - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - final boxAFinder = find.byKey(const Key('BoxA')); - final boxBFinder = find.byKey(const Key('BoxB')); - - expect(boxAFinder, findsOneWidget); - expect(boxBFinder, findsOneWidget); - - final boxASize = tester.getSize(boxAFinder); - final boxBSize = tester.getSize(boxBFinder); - - expect(boxASize.width, moreOrLessEquals(200, epsilon: 2)); - expect(boxBSize.width, moreOrLessEquals(800, epsilon: 2)); - - await tester.binding.setSurfaceSize(const Size(1200, 1000)); - await tester.pumpAndSettle(); - - final newBoxASize = tester.getSize(boxAFinder); - final newBoxBSize = tester.getSize(boxBFinder); - - expect(newBoxASize.width, moreOrLessEquals(350, epsilon: 2)); - expect(newBoxBSize.width, moreOrLessEquals(850, epsilon: 2)); - }, - ); - }); - - group('with a shrink and pixel child', () { - testWidgets('delta is distributed evenly', (tester) async { - final controller = ResizableController(); - await tester.binding.setSurfaceSize(const Size(502, 500)); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ResizableContainer( - controller: controller, - direction: Axis.horizontal, - divider: const ResizableDivider( - thickness: 1, - padding: 0, - ), - children: const [ - ResizableChild( - size: ResizableSize.shrink(), - child: SizedBox( - width: 200, - key: Key('BoxA'), - ), - ), - ResizableChild( - size: ResizableSize.pixels(300), - child: SizedBox( - key: Key('BoxB'), - ), - ), - ], - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - final boxAFinder = find.byKey(const Key('BoxA')); - final boxBFinder = find.byKey(const Key('BoxB')); - - expect(boxAFinder, findsOneWidget); - expect(boxBFinder, findsOneWidget); - - final boxASize = tester.getSize(boxAFinder); - final boxBSize = tester.getSize(boxBFinder); - - expect(boxASize.width, moreOrLessEquals(200, epsilon: 2)); - expect(boxBSize.width, moreOrLessEquals(300, epsilon: 2)); - - await tester.binding.setSurfaceSize(const Size(600, 500)); - await tester.pumpAndSettle(); - - final newBoxASize = tester.getSize(boxAFinder); - final newBoxBSize = tester.getSize(boxBFinder); - - expect(newBoxASize.width, moreOrLessEquals(250, epsilon: 2)); - expect(newBoxBSize.width, moreOrLessEquals(350, epsilon: 2)); - }); - }); - - group('with two pixel children', () { - testWidgets('delta is distributed evenly', (tester) async { - final controller = ResizableController(); - await tester.binding.setSurfaceSize(const Size(502, 500)); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ResizableContainer( - controller: controller, - direction: Axis.horizontal, - divider: const ResizableDivider( - thickness: 1, - padding: 0, - ), - children: const [ - ResizableChild( - size: ResizableSize.pixels(200), - child: SizedBox( - key: Key('BoxA'), - ), - ), - ResizableChild( - size: ResizableSize.pixels(300), - child: SizedBox( - key: Key('BoxB'), - ), - ), - ], - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - final boxAFinder = find.byKey(const Key('BoxA')); - final boxBFinder = find.byKey(const Key('BoxB')); - - expect(boxAFinder, findsOneWidget); - expect(boxBFinder, findsOneWidget); - - final boxASize = tester.getSize(boxAFinder); - final boxBSize = tester.getSize(boxBFinder); - - expect(boxASize.width, moreOrLessEquals(200, epsilon: 2)); - expect(boxBSize.width, moreOrLessEquals(300, epsilon: 2)); - - await tester.binding.setSurfaceSize(const Size(600, 500)); - await tester.pumpAndSettle(); - - final newBoxASize = tester.getSize(boxAFinder); - final newBoxBSize = tester.getSize(boxBFinder); - - expect(newBoxASize.width, moreOrLessEquals(250, epsilon: 2)); - expect(newBoxBSize.width, moreOrLessEquals(350, epsilon: 2)); - }); - }); + expect(controller.sizes.first, moreOrLessEquals(800, epsilon: 2)); + expect(controller.sizes.last, moreOrLessEquals(200, epsilon: 2)); }); }); } diff --git a/test/resizable_controller_test.dart b/test/resizable_controller_test.dart index cd7f469..faed512 100644 --- a/test/resizable_controller_test.dart +++ b/test/resizable_controller_test.dart @@ -1,4 +1,3 @@ -import 'package:fake_async/fake_async.dart'; import 'package:flutter/material.dart'; import 'package:flutter_resizable_container/flutter_resizable_container.dart'; import 'package:flutter_resizable_container/src/resizable_controller.dart'; @@ -6,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - group(ResizableController, () { late ResizableController controller; late ResizableControllerManager manager; @@ -18,7 +16,7 @@ void main() { tearDown(() => controller.dispose()); - group('.pixels', () { + group('.sizes', () { setUp(() { controller.setChildren(const [ ResizableChild( @@ -31,15 +29,11 @@ void main() { ), ]); - fakeAsync((async) { - manager.setAvailableSpace(300); - manager.setRenderedSizes([100, 200]); - async.flushTimers(); - }); + manager.setAvailableSpace(300); }); - test('returns a list of pixel sizes', () { - expect(controller.pixels, equals([100, 200])); + test('returns a list of sizes', () { + expect(controller.sizes, equals([100, 200])); }); }); @@ -56,11 +50,7 @@ void main() { ), ]); - fakeAsync((async) { - manager.setAvailableSpace(300); - manager.setRenderedSizes([100, 200]); - async.flushTimers(); - }); + manager.setAvailableSpace(300); }); test('returns a list of ratios', () { @@ -82,11 +72,7 @@ void main() { ), ]); - fakeAsync((async) { - manager.setAvailableSpace(300); - manager.setRenderedSizes([100, 200]); - async.flushTimers(); - }); + manager.setAvailableSpace(300); }); test('does not notify listeners', () { @@ -99,115 +85,65 @@ void main() { }); }); - group('when changing the available space', () { - group('when only pixel sizes are present', () { - setUp(() { - controller.setChildren(const [ - ResizableChild( - size: ResizableSize.pixels(100), - child: SizedBox.shrink(), - ), - ResizableChild( - size: ResizableSize.pixels(200), - child: SizedBox.shrink(), - ), - ]); - - fakeAsync((async) { - manager.setAvailableSpace(300); - manager.setRenderedSizes([100, 200]); - async.flushTimers(); - }); - }); - - test('adjusts child sizes', () { - manager.setAvailableSpace(400); - expect(controller.pixels, equals([150.0, 250.0])); - }); + group('when setting the value for the first time', () { + setUp(() { + controller.setChildren(const [ + ResizableChild( + size: ResizableSize.pixels(100), + child: SizedBox.shrink(), + ), + ResizableChild( + size: ResizableSize.ratio(1 / 2), + child: SizedBox.shrink(), + ), + ResizableChild(child: SizedBox.shrink()), + ]); }); - group('when an expand child is present', () { - setUp(() { - controller.setChildren(const [ - ResizableChild( - size: ResizableSize.pixels(100), - child: SizedBox.shrink(), - ), - ResizableChild( - size: ResizableSize.expand(), - child: SizedBox.shrink(), - ), - ]); - - fakeAsync((async) { - manager.setAvailableSpace(300); - manager.setRenderedSizes([100, 200]); - async.flushTimers(); - }); - }); - - test('only adjusts the expandable child', () { - manager.setAvailableSpace(400); - expect(controller.pixels, equals([100.0, 300.0])); - }); + test('sets sizes based on child starting size', () { + manager.setAvailableSpace(300); + expect(controller.sizes, equals([100, 100, 100])); }); - group('when an expandable is present and has a constraint', () { - setUp(() { - controller.setChildren(const [ - ResizableChild( - size: ResizableSize.pixels(100), - child: SizedBox.shrink(), - ), - ResizableChild( - maxSize: 225.0, - size: ResizableSize.expand(), - child: SizedBox.shrink(), - ), - ]); - - fakeAsync((async) { - manager.setAvailableSpace(300); - manager.setRenderedSizes([100, 200]); - async.flushTimers(); - }); - }); - - test('adjusts the expandable child to its maximum size', () { - manager.setAvailableSpace(400); - expect(controller.pixels.last, equals(225.0)); - }); - - test('distributes remaining delta to other children', () { - manager.setAvailableSpace(400); - expect(controller.pixels.first, equals(175.0)); - }); + test('does not notify listeners', () { + var notified = false; + controller.addListener(() => notified = true); + manager.setAvailableSpace(300); + expect(notified, isFalse); }); + }); - group('when a shrink size is present', () { - setUp(() { - controller.setChildren(const [ - ResizableChild( - size: ResizableSize.pixels(100), - child: SizedBox.shrink(), - ), - ResizableChild( - size: ResizableSize.shrink(), - child: SizedBox.shrink(), - ), - ]); - - fakeAsync((async) { - manager.setAvailableSpace(300); - manager.setRenderedSizes([100, 200]); - async.flushTimers(); - }); - }); - - test('adjusts the children equally', () { - manager.setAvailableSpace(400); - expect(controller.pixels, equals([150.0, 250.0])); - }); + group('when updating the available space', () { + setUp(() { + controller.setChildren(const [ + ResizableChild( + size: ResizableSize.pixels(100), + child: SizedBox.shrink(), + ), + ResizableChild( + size: ResizableSize.ratio(1 / 2), + child: SizedBox.shrink(), + ), + ResizableChild(child: SizedBox.shrink()), + ]); + + manager.setAvailableSpace(300); + }); + + test('adjusts child sizes', () { + // only the "expandable" child (last) should change + final expected = [...controller.sizes]; + expected.last += 300; + + manager.setAvailableSpace(600); + expect(controller.sizes, equals(expected)); + }); + + test('notifies listeners', () { + var notified = false; + controller.addListener(() => notified = true); + manager.setAvailableSpace(600); + expect(notified, isTrue); }); }); }); @@ -234,7 +170,7 @@ void main() { ); }); - test('notifies listeners', () { + test('does not notify listeners', () { var notified = false; controller.addListener(() => notified = true); controller.setChildren(const [ @@ -243,10 +179,12 @@ void main() { child: SizedBox.shrink(), ), ]); - expect(notified, isTrue); + expect(notified, isFalse); }); + }); - test('sets the list of children', () { + group('#updateChildren', () { + setUp(() { controller.setChildren(const [ ResizableChild( size: ResizableSize.pixels(100), @@ -255,6 +193,20 @@ void main() { ResizableChild( child: SizedBox.shrink(), ), + ]); + + manager.setAvailableSpace(200); + }); + + test('sets the list of children', () { + manager.updateChildren(const [ + ResizableChild( + size: ResizableSize.pixels(100), + child: SizedBox.shrink(), + ), + ResizableChild( + child: SizedBox.shrink(), + ), ResizableChild( child: SizedBox.shrink(), ), @@ -266,12 +218,40 @@ void main() { ); }); - test('requests a new layout', () { - controller.setChildren([ - ResizableChild(child: SizedBox.shrink()), + test('updates children sizes', () { + manager.updateChildren(const [ + ResizableChild( + size: ResizableSize.pixels(100), + child: SizedBox.shrink(), + ), + ResizableChild( + child: SizedBox.shrink(), + ), + ResizableChild( + child: SizedBox.shrink(), + ), + ]); + + expect(controller.sizes, equals([100, 50, 50])); + }); + + test('does not notify listeners', () { + var notified = false; + controller.addListener(() => notified = true); + manager.updateChildren(const [ + ResizableChild( + size: ResizableSize.pixels(100), + child: SizedBox.shrink(), + ), + ResizableChild( + child: SizedBox.shrink(), + ), + ResizableChild( + child: SizedBox.shrink(), + ), ]); - expect(controller.needsLayout, isTrue); + expect(notified, isFalse); }); }); @@ -290,21 +270,18 @@ void main() { ), ]); - fakeAsync((async) { - manager.setAvailableSpace(200); - manager.setRenderedSizes([100, 50, 50]); - }); + manager.setAvailableSpace(200); }); group('when increasing the size', () { test('increases the size of the target child', () { manager.adjustChildSize(index: 1, delta: 10); - expect(controller.pixels[1], equals(60)); + expect(controller.sizes[1], equals(60)); }); test('decreases the size of the adjacent child', () { manager.adjustChildSize(index: 1, delta: 10); - expect(controller.pixels[2], equals(40)); + expect(controller.sizes[2], equals(40)); }); test('notifies listeners', () { @@ -331,10 +308,7 @@ void main() { ), ]); - fakeAsync((async) { - manager.setAvailableSpace(200); - manager.setRenderedSizes([100, 50, 50]); - }); + manager.setAvailableSpace(200); }); group('when the list is the wrong length', () { @@ -375,14 +349,14 @@ void main() { }); }); - test('requests a new layout', () { + test('sets child sizes', () { controller.setSizes(const [ ResizableSize.pixels(100), - ResizableSize.pixels(50), - ResizableSize.pixels(50), + ResizableSize.ratio(0.5), + ResizableSize.expand(), ]); - expect(controller.needsLayout, isTrue); + expect(controller.sizes, equals([100, 50, 50])); }); }); });