blob: 941875a4701cb6e2982cb82273dd2c518453a5c8 [file] [log] [blame]
// 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 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'base_grid_layout.dart';
import 'render_dynamic_grid.dart';
import 'wrap_layout.dart';
/// A [DynamicSliverGridLayout] that creates tiles with varying main axis
/// sizes and fixed cross axis sizes, generating a staggered layout. The extent
/// in the main axis will be defined by the child's size and must be finite.
/// Similar to Android's StaggeredGridLayoutManager:
/// [https://developer.android.com/reference/androidx/recyclerview/widget/StaggeredGridLayoutManager].
///
/// The tiles are placed in the column (or row in case [scrollDirection] is set
/// to [Axis.horizontal]) with the minimum extent, which means children are not
/// necessarily laid out in sequential order.
///
/// See also:
///
/// * [SliverGridWrappingTileLayout], a similar layout that allows tiles to be
/// freely sized in the main and cross axis.
/// * [DynamicSliverGridDelegateWithMaxCrossAxisExtent], which creates
/// staggered layouts with a maximum extent in the cross axis.
/// * [DynamicSliverGridDelegateWithFixedCrossAxisCount], which creates
/// staggered layouts with a consistent amount of tiles in the cross axis.
/// * [DynamicGridView.staggered], which uses these delegates to create
/// staggered layouts.
/// * [DynamicSliverGridGeometry], which establishes the position of a child
/// and pass the child's desired proportions to a [DynamicSliverGridLayout].
/// * [RenderDynamicSliverGrid], which is the sliver where the dynamic sized
/// tiles are positioned.
class SliverGridStaggeredTileLayout extends DynamicSliverGridLayout {
/// Creates a layout with dynamic main axis extents determined by the child's
/// size and fixed cross axis extents.
///
/// All arguments must be not null. The [crossAxisCount] argument must be
/// greater than zero. The [mainAxisSpacing], [crossAxisSpacing] and
/// [childCrossAxisExtent] arguments must not be negative.
SliverGridStaggeredTileLayout({
required this.crossAxisCount,
required this.mainAxisSpacing,
required this.crossAxisSpacing,
required this.childCrossAxisExtent,
required this.scrollDirection,
}) : assert(crossAxisCount != null && crossAxisCount > 0),
assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0);
/// The number of children in the cross axis.
final int crossAxisCount;
/// 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 cross axis.
final double childCrossAxisExtent;
/// The axis along which the scroll view scrolls.
final Axis scrollDirection;
/// The collection of scroll offsets for every row or column across the main
/// axis. It includes the tiles' sizes and the spacing between them.
final List<double> _scrollOffsetForMainAxis = <double>[];
/// The amount of tiles in every row or column across the main axis.
final List<int> _mainAxisCount = <int>[];
/// Returns the row or column with the minimum extent for the next child to
/// be laid out.
int _getNextCrossAxisSlot() {
int nextCrossAxisSlot = 0;
double minScrollOffset = double.infinity;
if (_scrollOffsetForMainAxis.length < crossAxisCount) {
nextCrossAxisSlot = _scrollOffsetForMainAxis.length;
_scrollOffsetForMainAxis.add(0.0);
return nextCrossAxisSlot;
}
for (int i = 0; i < crossAxisCount; i++) {
if (_scrollOffsetForMainAxis[i] < minScrollOffset) {
nextCrossAxisSlot = i;
minScrollOffset = _scrollOffsetForMainAxis[i];
}
}
return nextCrossAxisSlot;
}
@override
bool reachedTargetScrollOffset(double targetOffset) {
for (final double scrollOffset in _scrollOffsetForMainAxis) {
if (scrollOffset < targetOffset) {
return false;
}
}
return true;
}
@override
DynamicSliverGridGeometry getGeometryForChildIndex(int index) {
return DynamicSliverGridGeometry(
scrollOffset: 0.0,
crossAxisOffset: 0.0,
mainAxisExtent: double.infinity,
crossAxisExtent: childCrossAxisExtent,
);
}
@override
DynamicSliverGridGeometry updateGeometryForChildIndex(
int index,
Size childSize,
) {
final int crossAxisSlot = _getNextCrossAxisSlot();
final double currentScrollOffset = _scrollOffsetForMainAxis[crossAxisSlot];
final double childMainAxisExtent =
scrollDirection == Axis.vertical ? childSize.height : childSize.width;
final double scrollOffset = currentScrollOffset +
(_mainAxisCount.length >= crossAxisCount
? _mainAxisCount[crossAxisSlot]
: 0) *
mainAxisSpacing;
final double crossAxisOffset =
crossAxisSlot * (childCrossAxisExtent + crossAxisSpacing);
final double mainAxisExtent =
scrollDirection == Axis.vertical ? childSize.height : childSize.width;
_scrollOffsetForMainAxis[crossAxisSlot] =
childMainAxisExtent + _scrollOffsetForMainAxis[crossAxisSlot];
_mainAxisCount.length >= crossAxisCount
? _mainAxisCount[crossAxisSlot] += 1
: _mainAxisCount.add(1);
return DynamicSliverGridGeometry(
scrollOffset: scrollOffset,
crossAxisOffset: crossAxisOffset,
mainAxisExtent: mainAxisExtent,
crossAxisExtent: childCrossAxisExtent,
);
}
}
/// Creates dynamic grid layouts with a fixed number of tiles in the cross axis
/// and varying main axis size dependent on the child's corresponding finite
/// extent. It uses the same logic as
/// [SliverGridDelegateWithFixedCrossAxisCount] where the total extent in the
/// cross axis is distributed equally between the specified amount of tiles,
/// but with a [SliverGridStaggeredTileLayout].
///
/// For example, if the grid is vertical, this delegate will create a layout
/// with a fixed number of columns. If the grid is horizontal, this delegate
/// will create a layout with a fixed number of rows.
///
/// This sample code shows how to use it independently with a [DynamicGridView]
/// constructor:
///
/// ```dart
/// DynamicGridView(
/// gridDelegate: const DynamicSliverGridDelegateWithFixedCrossAxisCount(
/// crossAxisCount: 4,
/// ),
/// children: List<Widget>.generate(
/// 50,
/// (int index) => SizedBox(
/// height: index % 2 * 20 + 20,
/// child: Text('Index $index'),
/// ),
/// ),
/// );
/// ```
///
/// See also:
///
/// * [DynamicSliverGridDelegateWithMaxCrossAxisExtent], which creates a
/// dynamic layout with tiles that have a maximum cross-axis extent
/// and varying main axis size.
/// * [DynamicGridView], which can use this delegate to control the layout of
/// its tiles.
/// * [DynamicSliverGridGeometry], which establishes the position of a child
/// and pass the child's desired proportions to [DynamicSliverGridLayout].
/// * [RenderDynamicSliverGrid], which is the sliver where the dynamic sized
/// tiles are positioned.
class DynamicSliverGridDelegateWithFixedCrossAxisCount
extends SliverGridDelegateWithFixedCrossAxisCount {
/// Creates a delegate that makes grid layouts with a fixed number of tiles
/// in the cross axis and varying main axis size dependent on the child's
/// corresponding finite extent.
///
/// Only the [crossAxisCount] argument needs to be greater than zero. All of
/// them must be not null.
const DynamicSliverGridDelegateWithFixedCrossAxisCount({
required super.crossAxisCount,
super.mainAxisSpacing = 0.0,
super.crossAxisSpacing = 0.0,
}) : assert(crossAxisCount != null && crossAxisCount > 0),
assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
assert(crossAxisSpacing != null && crossAxisSpacing >= 0);
bool _debugAssertIsValid() {
assert(crossAxisCount > 0);
assert(mainAxisSpacing >= 0.0);
assert(crossAxisSpacing >= 0.0);
assert(childAspectRatio > 0.0);
return true;
}
@override
DynamicSliverGridLayout getLayout(SliverConstraints constraints) {
assert(_debugAssertIsValid());
final double usableCrossAxisExtent = math.max(
0.0,
constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1),
);
final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
return SliverGridStaggeredTileLayout(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing,
childCrossAxisExtent: childCrossAxisExtent,
scrollDirection: axisDirectionToAxis(constraints.axisDirection),
);
}
}
/// Creates dynamic grid layouts with tiles that each have a maximum cross-axis
/// extent and varying main axis size dependent on the child's corresponding
/// finite extent. It uses the same logic as
/// [SliverGridDelegateWithMaxCrossAxisExtent] where every tile has the same
/// cross axis size and does not exceed the provided max extent, but with a
/// [SliverGridStaggeredTileLayout].
///
/// This delegate will select a cross-axis extent for the tiles that is as
/// large as possible subject to the following conditions:
///
/// * The extent evenly divides the cross-axis extent of the grid.
/// * The extent is at most [maxCrossAxisExtent].
///
/// This sample code shows how to use it independently with a [DynamicGridView]
/// constructor:
///
/// ```dart
/// DynamicGridView(
/// gridDelegate: const DynamicSliverGridDelegateWithMaxCrossAxisExtent(
/// maxCrossAxisExtent: 100,
/// ),
/// children: List<Widget>.generate(
/// 50,
/// (int index) => SizedBox(
/// height: index % 2 * 20 + 20,
/// child: Text('Index $index'),
/// ),
/// ),
/// );
/// ```
///
/// See also:
///
/// * [DynamicSliverGridDelegateWithFixedCrossAxisCount], which creates a
/// layout with a fixed number of tiles in the cross axis.
/// * [DynamicGridView], which can use this delegate to control the layout of
/// its tiles.
/// * [DynamicSliverGridGeometry], which establishes the position of a child
/// and pass the child's desired proportions to [DynamicSliverGridLayout].
/// * [RenderDynamicSliverGrid], which is the sliver where the dynamic sized
/// tiles are positioned.
class DynamicSliverGridDelegateWithMaxCrossAxisExtent
extends SliverGridDelegateWithMaxCrossAxisExtent {
/// Creates a delegate that makes grid layouts with tiles that have a maximum
/// cross-axis extent and varying main axis size dependent on the child's
/// corresponding finite extent.
///
/// Only the [maxCrossAxisExtent] argument needs to be greater than zero.
/// All of them must be not null.
const DynamicSliverGridDelegateWithMaxCrossAxisExtent({
required super.maxCrossAxisExtent,
super.mainAxisSpacing = 0.0,
super.crossAxisSpacing = 0.0,
}) : assert(maxCrossAxisExtent != null && maxCrossAxisExtent > 0),
assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
assert(crossAxisSpacing != null && crossAxisSpacing >= 0);
bool _debugAssertIsValid(double crossAxisExtent) {
assert(crossAxisExtent > 0.0);
assert(maxCrossAxisExtent > 0.0);
assert(mainAxisSpacing >= 0.0);
assert(crossAxisSpacing >= 0.0);
assert(childAspectRatio > 0.0);
return true;
}
@override
DynamicSliverGridLayout getLayout(SliverConstraints constraints) {
assert(_debugAssertIsValid(constraints.crossAxisExtent));
final int crossAxisCount =
(constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing))
.ceil();
final double usableCrossAxisExtent = math.max(
0.0,
constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1),
);
final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
return SliverGridStaggeredTileLayout(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing,
childCrossAxisExtent: childCrossAxisExtent,
scrollDirection: axisDirectionToAxis(constraints.axisDirection),
);
}
}