From a2e65e9656a018692be2adc4f3e2d689b82adea5 Mon Sep 17 00:00:00 2001 From: snat-s <65375294+snat-s@users.noreply.github.com> Date: Tue, 30 Aug 2022 16:59:09 -0500 Subject: [PATCH] [dynamic_layouts] Add Wrap layout and updates to the README (#2513) --- packages/dynamic_layouts/README.md | 131 +++- .../dynamic_layouts/lib/dynamic_layouts.dart | 1 + .../lib/src/base_grid_layout.dart | 11 +- .../dynamic_layouts/lib/src/dynamic_grid.dart | 74 +- .../dynamic_layouts/lib/src/wrap_layout.dart | 268 ++++++++ .../test/base_grid_layout_test.dart | 4 +- .../test/dynamic_grid_test.dart | 15 +- .../test/wrap_layout_test.dart | 636 ++++++++++++++++++ 8 files changed, 1113 insertions(+), 27 deletions(-) create mode 100644 packages/dynamic_layouts/lib/src/wrap_layout.dart create mode 100644 packages/dynamic_layouts/test/wrap_layout_test.dart diff --git a/packages/dynamic_layouts/README.md b/packages/dynamic_layouts/README.md index fc7bdab71d..0973efacd2 100644 --- a/packages/dynamic_layouts/README.md +++ b/packages/dynamic_layouts/README.md @@ -13,25 +13,138 @@ and the Flutter guide for [developing packages and plugins](https://flutter.dev/developing-packages). --> -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. + ## Features +This package provides support for multi sized tiles and different layouts. +Currently the layouts that are implemented in this package are `Stagger` and +`Wrap`. -TODO: List what your package can do. Maybe include images, gifs, or videos. +The following are some demos of how each of the grids look. + +A stagger grid demo: + + + +A wrap demo: + + + +### Stagger Features + +`DynamicGridView` is a subclass of `GridView` and gives access +to the `SliverGridDelegate`s that are already implemented in the Flutter +Framework. Some `SliverGridDelegate`s are `SliverGridDelegateWithMaxCrossAxisExtent` and +`SliverGridDelegateWithFixedCrossAxisCount`. This layout can be used with +`DynamicGridView.stagger`. + +### Wrap Features + +The Wrap layout is able to do runs of different widgets and adapt accordingly with +the sizes of the children. It can leave spacing with `mainAxisSpacing` and +`crossAxisSpacing`. + +Having different sizes in only one of the axis is possible by +changing the values of `childCrossAxisExtent` and `childMainAxisExtent`. These +values by default are set to have loose constraints, but by giving `childCrossAxisExtent` a specific value like +100 pixels, it will make all of the children 100 pixels in the main axis. +This layout can be used with `DynamicGridView.wrap` and with +`DynamicGridView.builder` and `SliverGridDelegateWithWrapping` as the delegate. ## Getting started -TODO: List prerequisites and provide or point to information on how to -start using the package. + ## Usage -TODO: Include short and useful examples for package users. Add longer examples -to `/example` folder. +Use `DynamicGridView`s to access this layouts. +`DynamicGridView` has some constructors that use `SliverChildListDelegate` like +`.wrap` and `.stagger`. For a more efficient option that uses `SliverChildBuilderDelegate` use +`.builder`, it works the same as `GridView.builder`. + +### Wrap + +The following are simple examples of how to use `DynamicGridView.wrap`. + + +```dart +final List children = List.generate( + 250, + (index) => Container( + height: index.isEven ? 100 : 50, + width: index.isEven ? 95 : 180, + color: index.isEven ? Colors.red : Colors.blue, + child: Center(child: Text('Item $index')), + ), + ); + +DynamicGridView.wrap( + mainAxisSpacing: 10, + crossAxisSpacing: 20, + children: children, +); +``` + +The following example uses `DynamicGridView.builder` with +`SliverGridDelegateWithWrapping`. + + +```dart +DynamicGridView.builder( + gridDelegate: const SliverGridDelegateWithWrapping( + mainAxisSpacing: 20, + childMainAxisExtent: 250, + childCrossAxisExtent: 50, + ), + itemBuilder: (BuildContext context, int index) { + return Container( + height: 200, + color: index.isEven ? Colors.amber : Colors.blue, + child: Center( + child: Text('$index'), + ), + ); + }, + ), +``` + +By using `childCrossAxisExtent` and `childMainAxisExtent` the main axis +can be limited to have a specific size and the other can be set to loose +constraints. + + +```dart +DynamicGridView.builder( + gridDelegate: const SliverGridDelegateWithWrapping( + mainAxisSpacing: 20, + childMainAxisExtent: 250, + ), + itemBuilder: (BuildContext context, int index) { + return Container( + height: 200, + color: index.isEven ? Colors.amber : Colors.blue, + child: Center( + child: Text('$index'), + ), + ); + }, +), +``` + +### Stagger + +The `Stagger` layout can be used with the constructor +`DynamicGridView.stagger` and still use the delegates from `GridView` +like `SliverGridDelegateWithMaxCrossAxisExtent` and +`SliverGridDelegateWithFixedCrossAxisCount`. + + + + ## Additional information -TODO: Tell users more about the package: where to find more information, how to + diff --git a/packages/dynamic_layouts/lib/dynamic_layouts.dart b/packages/dynamic_layouts/lib/dynamic_layouts.dart index 3f3537490e..ad714663fb 100644 --- a/packages/dynamic_layouts/lib/dynamic_layouts.dart +++ b/packages/dynamic_layouts/lib/dynamic_layouts.dart @@ -5,3 +5,4 @@ export 'src/base_grid_layout.dart'; export 'src/dynamic_grid.dart'; export 'src/render_dynamic_grid.dart'; +export 'src/wrap_layout.dart'; diff --git a/packages/dynamic_layouts/lib/src/base_grid_layout.dart b/packages/dynamic_layouts/lib/src/base_grid_layout.dart index 0c6b6bae18..e33e7d6a80 100644 --- a/packages/dynamic_layouts/lib/src/base_grid_layout.dart +++ b/packages/dynamic_layouts/lib/src/base_grid_layout.dart @@ -37,14 +37,14 @@ class DynamicSliverGridGeometry extends SliverGridGeometry { crossAxisExtent.isInfinite ? 0.0 : crossAxisExtent; switch (constraints.axis) { - case Axis.horizontal: + case Axis.vertical: return BoxConstraints( minHeight: mainMinExtent, maxHeight: mainAxisExtent, minWidth: crossMinExtent, maxWidth: crossAxisExtent, ); - case Axis.vertical: + case Axis.horizontal: return BoxConstraints( minHeight: crossMinExtent, maxHeight: crossAxisExtent, @@ -68,14 +68,17 @@ abstract class DynamicSliverGridLayout extends SliverGridLayout { /// provide looser constraints to the child, whose size after layout can be /// reported back to the layout object in [updateGeometryForChildIndex]. @override - SliverGridGeometry getGeometryForChildIndex(int index); + DynamicSliverGridGeometry getGeometryForChildIndex(int index); /// Update the size and position of the child with the given index, /// considering the size of the child after layout. /// /// This is used to update the layout object after the child has laid out, /// allowing the layout pattern to adapt to the child's size. - SliverGridGeometry updateGeometryForChildIndex(int index, Size childSize); + DynamicSliverGridGeometry updateGeometryForChildIndex( + int index, + Size childSize, + ); /// Called by [RenderDynamicSliverGrid] to validate the layout pattern has /// filled the screen. diff --git a/packages/dynamic_layouts/lib/src/dynamic_grid.dart b/packages/dynamic_layouts/lib/src/dynamic_grid.dart index 9649a3ff3a..a9aceccd50 100644 --- a/packages/dynamic_layouts/lib/src/dynamic_grid.dart +++ b/packages/dynamic_layouts/lib/src/dynamic_grid.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'render_dynamic_grid.dart'; +import 'wrap_layout.dart'; /// A scrollable, 2D array of widgets. /// @@ -12,13 +13,10 @@ import 'render_dynamic_grid.dart'; class DynamicGridView extends GridView { /// Creates a scrollable, 2D array of widgets with a custom /// [SliverGridDelegate]. - /// - // TODO(all): what other parameters should we add to these - // constructors, here, builder, etc.? - // + reverse - // + scrollDirection DynamicGridView({ super.key, + super.scrollDirection, + super.reverse, required super.gridDelegate, // This creates a SliverChildListDelegate in the super class. super.children = const [], @@ -27,13 +25,77 @@ class DynamicGridView extends GridView { /// Creates a scrollable, 2D array of widgets that are created on demand. DynamicGridView.builder({ super.key, + super.scrollDirection, + super.reverse, required super.gridDelegate, // This creates a SliverChildBuilderDelegate in the super class. required super.itemBuilder, super.itemCount, }) : super.builder(); - // TODO(snat-s): DynamicGridView.wrap? + /// Creates a scrollable, 2D array of widgets with tiles where each tile can + /// have its own size. + /// + /// Uses a [SliverGridDelegateWithWrapping] as the [gridDelegate]. + /// + /// The following example shows how to use the DynamicGridView.wrap constructor. + /// + /// ```dart + /// DynamicGridView.wrap( + /// mainAxisSpacing: 10, + /// crossAxisSpacing: 20, + /// children: [ + /// Container( + /// height: 100, + /// width: 200, + /// color: Colors.amberAccent[100], + /// child: const Center(child: Text('Item 1') + /// ), + /// ), + /// Container( + /// height: 50, + /// width: 70, + /// color: Colors.blue[100], + /// child: const Center(child: Text('Item 2'), + /// ), + /// ), + /// Container( + /// height: 82, + /// width: 300, + /// color: Colors.pink[100], + /// child: const Center(child: Text('Item 3'), + /// ), + /// ), + /// Container( + /// color: Colors.green[100], + /// child: const Center(child: Text('Item 3'), + /// ), + /// ), + /// ], + /// ), + /// ``` + /// + /// See also: + /// + /// * [SliverGridDelegateWithWrapping] to see a more detailed explanation of + /// how the wrapping works. + DynamicGridView.wrap({ + super.key, + super.scrollDirection, + super.reverse, + double mainAxisSpacing = 0.0, + double crossAxisSpacing = 0.0, + double childCrossAxisExtent = double.infinity, + double childMainAxisExtent = double.infinity, + super.children = const [], + }) : super( + gridDelegate: SliverGridDelegateWithWrapping( + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + childCrossAxisExtent: childCrossAxisExtent, + childMainAxisExtent: childMainAxisExtent, + ), + ); // TODO(DavBot09): DynamicGridView.stagger? diff --git a/packages/dynamic_layouts/lib/src/wrap_layout.dart b/packages/dynamic_layouts/lib/src/wrap_layout.dart new file mode 100644 index 0000000000..2a13173803 --- /dev/null +++ b/packages/dynamic_layouts/lib/src/wrap_layout.dart @@ -0,0 +1,268 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/rendering.dart'; + +import 'base_grid_layout.dart'; + +// The model that tracks the current max size of the Sliver in the mainAxis and +// tracks if there is still space on the crossAxis. +class _RunMetrics { + _RunMetrics({ + required this.maxSliver, + required this.currentSizeUsed, + required this.scrollOffset, + }); + + /// The biggest sliver size for the current run. + double maxSliver; + + /// The current size that has been used in the current run. + double currentSizeUsed; + + /// The scroll offset in the current run. + double scrollOffset; +} + +/// A [DynamicSliverGridLayout] that uses dynamically sized tiles. +/// +/// Rather that providing a grid with a [DynamicSliverGridLayout] directly, instead +/// provide the grid a [SliverGridDelegate], which can compute a +/// [DynamicSliverGridLayout] given the current [SliverConstraints]. +/// +/// This layout is used by [SliverGridDelegateWithWrapping]. +/// +/// See also: +/// +/// * [SliverGridDelegateWithWrapping], which uses this layout. +/// * [DynamicSliverGridLayout], which represents an arbitrary dynamic tile layout. +/// * [DynamicSliverGridGeometry], which represents the size and position of a +/// single tile in a grid. +/// * [SliverGridDelegate.getLayout], which returns this object to describe the +/// delegate's layout. +/// * [RenderDynamicSliverGrid], which uses this class during its +/// [RenderDynamicSliverGrid.performLayout] method. +class SliverGridWrappingTileLayout extends DynamicSliverGridLayout { + /// Creates a layout that uses dynamic sized and spaced tiles. + /// + /// All of the arguments must not be null and must not be negative. + SliverGridWrappingTileLayout({ + required this.mainAxisSpacing, + required this.crossAxisSpacing, + required this.childMainAxisExtent, + required this.childCrossAxisExtent, + required this.crossAxisExtent, + required this.scrollDirection, + }) : assert(mainAxisSpacing != null && mainAxisSpacing >= 0), + assert(crossAxisSpacing != null && crossAxisSpacing >= 0), + assert(childMainAxisExtent != null && childMainAxisExtent >= 0), + assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0), + assert(crossAxisExtent != null && crossAxisExtent >= 0), + assert(scrollDirection != null && + (scrollDirection == Axis.horizontal || + scrollDirection == Axis.vertical)); + + /// The direction in wich the layout should be built. + final Axis scrollDirection; + + /// The extent of the child in the non-scrolling axis. + final double crossAxisExtent; + + /// The number of logical pixels between each child along the main axis. + final double mainAxisSpacing; + + /// The number of logical pixels between each child along the cross axis. + final double crossAxisSpacing; + + /// The number of pixels from the leading edge of one tile to the trailing + /// edge of the same tile in the main axis. + final double childMainAxisExtent; + + /// The number of pixels from the leading edge of one tile to the trailing + /// edge of the same tile in the cross axis. + final double childCrossAxisExtent; + + /// The model that is used internally to keep track of how much space is left + /// and how much has been used. + final List<_RunMetrics> _model = <_RunMetrics>[ + _RunMetrics(maxSliver: 0.0, currentSizeUsed: 0.0, scrollOffset: 0.0) + ]; + + // This method provides the initial constraints for the child to layout, + // and then it is updated with the final size later in + // updateGeometryForChildIndex. + @override + DynamicSliverGridGeometry getGeometryForChildIndex(int index) { + return DynamicSliverGridGeometry( + scrollOffset: 0, + crossAxisOffset: 0, + mainAxisExtent: childMainAxisExtent, + crossAxisExtent: childCrossAxisExtent, + ); + } + + @override + DynamicSliverGridGeometry updateGeometryForChildIndex( + int index, + Size childSize, + ) { + final double scrollOffset = _model.last.scrollOffset; + final double currentSizeUsed = _model.last.currentSizeUsed; + late final double addedSize; + + switch (scrollDirection) { + case Axis.vertical: + addedSize = currentSizeUsed + childSize.width + crossAxisSpacing; + break; + case Axis.horizontal: + addedSize = currentSizeUsed + childSize.height + mainAxisSpacing; + break; + } + + if (addedSize > crossAxisExtent && _model.last.currentSizeUsed > 0.0) { + switch (scrollDirection) { + case Axis.vertical: + _model.add( + _RunMetrics( + maxSliver: childSize.height + mainAxisSpacing, + currentSizeUsed: childSize.width + crossAxisSpacing, + scrollOffset: + scrollOffset + _model.last.maxSliver + mainAxisSpacing, + ), + ); + break; + case Axis.horizontal: + _model.add( + _RunMetrics( + maxSliver: childSize.width + crossAxisSpacing, + currentSizeUsed: childSize.height + mainAxisSpacing, + scrollOffset: + scrollOffset + _model.last.maxSliver + crossAxisSpacing, + ), + ); + break; + } + + return DynamicSliverGridGeometry( + scrollOffset: _model.last.scrollOffset, + crossAxisOffset: 0.0, + mainAxisExtent: childSize.height + mainAxisSpacing, + crossAxisExtent: childSize.width + crossAxisSpacing, + ); + } else { + _model.last.currentSizeUsed = addedSize; + } + + switch (scrollDirection) { + case Axis.vertical: + if (childSize.height + mainAxisSpacing > _model.last.maxSliver) { + _model.last.maxSliver = childSize.height + mainAxisSpacing; + } + break; + case Axis.horizontal: + if (childSize.width + crossAxisSpacing > _model.last.maxSliver) { + _model.last.maxSliver = childSize.width + crossAxisSpacing; + } + break; + } + + return DynamicSliverGridGeometry( + scrollOffset: scrollOffset, + crossAxisOffset: currentSizeUsed, + mainAxisExtent: childSize.height, + crossAxisExtent: childSize.width, + ); + } + + @override + bool reachedTargetScrollOffset(double targetOffset) { + return _model.last.scrollOffset > targetOffset; + } +} + +/// A [SliverGridDelegate] for creating grids that wrap variably sized tiles. +/// +/// For example, if the grid is vertical, this delegate will create a layout +/// where the children are layed out until they fill the horizontal axis and then +/// they continue in the next row. If the grid is horizontal, this delegate will +/// do the same but it will fill the vertical axis and will pass to another +/// column until it finishes. +/// +/// This delegate creates grids with different sized tiles. Tiles +/// can have fixed dimensions if [childCrossAxisExtent] or +/// [childMainAxisExtent] are provided. +/// +/// See also: +/// * [DynamicGridView.wrap], wich is a constructor to use this [SliverGridDelegate], +/// like `GridView.extent`. +/// * [DynamicGridView], which can use this delegate to control the layout of its +/// tiles. +/// * [RenderDynamicSliverGrid], which can use this delegate to control the +/// layout of its tiles. +class SliverGridDelegateWithWrapping extends SliverGridDelegate { + /// Create a delegate that wraps variably sized tiles. + /// + /// The children widgets are provided with loose constraints, and if any of the + /// extent parameters are set, the children are given tight constraints. + /// The way that children are made to have loose constraints is by assigning + /// the value of [double.infinity] to [childMainAxisExtent] and + /// [childCrossAxisExtent]. + /// To have same sized tiles with the wrapping, specify the [childCrossAxisExtent] + /// and the [childMainAxisExtent] to be the same size. Or only one of them to + /// be of a certain size in one of the axis. + const SliverGridDelegateWithWrapping({ + this.mainAxisSpacing = 0.0, + this.crossAxisSpacing = 0.0, + this.childCrossAxisExtent = double.infinity, + this.childMainAxisExtent = double.infinity, + }) : assert(mainAxisSpacing != null && mainAxisSpacing >= 0), + assert(crossAxisSpacing != null && crossAxisSpacing >= 0); + + /// The number of pixels from the leading edge of one tile to the trailing + /// edge of the same tile in the main axis. + /// + /// Defaults to [double.infinity] to provide the child with loose constraints. + final double childMainAxisExtent; + + /// The number of pixels from the leading edge of one tile to the trailing + /// edge of the same tile in the cross axis. + /// + /// Defaults to [double.infinity] to provide the child with loose constraints. + final double childCrossAxisExtent; + + /// The number of logical pixels between each child along the main axis. + /// + /// Defaults to 0.0 + final double mainAxisSpacing; + + /// The number of logical pixels between each child along the cross axis. + /// + /// Defaults to 0.0 + final double crossAxisSpacing; + + bool _debugAssertIsValid() { + assert(mainAxisSpacing >= 0.0); + assert(crossAxisSpacing >= 0.0); + return true; + } + + @override + SliverGridLayout getLayout(SliverConstraints constraints) { + assert(_debugAssertIsValid()); + return SliverGridWrappingTileLayout( + childMainAxisExtent: childMainAxisExtent, + childCrossAxisExtent: childCrossAxisExtent, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + scrollDirection: axisDirectionToAxis(constraints.axisDirection), + crossAxisExtent: constraints.crossAxisExtent, + ); + } + + @override + bool shouldRelayout(SliverGridDelegateWithWrapping oldDelegate) { + return oldDelegate.mainAxisSpacing != mainAxisSpacing || + oldDelegate.crossAxisSpacing != crossAxisSpacing; + } +} diff --git a/packages/dynamic_layouts/test/base_grid_layout_test.dart b/packages/dynamic_layouts/test/base_grid_layout_test.dart index 69638e8298..59e6bd5564 100644 --- a/packages/dynamic_layouts/test/base_grid_layout_test.dart +++ b/packages/dynamic_layouts/test/base_grid_layout_test.dart @@ -12,8 +12,8 @@ void main() { const DynamicSliverGridGeometry geometry = DynamicSliverGridGeometry( scrollOffset: 0, crossAxisOffset: 0, - mainAxisExtent: 150.0, - crossAxisExtent: 50.0, + crossAxisExtent: 150.0, + mainAxisExtent: 50.0, ); // Vertical diff --git a/packages/dynamic_layouts/test/dynamic_grid_test.dart b/packages/dynamic_layouts/test/dynamic_grid_test.dart index e6ad3300ae..52b17dd37e 100644 --- a/packages/dynamic_layouts/test/dynamic_grid_test.dart +++ b/packages/dynamic_layouts/test/dynamic_grid_test.dart @@ -38,7 +38,7 @@ void main() { ), ); - // Only the visible tiles have ben laid out. + // Only the visible tiles have been laid out. expect(find.text('Index 0'), findsOneWidget); expect(tester.getTopLeft(find.text('Index 0')), Offset.zero); expect(find.text('Index 1'), findsOneWidget); @@ -69,7 +69,7 @@ void main() { ), ); - // Only the visible tiles have ben laid out, up to itemCount. + // Only the visible tiles have been laid out, up to itemCount. expect(find.text('Index 0'), findsOneWidget); expect(tester.getTopLeft(find.text('Index 0')), Offset.zero); expect(find.text('Index 1'), findsOneWidget); @@ -119,9 +119,9 @@ class TestSimpleLayout extends DynamicSliverGridLayout { static const double childExtent = 50.0; @override - SliverGridGeometry getGeometryForChildIndex(int index) { + DynamicSliverGridGeometry getGeometryForChildIndex(int index) { final double crossAxisStart = (index % crossAxisCount) * childExtent; - return SliverGridGeometry( + return DynamicSliverGridGeometry( scrollOffset: (index ~/ crossAxisCount) * childExtent, crossAxisOffset: crossAxisStart, mainAxisExtent: childExtent, @@ -133,7 +133,10 @@ class TestSimpleLayout extends DynamicSliverGridLayout { bool reachedTargetScrollOffset(double targetOffset) => true; @override - SliverGridGeometry updateGeometryForChildIndex(int index, Size childSize) { + DynamicSliverGridGeometry updateGeometryForChildIndex( + int index, + Size childSize, + ) { return getGeometryForChildIndex(index); } } @@ -142,7 +145,7 @@ class TestDelegate extends SliverGridDelegateWithFixedCrossAxisCount { TestDelegate({required super.crossAxisCount}); @override - SliverGridLayout getLayout(SliverConstraints constraints) { + DynamicSliverGridLayout getLayout(SliverConstraints constraints) { return TestSimpleLayout(crossAxisCount: crossAxisCount); } } diff --git a/packages/dynamic_layouts/test/wrap_layout_test.dart b/packages/dynamic_layouts/test/wrap_layout_test.dart new file mode 100644 index 0000000000..d595fe75df --- /dev/null +++ b/packages/dynamic_layouts/test/wrap_layout_test.dart @@ -0,0 +1,636 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:dynamic_layouts/dynamic_layouts.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets( + 'DynamicGridView generates children and checks if they are layed out', + (WidgetTester tester) async { + final List children = List.generate( + 10, + (int index) => SizedBox( + height: index.isEven ? 100 : 50, + width: index.isEven ? 95 : 180, + child: Text('Item $index'), + ), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DynamicGridView( + gridDelegate: const SliverGridDelegateWithWrapping(), + children: children, + ), + ), + ), + ); + + // Check that the children are in the tree + for (int i = 0; i < 10; i++) { + expect(find.text('Item $i'), findsOneWidget); + } + // Check that the children are in the right position + expect(tester.getTopLeft(find.text('Item 0')), const Offset(0.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(95.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(275.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 3')), const Offset(370.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 4')), const Offset(550.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 5')), const Offset(0.0, 100.0)); + expect(tester.getTopLeft(find.text('Item 6')), const Offset(180.0, 100.0)); + expect(tester.getTopLeft(find.text('Item 7')), const Offset(275.0, 100.0)); + expect(tester.getTopLeft(find.text('Item 8')), const Offset(455.0, 100.0)); + expect(tester.getTopLeft(find.text('Item 9')), const Offset(550.0, 100.0)); + }); + + testWidgets( + 'Test for wrap that generates children and checks if they are layed out', + (WidgetTester tester) async { + final List children = List.generate( + 10, + (int index) => SizedBox( + height: index.isEven ? 100 : 50, + width: index.isEven ? 95 : 180, + child: Text('Item $index'), + ), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DynamicGridView.wrap( + children: children, + ), + ), + ), + ); + for (int i = 0; i < 10; i++) { + expect(find.text('Item $i'), findsOneWidget); + } + // Check that the children are in the right position + expect(tester.getTopLeft(find.text('Item 0')), const Offset(0.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(95.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(275.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 3')), const Offset(370.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 4')), const Offset(550.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 5')), const Offset(0.0, 100.0)); + expect(tester.getTopLeft(find.text('Item 6')), const Offset(180.0, 100.0)); + expect(tester.getTopLeft(find.text('Item 7')), const Offset(275.0, 100.0)); + expect(tester.getTopLeft(find.text('Item 8')), const Offset(455.0, 100.0)); + expect(tester.getTopLeft(find.text('Item 9')), const Offset(550.0, 100.0)); + }); + + testWidgets('Test for wrap to be laying child dynamically', + (WidgetTester tester) async { + final List children = List.generate( + 20, + (int index) => SizedBox( + height: index.isEven ? 1000 : 50, + width: index.isEven ? 95 : 180, + child: Text('Item $index'), + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DynamicGridView.builder( + itemCount: children.length, + gridDelegate: const SliverGridDelegateWithWrapping(), + itemBuilder: (BuildContext context, int index) => children[index], + ), + ), + ), + ); + for (int i = 0; i < 5; i++) { + expect(find.text('Item $i'), findsOneWidget); + } + // Check that the children are in the right position + expect(tester.getTopLeft(find.text('Item 0')), const Offset(0.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(95.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(275.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 3')), const Offset(370.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 4')), const Offset(550.0, 0.0)); + expect(find.text('Item 5'), findsNothing); + await tester.scrollUntilVisible(find.text('Item 19'), 500.0); + await tester.pumpAndSettle(); + + expect(find.text('Item 18'), findsOneWidget); + expect(tester.getTopLeft(find.text('Item 18')), const Offset(455.0, 0.0)); + + expect(find.text('Item 0'), findsNothing); + expect(find.text('Item 1'), findsNothing); + expect(find.text('Item 2'), findsNothing); + expect(find.text('Item 3'), findsNothing); + }); + + testWidgets( + 'Test for DynamicGridView.wrap to scrollDirection Axis.horizontal', + (WidgetTester tester) async { + final List children = List.generate( + 20, + (int index) => SizedBox( + height: index.isEven ? 100 : 50, + width: index.isEven ? 100 : 180, + child: Text('Item $index'), + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DynamicGridView.wrap( + scrollDirection: Axis.horizontal, + children: children, + ), + ), + ), + ); + for (int i = 0; i < 20; i++) { + expect(find.text('Item $i'), findsOneWidget); + } + // Check that the children are in the right position + double dy = 0, dx = 0; + for (int i = 0; i < 20; i++) { + if (dy >= 600.0) { + dy = 0.0; + dx += 180.0; + } + expect(tester.getTopLeft(find.text('Item $i')), Offset(dx, dy)); + dy += i.isEven ? 100 : 50; + } + }); + + testWidgets('Test DynamicGridView.builder for GridView.reverse to true', + (WidgetTester tester) async { + final List children = List.generate( + 10, + (int index) => SizedBox( + height: index.isEven ? 100 : 50, + width: index.isEven ? 100 : 180, + child: Text('Item $index'), + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DynamicGridView.builder( + reverse: true, + itemCount: children.length, + gridDelegate: const SliverGridDelegateWithWrapping(), + itemBuilder: (BuildContext context, int index) => children[index], + ), + ), + ), + ); + for (int i = 0; i < 10; i++) { + expect(find.text('Item $i'), findsOneWidget); + } + double dx = 0.0, dy = 600.0; + for (int i = 0; i < 10; i++) { + if (dx >= 600.0) { + dx = 0.0; + dy -= 100.0; + } + expect(tester.getBottomLeft(find.text('Item $i')), Offset(dx, dy)); + dx += i.isEven ? 100 : 180; + } + }); + + testWidgets('DynamicGridView.wrap for GridView.reverse to true', + (WidgetTester tester) async { + final List children = List.generate( + 20, + (int index) => SizedBox( + height: index.isEven ? 100 : 50, + width: index.isEven ? 100 : 180, + child: Text('Item $index'), + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DynamicGridView.wrap( + reverse: true, + children: children, + ), + ), + ), + ); + for (int i = 0; i < 20; i++) { + expect(find.text('Item $i'), findsOneWidget); + } + // Check that the children are in the right position + double dx = 0.0, dy = 600.0; + for (int i = 0; i < 20; i++) { + if (dx >= 600.0) { + dx = 0.0; + dy -= 100.0; + } + expect(tester.getBottomLeft(find.text('Item $i')), Offset(dx, dy)); + dx += i.isEven ? 100 : 180; + } + }); + + testWidgets('DynamicGridView.wrap dismiss keyboard onDrag test', + (WidgetTester tester) async { + final List focusNodes = + List.generate(50, (int i) => FocusNode()); + + await tester.pumpWidget( + textFieldBoilerplate( + child: GridView.extent( + padding: EdgeInsets.zero, + maxCrossAxisExtent: 300, + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + children: focusNodes.map((FocusNode focusNode) { + return Container( + height: 50, + color: Colors.green, + child: TextField( + focusNode: focusNode, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ); + }).toList(), + ), + ), + ); + + final Finder finder = find.byType(TextField).first; + final TextField textField = tester.widget(finder); + await tester.showKeyboard(finder); + expect(textField.focusNode!.hasFocus, isTrue); + + await tester.drag(finder, const Offset(0.0, -40.0)); + await tester.pumpAndSettle(); + expect(textField.focusNode!.hasFocus, isFalse); + }); + + testWidgets('ChildMainAxisExtent & childCrossAxisExtent are respected', + (WidgetTester tester) async { + final List children = List.generate( + 10, + (int index) => SizedBox( + key: Key(index.toString()), + child: Text('Item $index'), + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DynamicGridView.builder( + gridDelegate: const SliverGridDelegateWithWrapping( + childMainAxisExtent: 150, + childCrossAxisExtent: 200, + ), + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + ), + ), + ), + ); + + for (int i = 0; i < 10; i++) { + final Size sizeOfCurrent = tester.getSize(find.byKey(Key('$i'))); + expect(sizeOfCurrent.width, equals(200)); + expect(sizeOfCurrent.height, equals(150)); + } + // Check that the children are in the right position + double dy = 0, dx = 0; + for (int i = 0; i < 10; i++) { + if (dx > 600.0) { + dx = 0.0; + dy += 150.0; + } + expect(tester.getTopLeft(find.text('Item $i')), Offset(dx, dy)); + dx += 200; + } + }); + + testWidgets('ChildMainAxisExtent is respected', (WidgetTester tester) async { + final List children = List.generate( + 10, + (int index) => SizedBox( + key: Key(index.toString()), + width: index.isEven ? 100 : 180, + child: Text('Item $index'), + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DynamicGridView.builder( + gridDelegate: const SliverGridDelegateWithWrapping( + childMainAxisExtent: 200, + ), + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + ), + ), + ), + ); + + for (int i = 0; i < 10; i++) { + final Size sizeOfCurrent = tester.getSize(find.byKey(Key('$i'))); + expect(sizeOfCurrent.height, equals(200)); + } + // Check that the children are in the right position + double dy = 0, dx = 0; + for (int i = 0; i < 10; i++) { + if (dx >= 600.0) { + dx = 0.0; + dy += 200.0; + } + expect(tester.getTopLeft(find.text('Item $i')), Offset(dx, dy)); + dx += i.isEven ? 100 : 180; + } + }); + + testWidgets('ChildCrossAxisExtent is respected', (WidgetTester tester) async { + final List children = List.generate( + 10, + (int index) => SizedBox( + height: index.isEven ? 100 : 50, + key: Key(index.toString()), + child: Text('Item $index'), + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DynamicGridView.builder( + gridDelegate: const SliverGridDelegateWithWrapping( + childCrossAxisExtent: 150, + ), + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + ), + ), + ), + ); + + for (int i = 0; i < 10; i++) { + final Size sizeOfCurrent = tester.getSize(find.byKey(Key('$i'))); + expect(sizeOfCurrent.width, equals(150)); + } + // Check that the children are in the right position + double dy = 0, dx = 0; + for (int i = 0; i < 10; i++) { + if (dx >= 750.0) { + dx = 0.0; + dy += 100.0; + } + expect(tester.getTopLeft(find.text('Item $i')), Offset(dx, dy)); + dx += 150; + } + }); + + testWidgets('Test wrap to see nothing affected if elements are deleted.', + (WidgetTester tester) async { + late StateSetter stateSetter; + final List children = List.generate( + 10, + (int index) => SizedBox( + height: index.isEven ? 100 : 50, + width: index.isEven ? 100 : 180, + child: Text('Item $index'), + ), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + stateSetter = setState; + return DynamicGridView.builder( + gridDelegate: const SliverGridDelegateWithWrapping(), + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + ); + }), + ), + ), + ); + // See if the children are in the tree. + for (int i = 0; i < 10; i++) { + expect(find.text('Item $i'), findsOneWidget); + } + // See if they are layed properly. + double dx = 0.0, dy = 0.0; + for (int i = 0; i < 10; i++) { + if (dx >= 600) { + dx = 0.0; + dy += 100; + } + expect(tester.getTopLeft(find.text('Item $i')), Offset(dx, dy)); + dx += i.isEven ? 100 : 180; + } + stateSetter(() { + // Remove children + children.removeAt(0); + children.removeAt(8); + children.removeAt(5); + }); + + await tester.pump(); + + // See if the proper widgets are in the tree. + expect(find.text('Item 0'), findsNothing); + expect(find.text('Item 6'), findsNothing); + expect(find.text('Item 9'), findsNothing); + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Item 2'), findsOneWidget); + expect(find.text('Item 3'), findsOneWidget); + expect(find.text('Item 4'), findsOneWidget); + expect(find.text('Item 5'), findsOneWidget); + expect(find.text('Item 7'), findsOneWidget); + expect(find.text('Item 8'), findsOneWidget); + + // See if the proper widgets are in the tree. + expect(tester.getTopLeft(find.text('Item 1')), const Offset(0.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(180.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 3')), const Offset(280.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 4')), const Offset(460.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 5')), const Offset(560.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 7')), const Offset(0.0, 100.0)); + expect(tester.getTopLeft(find.text('Item 8')), const Offset(180.0, 100.0)); + }); + + testWidgets('Test wrap in Axis.vertical direction', + (WidgetTester tester) async { + final List children = List.generate( + 5, + (int index) => SizedBox( + height: index.isEven ? 100 : 50, + width: index.isEven ? 100 : 180, + child: Text('Item $index'), + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DynamicGridView.builder( + itemCount: children.length, + gridDelegate: const SliverGridDelegateWithWrapping(), + itemBuilder: (BuildContext context, int index) => children[index], + ), + ), + ), + ); + + // Change the size of the screen + await tester.binding.setSurfaceSize(const Size(500, 100)); + await tester.pumpAndSettle(); + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Item 2'), findsOneWidget); + expect(tester.getTopLeft(find.text('Item 0')), const Offset(0.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(100.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(280.0, 0.0)); + expect(find.text('Item 3'), findsNothing); + expect(find.text('Item 4'), findsNothing); + await tester.binding.setSurfaceSize(const Size(560, 100)); + await tester.pumpAndSettle(); + expect(find.text('Item 3'), findsOneWidget); + expect(tester.getTopLeft(find.text('Item 3')), const Offset(380.0, 0.0)); + expect(find.text('Item 4'), findsNothing); + await tester.binding.setSurfaceSize(const Size(280, 100)); + // resets the screen to its original size after the test end + addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + await tester.pumpAndSettle(); + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Item 1'), findsOneWidget); + expect(tester.getTopLeft(find.text('Item 0')), const Offset(0.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(100.0, 0.0)); + expect(find.text('Item 2'), findsNothing); + expect(find.text('Item 3'), findsNothing); + expect(find.text('Item 4'), findsNothing); + }); + + testWidgets('Test wrap in Axis.horizontal direction', + (WidgetTester tester) async { + final List children = List.generate( + 5, + (int index) => SizedBox( + height: index.isEven ? 100 : 50, + width: index.isEven ? 100 : 180, + child: Text('Item $index'), + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DynamicGridView.wrap( + scrollDirection: Axis.horizontal, + children: children, + ), + ), + ), + ); + + // Change the size of the screen + await tester.binding.setSurfaceSize(const Size(180, 150)); + await tester.pumpAndSettle(); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Item 1'), findsOneWidget); + expect(tester.getTopLeft(find.text('Item 0')), const Offset(0.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(0.0, 100.0)); + + expect(find.text('Item 2'), findsNothing); + expect(find.text('Item 3'), findsNothing); + + await tester.binding.setSurfaceSize(const Size(180, 400)); + await tester.pumpAndSettle(); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Item 2'), findsOneWidget); + expect(find.text('Item 3'), findsOneWidget); + expect(find.text('Item 4'), findsOneWidget); + + expect(tester.getTopLeft(find.text('Item 0')), const Offset(0.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(0.0, 100.0)); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(0.0, 150.0)); + expect(tester.getTopLeft(find.text('Item 3')), const Offset(0.0, 250.0)); + expect(tester.getTopLeft(find.text('Item 4')), const Offset(0.0, 300.0)); + + await tester.binding.setSurfaceSize(const Size(560, 100)); + // resets the screen to its original size after the test end + addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + await tester.pumpAndSettle(); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Item 2'), findsOneWidget); + expect(find.text('Item 3'), findsOneWidget); + expect(find.text('Item 4'), findsNothing); + + expect(tester.getTopLeft(find.text('Item 0')), const Offset(0.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(100.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(280.0, 0.0)); + expect(tester.getTopLeft(find.text('Item 3')), const Offset(380.0, 0.0)); + }); +} + +Widget textFieldBoilerplate({required Widget child}) { + return MaterialApp( + home: Localizations( + locale: const Locale('en', 'US'), + delegates: >[ + WidgetsLocalizationsDelegate(), + MaterialLocalizationsDelegate(), + ], + child: Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(size: Size(800.0, 600.0)), + child: Center( + child: Material( + child: child, + ), + ), + ), + ), + ), + ); +} + +class MaterialLocalizationsDelegate + extends LocalizationsDelegate { + @override + bool isSupported(Locale locale) => true; + + @override + Future load(Locale locale) => + DefaultMaterialLocalizations.load(locale); + + @override + bool shouldReload(MaterialLocalizationsDelegate old) => false; +} + +class WidgetsLocalizationsDelegate + extends LocalizationsDelegate { + @override + bool isSupported(Locale locale) => true; + + @override + Future load(Locale locale) => + DefaultWidgetsLocalizations.load(locale); + + @override + bool shouldReload(WidgetsLocalizationsDelegate old) => false; +}