Skip to content

GridView sample code #131900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions examples/api/lib/widgets/scroll_view/grid_view.0.dart
Original file line number Diff line number Diff line change
@@ -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>[ 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;
}
}
32 changes: 32 additions & 0 deletions examples/api/test/widgets/scroll_view/grid_view.0_test.dart
Original file line number Diff line number Diff line change
@@ -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<double> scrollOffsets = List<double>.generate(10, (int i) => layout.computeMaxScrollOffset(i));
expect(scrollOffsets, <double>[0.0, 0.0, 100.0, 100.0, 200.0, 300.0, 300.0, 400.0, 400.0, 500.0]);
final List<int> minOffsets = List<int>.generate(10, (int i) => layout.getMinChildIndexForScrollOffset(i * 80.0));
expect(minOffsets, <int>[0, 0, 2, 4, 5, 7, 7, 9, 10, 12]);
final List<int> maxOffsets = List<int>.generate(10, (int i) => layout.getMaxChildIndexForScrollOffset(i * 80.0));
expect(maxOffsets, <double>[1, 1, 3, 4, 6, 8, 8, 9, 11, 13]);
final List<SliverGridGeometry> offsets = List<SliverGridGeometry>.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;
});
});
}
40 changes: 33 additions & 7 deletions packages/flutter/lib/src/rendering/sliver_grid.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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:
///
Expand Down Expand Up @@ -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
Expand Down
32 changes: 23 additions & 9 deletions packages/flutter/lib/src/widgets/scroll_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down