diff --git a/examples/api/lib/widgets/scroll_view/grid_view.0.dart b/examples/api/lib/widgets/scroll_view/grid_view.0.dart new file mode 100644 index 0000000000..218429fde5 --- /dev/null +++ b/examples/api/lib/widgets/scroll_view/grid_view.0.dart @@ -0,0 +1,190 @@ +// Copyright 2014 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 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +void main() => runApp(const GridViewExampleApp()); + +class GridViewExampleApp extends StatelessWidget { + const GridViewExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Padding( + padding: const EdgeInsets.all(20.0), + child: Card( + elevation: 8.0, + child: GridView.builder( + padding: const EdgeInsets.all(12.0), + gridDelegate: CustomGridDelegate(dimension: 240.0), + // Try uncommenting some of these properties to see the effect on the grid: + // itemCount: 20, // The default is that the number of grid tiles is infinite. + // scrollDirection: Axis.horizontal, // The default is vertical. + // reverse: true, // The default is false, going down (or left to right). + itemBuilder: (BuildContext context, int index) { + final math.Random random = math.Random(index); + return GridTile( + header: GridTileBar( + title: Text('$index', style: const TextStyle(color: Colors.black)), + ), + child: Container( + margin: const EdgeInsets.all(12.0), + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + gradient: const RadialGradient( + colors: [ Color(0x0F88EEFF), Color(0x2F0099BB) ], + ), + ), + child: FlutterLogo( + style: FlutterLogoStyle.values[random.nextInt(FlutterLogoStyle.values.length)], + ), + ), + ); + }, + ), + ), + ), + ); + } +} + +class CustomGridDelegate extends SliverGridDelegate { + CustomGridDelegate({ required this.dimension }); + + // This is the desired height of each row (and width of each square). + // When there is not enough room, we shrink this to the width of the scroll view. + final double dimension; + + // The layout is two rows of squares, then one very wide cell, repeat. + + @override + SliverGridLayout getLayout(SliverConstraints constraints) { + // Determine how many squares we can fit per row. + int count = constraints.crossAxisExtent ~/ dimension; + if (count < 1) { + count = 1; // Always fit at least one regardless. + } + final double squareDimension = constraints.crossAxisExtent / count; + return CustomGridLayout( + crossAxisCount: count, + fullRowPeriod: 3, // Number of rows per block (one of which is the full row). + dimension: squareDimension, + ); + } + + @override + bool shouldRelayout(CustomGridDelegate oldDelegate) { + return dimension != oldDelegate.dimension; + } +} + +class CustomGridLayout extends SliverGridLayout { + const CustomGridLayout({ + required this.crossAxisCount, + required this.dimension, + required this.fullRowPeriod, + }) : assert(crossAxisCount > 0), + assert(fullRowPeriod > 1), + loopLength = crossAxisCount * (fullRowPeriod - 1) + 1, + loopHeight = fullRowPeriod * dimension; + + final int crossAxisCount; + final double dimension; + final int fullRowPeriod; + + // Computed values. + final int loopLength; + final double loopHeight; + + @override + double computeMaxScrollOffset(int childCount) { + // This returns the scroll offset of the end side of the childCount'th child. + // In the case of this example, this method is not used, since the grid is + // infinite. However, if one set an itemCount on the GridView above, this + // function would be used to determine how far to allow the user to scroll. + if (childCount == 0 || dimension == 0) { + return 0; + } + return (childCount ~/ loopLength) * loopHeight + + ((childCount % loopLength) ~/ crossAxisCount) * dimension; + } + + @override + SliverGridGeometry getGeometryForChildIndex(int index) { + // This returns the position of the index'th tile. + // + // The SliverGridGeometry object returned from this method has four + // properties. For a grid that scrolls down, as in this example, the four + // properties are equivalent to x,y,width,height. However, since the + // GridView is direction agnostic, the names used for SliverGridGeometry are + // also direction-agnostic. + // + // Try changing the scrollDirection and reverse properties on the GridView + // to see how this algorithm works in any direction (and why, therefore, the + // names are direction-agnostic). + final int loop = index ~/ loopLength; + final int loopIndex = index % loopLength; + if (loopIndex == loopLength - 1) { + // Full width case. + return SliverGridGeometry( + scrollOffset: (loop + 1) * loopHeight - dimension, // "y" + crossAxisOffset: 0, // "x" + mainAxisExtent: dimension, // "height" + crossAxisExtent: crossAxisCount * dimension, // "width" + ); + } + // Square case. + final int rowIndex = loopIndex ~/ crossAxisCount; + final int columnIndex = loopIndex % crossAxisCount; + return SliverGridGeometry( + scrollOffset: (loop * loopHeight) + (rowIndex * dimension), // "y" + crossAxisOffset: columnIndex * dimension, // "x" + mainAxisExtent: dimension, // "height" + crossAxisExtent: dimension, // "width" + ); + } + + @override + int getMinChildIndexForScrollOffset(double scrollOffset) { + // This returns the first index that is visible for a given scrollOffset. + // + // The GridView only asks for the geometry of children that are visible + // between the scroll offset passed to getMinChildIndexForScrollOffset and + // the scroll offset passed to getMaxChildIndexForScrollOffset. + // + // It is the responsibility of the SliverGridLayout to ensure that + // getGeometryForChildIndex is consistent with getMinChildIndexForScrollOffset + // and getMaxChildIndexForScrollOffset. + // + // Not every child between the minimum child index and the maximum child + // index need be visible (some may have scroll offsets that are outside the + // view; this happens commonly when the grid view places tiles out of + // order). However, doing this means the grid view is less efficient, as it + // will do work for children that are not visible. It is preferred that the + // children are returned in the order that they are laid out. + final int rows = scrollOffset ~/ dimension; + final int loops = rows ~/ fullRowPeriod; + final int extra = rows % fullRowPeriod; + return loops * loopLength + extra * crossAxisCount; + } + + @override + int getMaxChildIndexForScrollOffset(double scrollOffset) { + // (See commentary above.) + final int rows = scrollOffset ~/ dimension; + final int loops = rows ~/ fullRowPeriod; + final int extra = rows % fullRowPeriod; + final int count = loops * loopLength + extra * crossAxisCount; + if (extra == fullRowPeriod - 1) { + return count; + } + return count + crossAxisCount - 1; + } +} diff --git a/examples/api/test/widgets/scroll_view/grid_view.0_test.dart b/examples/api/test/widgets/scroll_view/grid_view.0_test.dart new file mode 100644 index 0000000000..9368a091d3 --- /dev/null +++ b/examples/api/test/widgets/scroll_view/grid_view.0_test.dart @@ -0,0 +1,32 @@ +// Copyright 2014 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 'package:flutter_api_samples/widgets/scroll_view/grid_view.0.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('$CustomGridLayout', (WidgetTester tester) async { + const CustomGridLayout layout = CustomGridLayout( + crossAxisCount: 2, + fullRowPeriod: 3, + dimension: 100, + ); + final List scrollOffsets = List.generate(10, (int i) => layout.computeMaxScrollOffset(i)); + expect(scrollOffsets, [0.0, 0.0, 100.0, 100.0, 200.0, 300.0, 300.0, 400.0, 400.0, 500.0]); + final List minOffsets = List.generate(10, (int i) => layout.getMinChildIndexForScrollOffset(i * 80.0)); + expect(minOffsets, [0, 0, 2, 4, 5, 7, 7, 9, 10, 12]); + final List maxOffsets = List.generate(10, (int i) => layout.getMaxChildIndexForScrollOffset(i * 80.0)); + expect(maxOffsets, [1, 1, 3, 4, 6, 8, 8, 9, 11, 13]); + final List offsets = List.generate(20, (int i) => layout.getGeometryForChildIndex(i)); + offsets.reduce((SliverGridGeometry a, SliverGridGeometry b) { + if (a.scrollOffset == b.scrollOffset) { + expect(a.crossAxisOffset, lessThan(b.crossAxisOffset)); + } else { + expect(a.scrollOffset, lessThan(b.scrollOffset)); + } + return b; + }); + }); +} diff --git a/packages/flutter/lib/src/rendering/sliver_grid.dart b/packages/flutter/lib/src/rendering/sliver_grid.dart index 78d412edf4..d09aae2aa4 100644 --- a/packages/flutter/lib/src/rendering/sliver_grid.dart +++ b/packages/flutter/lib/src/rendering/sliver_grid.dart @@ -13,6 +13,16 @@ import 'sliver_multi_box_adaptor.dart'; /// Describes the placement of a child in a [RenderSliverGrid]. /// +/// This class is similar to [Rect], in that it gives a two-dimensional position +/// and a two-dimensional dimension, but is direction-agnostic. +/// +/// {@tool dartpad} +/// This example shows how a custom [SliverGridLayout] uses [SliverGridGeometry] +/// to lay out the children. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart ** +/// {@end-tool} +/// /// See also: /// /// * [SliverGridLayout], which represents the geometry of all the tiles in a @@ -60,7 +70,7 @@ class SliverGridGeometry { double get trailingScrollOffset => scrollOffset + mainAxisExtent; /// Returns a tight [BoxConstraints] that forces the child to have the - /// required size. + /// required size, given a [SliverConstraints]. BoxConstraints getBoxConstraints(SliverConstraints constraints) { return constraints.asBoxConstraints( minExtent: mainAxisExtent, @@ -83,13 +93,22 @@ class SliverGridGeometry { /// The size and position of all the tiles in a [RenderSliverGrid]. /// -/// Rather that providing a grid with a [SliverGridLayout] directly, you instead -/// provide the grid a [SliverGridDelegate], which can compute a -/// [SliverGridLayout] given the current [SliverConstraints]. +/// Rather that providing a grid with a [SliverGridLayout] directly, the grid is +/// provided a [SliverGridDelegate], which computes a [SliverGridLayout] given a +/// set of [SliverConstraints]. This allows the algorithm to dynamically respond +/// to changes in the environment (e.g. the user rotating the device). /// /// The tiles can be placed arbitrarily, but it is more efficient to place tiles -/// in roughly in order by scroll offset because grids reify a contiguous -/// sequence of children. +/// roughly in order by scroll offset because grids reify a contiguous sequence +/// of children. +/// +/// {@tool dartpad} +/// This example shows how to construct a custom [SliverGridLayout] to lay tiles +/// in a grid form with some cells stretched to fit the entire width of the +/// grid (sometimes called "hero tiles"). +/// +/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart ** +/// {@end-tool} /// /// See also: /// @@ -240,9 +259,16 @@ class SliverGridRegularTileLayout extends SliverGridLayout { /// /// Given the current constraints on the grid, a [SliverGridDelegate] computes /// the layout for the tiles in the grid. The tiles can be placed arbitrarily, -/// but it is more efficient to place tiles in roughly in order by scroll offset +/// but it is more efficient to place tiles roughly in order by scroll offset /// because grids reify a contiguous sequence of children. /// +/// {@tool dartpad} +/// This example shows how a [SliverGridDelegate] returns a [SliverGridLayout] +/// configured based on the provided [SliverConstraints] in [getLayout]. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart ** +/// {@end-tool} +/// /// See also: /// /// * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index 47a1bddc3a..c097a1b785 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -1681,6 +1681,10 @@ class ListView extends BoxScrollView { /// [SliverList] or [SliverAppBar], can be put in the [CustomScrollView.slivers] /// list. /// +/// {@macro flutter.widgets.ScrollView.PageStorage} +/// +/// ## Examples +/// /// {@tool snippet} /// This example demonstrates how to create a [GridView] with two columns. The /// children are spaced apart using the `crossAxisSpacing` and `mainAxisSpacing` @@ -1786,6 +1790,25 @@ class ListView extends BoxScrollView { /// ``` /// {@end-tool} /// +/// {@tool dartpad} +/// This example shows a custom implementation of selection in list and grid views. +/// Use the button in the top right (possibly hidden under the DEBUG banner) to toggle between +/// [ListView] and [GridView]. +/// Long press any [ListTile] or [GridTile] to enable selection mode. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/list_view.0.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This example shows a custom [SliverGridDelegate]. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart ** +/// {@end-tool} +/// +/// ## Troubleshooting +/// +/// ### Padding +/// /// By default, [GridView] will automatically pad the limits of the /// grid's scrollable to avoid partial obstructions indicated by /// [MediaQuery]'s padding. To avoid this behavior, override with a @@ -1817,15 +1840,6 @@ class ListView extends BoxScrollView { /// ``` /// {@end-tool} /// -/// {@tool dartpad} -/// This example shows a custom implementation of [ListTile] selection in a [GridView] or [ListView]. -/// Long press any ListTile to enable selection mode. -/// -/// ** See code in examples/api/lib/widgets/scroll_view/list_view.0.dart ** -/// {@end-tool} -/// -/// {@macro flutter.widgets.ScrollView.PageStorage} -/// /// See also: /// /// * [SingleChildScrollView], which is a scrollable widget that has a single