sliver_grid.dart 25.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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;

xster's avatar
xster committed
7
import 'package:flutter/foundation.dart';
8 9 10 11 12 13 14 15 16 17

import 'box.dart';
import 'object.dart';
import 'sliver.dart';
import 'sliver_multi_box_adaptor.dart';

/// Describes the placement of a child in a [RenderSliverGrid].
///
/// See also:
///
18 19 20
///  * [SliverGridLayout], which represents the geometry of all the tiles in a
///    grid.
///  * [SliverGridLayout.getGeometryForChildIndex], which returns this object
21 22 23
///    to describe the child's placement.
///  * [RenderSliverGrid], which uses this class during its
///    [RenderSliverGrid.performLayout] method.
24
@immutable
25 26 27
class SliverGridGeometry {
  /// Creates an object that describes the placement of a child in a [RenderSliverGrid].
  const SliverGridGeometry({
28 29 30 31
    @required this.scrollOffset,
    @required this.crossAxisOffset,
    @required this.mainAxisExtent,
    @required this.crossAxisExtent,
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
  });

  /// The scroll offset of the leading edge of the child relative to the leading
  /// edge of the parent.
  final double scrollOffset;

  /// The offset of the child in the non-scrolling axis.
  ///
  /// If the scroll axis is vertical, this offset is from the left-most edge of
  /// the parent to the left-most edge of the child. If the scroll axis is
  /// horizontal, this offset is from the top-most edge of the parent to the
  /// top-most edge of the child.
  final double crossAxisOffset;

  /// The extent of the child in the scrolling axis.
  ///
  /// If the scroll axis is vertical, this extent is the child's height. If the
  /// scroll axis is horizontal, this extent is the child's width.
  final double mainAxisExtent;

  /// The extent of the child in the non-scrolling axis.
  ///
  /// If the scroll axis is vertical, this extent is the child's width. If the
  /// scroll axis is horizontal, this extent is the child's height.
  final double crossAxisExtent;

Adam Barth's avatar
Adam Barth committed
58 59 60 61
  /// The scroll offset of the trailing edge of the child relative to the
  /// leading edge of the parent.
  double get trailingScrollOffset => scrollOffset + mainAxisExtent;

62 63 64 65 66 67 68 69 70
  /// Returns a tight [BoxConstraints] that forces the child to have the
  /// required size.
  BoxConstraints getBoxConstraints(SliverConstraints constraints) {
    return constraints.asBoxConstraints(
      minExtent: mainAxisExtent,
      maxExtent: mainAxisExtent,
      crossAxisExtent: crossAxisExtent,
    );
  }
Adam Barth's avatar
Adam Barth committed
71 72 73

  @override
  String toString() {
74 75 76 77 78 79 80
    final List<String> properties = <String>[
      'scrollOffset: $scrollOffset',
      'crossAxisOffset: $crossAxisOffset',
      'mainAxisExtent: $mainAxisExtent',
      'crossAxisExtent: $crossAxisExtent',
    ];
    return 'SliverGridGeometry(${properties.join(', ')})';
Adam Barth's avatar
Adam Barth committed
81
  }
82 83
}

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
/// 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].
///
/// 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.
///
/// See also:
///
///  * [SliverGridRegularTileLayout], which represents a layout that uses
///    equally sized and spaced tiles.
///  * [SliverGridGeometry], which represents the size and position of a single
///    tile in a grid.
///  * [SliverGridDelegate.getLayout], which returns this object to describe the
101
///    delegate's layout.
102 103
///  * [RenderSliverGrid], which uses this class during its
///    [RenderSliverGrid.performLayout] method.
104
@immutable
105 106 107 108 109 110 111 112 113 114 115 116 117 118
abstract class SliverGridLayout {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const SliverGridLayout();

  /// The minimum child index that is visible at (or after) this scroll offset.
  int getMinChildIndexForScrollOffset(double scrollOffset);

  /// The maximum child index that is visible at (or before) this scroll offset.
  int getMaxChildIndexForScrollOffset(double scrollOffset);

  /// The size and position of the child with the given index.
  SliverGridGeometry getGeometryForChildIndex(int index);

119 120 121 122 123
  /// The scroll extent needed to fully display all the tiles if there are
  /// `childCount` children in total.
  ///
  /// The child count will never be null.
  double computeMaxScrollOffset(int childCount);
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
}

/// A [SliverGridLayout] that uses equally sized and spaced tiles.
///
/// 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].
///
/// This layout is used by [SliverGridDelegateWithFixedCrossAxisCount] and
/// [SliverGridDelegateWithMaxCrossAxisExtent].
///
/// See also:
///
///  * [SliverGridDelegateWithFixedCrossAxisCount], which uses this layout.
///  * [SliverGridDelegateWithMaxCrossAxisExtent], which uses this layout.
139
///  * [SliverGridLayout], which represents an arbitrary tile layout.
140 141 142
///  * [SliverGridGeometry], which represents the size and position of a single
///    tile in a grid.
///  * [SliverGridDelegate.getLayout], which returns this object to describe the
143
///    delegate's layout.
144 145 146 147 148 149 150
///  * [RenderSliverGrid], which uses this class during its
///    [RenderSliverGrid.performLayout] method.
class SliverGridRegularTileLayout extends SliverGridLayout {
  /// Creates a layout that uses equally sized and spaced tiles.
  ///
  /// All of the arguments must not be null and must not be negative. The
  /// `crossAxisCount` argument must be greater than zero.
151
  const SliverGridRegularTileLayout({
152 153 154 155 156
    @required this.crossAxisCount,
    @required this.mainAxisStride,
    @required this.crossAxisStride,
    @required this.childMainAxisExtent,
    @required this.childCrossAxisExtent,
157
    @required this.reverseCrossAxis,
158 159 160 161
  }) : assert(crossAxisCount != null && crossAxisCount > 0),
       assert(mainAxisStride != null && mainAxisStride >= 0),
       assert(crossAxisStride != null && crossAxisStride >= 0),
       assert(childMainAxisExtent != null && childMainAxisExtent >= 0),
162 163
       assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0),
       assert(reverseCrossAxis != null);
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182

  /// The number of children in the cross axis.
  final int crossAxisCount;

  /// The number of pixels from the leading edge of one tile to the leading edge
  /// of the next tile in the main axis.
  final double mainAxisStride;

  /// The number of pixels from the leading edge of one tile to the leading edge
  /// of the next tile in the cross axis.
  final double crossAxisStride;

  /// 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;
183

184 185 186 187 188 189 190 191 192 193 194
  /// Whether the children should be placed in the opposite order of increasing
  /// coordinates in the cross axis.
  ///
  /// For example, if the cross axis is horizontal, the children are placed from
  /// left to right when [reverseCrossAxis] is false and from right to left when
  /// [reverseCrossAxis] is true.
  ///
  /// Typically set to the return value of [axisDirectionIsReversed] applied to
  /// the [SliverConstraints.crossAxisDirection].
  final bool reverseCrossAxis;

195
  @override
196
  int getMinChildIndexForScrollOffset(double scrollOffset) {
197
    return mainAxisStride > 0.0 ? crossAxisCount * (scrollOffset ~/ mainAxisStride) : 0;
198 199 200 201
  }

  @override
  int getMaxChildIndexForScrollOffset(double scrollOffset) {
202 203 204 205 206
    if (mainAxisStride > 0.0) {
      final int mainAxisCount = (scrollOffset / mainAxisStride).ceil();
      return math.max(0, crossAxisCount * mainAxisCount - 1);
    }
    return 0;
207 208
  }

209 210
  double _getOffsetFromStartInCrossAxis(double crossAxisStart) {
    if (reverseCrossAxis)
211
      return crossAxisCount * crossAxisStride - crossAxisStart - childCrossAxisExtent - (crossAxisStride - childCrossAxisExtent);
212 213 214
    return crossAxisStart;
  }

215 216
  @override
  SliverGridGeometry getGeometryForChildIndex(int index) {
217
    final double crossAxisStart = (index % crossAxisCount) * crossAxisStride;
218
    return SliverGridGeometry(
219
      scrollOffset: (index ~/ crossAxisCount) * mainAxisStride,
220
      crossAxisOffset: _getOffsetFromStartInCrossAxis(crossAxisStart),
221 222 223 224 225 226
      mainAxisExtent: childMainAxisExtent,
      crossAxisExtent: childCrossAxisExtent,
    );
  }

  @override
227 228
  double computeMaxScrollOffset(int childCount) {
    assert(childCount != null);
229
    final int mainAxisCount = ((childCount - 1) ~/ crossAxisCount) + 1;
230 231 232
    final double mainAxisSpacing = mainAxisStride - childMainAxisExtent;
    return mainAxisStride * mainAxisCount - mainAxisSpacing;
  }
233 234
}

235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
/// Controls the layout of tiles in a grid.
///
/// 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
/// because grids reify a contiguous sequence of children.
///
/// See also:
///
///  * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with
///    a fixed number of tiles in the cross axis.
///  * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with
///    tiles that have a maximum cross-axis extent.
///  * [GridView], which uses this delegate to control the layout of its tiles.
///  * [SliverGrid], which uses this delegate to control the layout of its
///    tiles.
///  * [RenderSliverGrid], which uses this delegate to control the layout of its
///    tiles.
253 254 255 256 257
abstract class SliverGridDelegate {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const SliverGridDelegate();

258 259
  /// Returns information about the size and position of the tiles in the grid.
  SliverGridLayout getLayout(SliverConstraints constraints);
260

261 262 263 264 265 266
  /// Override this method to return true when the children need to be
  /// laid out.
  ///
  /// This should compare the fields of the current delegate and the given
  /// `oldDelegate` and return true if the fields are such that the layout would
  /// be different.
267
  bool shouldRelayout(covariant SliverGridDelegate oldDelegate);
268 269
}

270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
/// Creates grid layouts with a fixed number of tiles in the cross axis.
///
/// 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 delegate creates grids with equally sized and spaced tiles.
///
/// See also:
///
///  * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with
///    tiles that have a maximum cross-axis extent.
///  * [SliverGridDelegate], which creates arbitrary layouts.
///  * [GridView], which can use this delegate to control the layout of its
///    tiles.
///  * [SliverGrid], which can use this delegate to control the layout of its
///    tiles.
///  * [RenderSliverGrid], which can use this delegate to control the layout of
///    its tiles.
289
class SliverGridDelegateWithFixedCrossAxisCount extends SliverGridDelegate {
290 291 292 293 294 295
  /// Creates a delegate that makes grid layouts with a fixed number of tiles in
  /// the cross axis.
  ///
  /// All of the arguments must not be null. The `mainAxisSpacing` and
  /// `crossAxisSpacing` arguments must not be negative. The `crossAxisCount`
  /// and `childAspectRatio` arguments must be greater than zero.
296 297
  const SliverGridDelegateWithFixedCrossAxisCount({
    @required this.crossAxisCount,
298 299 300
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
301 302 303 304
  }) : assert(crossAxisCount != null && crossAxisCount > 0),
       assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
       assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
       assert(childAspectRatio != null && childAspectRatio > 0);
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326

  /// 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 ratio of the cross-axis to the main-axis extent of each child.
  final double childAspectRatio;

  bool _debugAssertIsValid() {
    assert(crossAxisCount > 0);
    assert(mainAxisSpacing >= 0.0);
    assert(crossAxisSpacing >= 0.0);
    assert(childAspectRatio > 0.0);
    return true;
  }

  @override
327
  SliverGridLayout getLayout(SliverConstraints constraints) {
328 329 330 331
    assert(_debugAssertIsValid());
    final double usableCrossAxisExtent = constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);
    final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
    final double childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
332
    return SliverGridRegularTileLayout(
333 334 335 336 337
      crossAxisCount: crossAxisCount,
      mainAxisStride: childMainAxisExtent + mainAxisSpacing,
      crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
      childMainAxisExtent: childMainAxisExtent,
      childCrossAxisExtent: childCrossAxisExtent,
338
      reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
339 340 341 342 343 344 345 346 347 348 349 350
    );
  }

  @override
  bool shouldRelayout(SliverGridDelegateWithFixedCrossAxisCount oldDelegate) {
    return oldDelegate.crossAxisCount != crossAxisCount
        || oldDelegate.mainAxisSpacing != mainAxisSpacing
        || oldDelegate.crossAxisSpacing != crossAxisSpacing
        || oldDelegate.childAspectRatio != childAspectRatio;
  }
}

351
/// Creates grid layouts with tiles that each have a maximum cross-axis extent.
352 353 354
///
/// This delegate will select a cross-axis extent for the tiles that is as
/// large as possible subject to the following conditions:
355
///
356 357
///  - The extent evenly divides the cross-axis extent of the grid.
///  - The extent is at most [maxCrossAxisExtent].
358
///
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
/// For example, if the grid is vertical, the grid is 500.0 pixels wide, and
/// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4
/// columns that are 125.0 pixels wide.
///
/// This delegate creates grids with equally sized and spaced tiles.
///
/// See also:
///
///  * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with
///    a fixed number of tiles in the cross axis.
///  * [SliverGridDelegate], which creates arbitrary layouts.
///  * [GridView], which can use this delegate to control the layout of its
///    tiles.
///  * [SliverGrid], which can use this delegate to control the layout of its
///    tiles.
///  * [RenderSliverGrid], which can use this delegate to control the layout of
///    its tiles.
376
class SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate {
377 378
  /// Creates a delegate that makes grid layouts with tiles that have a maximum
  /// cross-axis extent.
379
  ///
380 381 382
  /// All of the arguments must not be null. The [maxCrossAxisExtent] and
  /// [mainAxisSpacing], and [crossAxisSpacing] arguments must not be negative.
  /// The [childAspectRatio] argument must be greater than zero.
383 384
  const SliverGridDelegateWithMaxCrossAxisExtent({
    @required this.maxCrossAxisExtent,
385 386 387
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
388 389 390 391
  }) : assert(maxCrossAxisExtent != null && maxCrossAxisExtent >= 0),
       assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
       assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
       assert(childAspectRatio != null && childAspectRatio > 0);
392

393 394 395 396 397 398 399 400 401 402 403
  /// The maximum extent of tiles in the cross axis.
  ///
  /// 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].
  ///
  /// For example, if the grid is vertical, the grid is 500.0 pixels wide, and
  /// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4
  /// columns that are 125.0 pixels wide.
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
  final double maxCrossAxisExtent;

  /// 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 ratio of the cross-axis to the main-axis extent of each child.
  final double childAspectRatio;

  bool _debugAssertIsValid() {
    assert(maxCrossAxisExtent > 0.0);
    assert(mainAxisSpacing >= 0.0);
    assert(crossAxisSpacing >= 0.0);
    assert(childAspectRatio > 0.0);
    return true;
  }

  @override
424
  SliverGridLayout getLayout(SliverConstraints constraints) {
425
    assert(_debugAssertIsValid());
426
    final int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil();
427 428 429
    final double usableCrossAxisExtent = constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);
    final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
    final double childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
430
    return SliverGridRegularTileLayout(
431 432 433 434 435
      crossAxisCount: crossAxisCount,
      mainAxisStride: childMainAxisExtent + mainAxisSpacing,
      crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
      childMainAxisExtent: childMainAxisExtent,
      childCrossAxisExtent: childCrossAxisExtent,
436
      reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
437 438 439 440 441 442 443 444 445 446 447 448
    );
  }

  @override
  bool shouldRelayout(SliverGridDelegateWithMaxCrossAxisExtent oldDelegate) {
    return oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent
        || oldDelegate.mainAxisSpacing != mainAxisSpacing
        || oldDelegate.crossAxisSpacing != crossAxisSpacing
        || oldDelegate.childAspectRatio != childAspectRatio;
  }
}

449
/// Parent data structure used by [RenderSliverGrid].
450
class SliverGridParentData extends SliverMultiBoxAdaptorParentData {
451 452 453 454 455 456
  /// The offset of the child in the non-scrolling axis.
  ///
  /// If the scroll axis is vertical, this offset is from the left-most edge of
  /// the parent to the left-most edge of the child. If the scroll axis is
  /// horizontal, this offset is from the top-most edge of the parent to the
  /// top-most edge of the child.
457 458 459 460 461 462
  double crossAxisOffset;

  @override
  String toString() => 'crossAxisOffset=$crossAxisOffset; ${super.toString()}';
}

463
/// A sliver that places multiple box children in a two dimensional arrangement.
464 465 466 467 468 469 470 471 472 473 474
///
/// [RenderSliverGrid] places its children in arbitrary positions determined by
/// [gridDelegate]. Each child is forced to have the size specified by the
/// [gridDelegate].
///
/// See also:
///
///  * [RenderSliverList], which places its children in a linear
///    array.
///  * [RenderSliverFixedExtentList], which places its children in a linear
///    array with a fixed extent in the main axis.
475
class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
476 477 478 479
  /// Creates a sliver that contains multiple box children that whose size and
  /// position are determined by a delegate.
  ///
  /// The [childManager] and [gridDelegate] arguments must not be null.
480 481 482
  RenderSliverGrid({
    @required RenderSliverBoxChildManager childManager,
    @required SliverGridDelegate gridDelegate,
483 484 485
  }) : assert(gridDelegate != null),
       _gridDelegate = gridDelegate,
       super(childManager: childManager);
486 487 488 489

  @override
  void setupParentData(RenderObject child) {
    if (child.parentData is! SliverGridParentData)
490
      child.parentData = SliverGridParentData();
491 492
  }

493
  /// The delegate that controls the size and position of the children.
494 495
  SliverGridDelegate get gridDelegate => _gridDelegate;
  SliverGridDelegate _gridDelegate;
496 497 498
  set gridDelegate(SliverGridDelegate value) {
    assert(value != null);
    if (_gridDelegate == value)
499
      return;
500 501
    if (value.runtimeType != _gridDelegate.runtimeType ||
        value.shouldRelayout(_gridDelegate))
502
      markNeedsLayout();
503
    _gridDelegate = value;
504 505 506 507
  }

  @override
  double childCrossAxisPosition(RenderBox child) {
508
    final SliverGridParentData childParentData = child.parentData as SliverGridParentData;
509 510 511 512 513
    return childParentData.crossAxisOffset;
  }

  @override
  void performLayout() {
514
    childManager.didStartLayout();
515 516
    childManager.setDidUnderflow(false);

517
    final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
518
    assert(scrollOffset >= 0.0);
519 520 521
    final double remainingExtent = constraints.remainingCacheExtent;
    assert(remainingExtent >= 0.0);
    final double targetEndScrollOffset = scrollOffset + remainingExtent;
522

523 524 525
    final SliverGridLayout layout = _gridDelegate.getLayout(constraints);

    final int firstIndex = layout.getMinChildIndexForScrollOffset(scrollOffset);
526 527
    final int targetLastIndex = targetEndScrollOffset.isFinite ?
      layout.getMaxChildIndexForScrollOffset(targetEndScrollOffset) : null;
528 529 530 531

    if (firstChild != null) {
      final int oldFirstIndex = indexOf(firstChild);
      final int oldLastIndex = indexOf(lastChild);
532 533 534 535
      final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount) as int;
      final int trailingGarbage = targetLastIndex == null
        ? 0
        : ((oldLastIndex - targetLastIndex).clamp(0, childCount) as int);
536 537 538
      collectGarbage(leadingGarbage, trailingGarbage);
    } else {
      collectGarbage(0, 0);
539 540
    }

541
    final SliverGridGeometry firstChildGridGeometry = layout.getGeometryForChildIndex(firstIndex);
542
    final double leadingScrollOffset = firstChildGridGeometry.scrollOffset;
Adam Barth's avatar
Adam Barth committed
543
    double trailingScrollOffset = firstChildGridGeometry.trailingScrollOffset;
544 545

    if (firstChild == null) {
546 547 548
      if (!addInitialChild(index: firstIndex, layoutOffset: firstChildGridGeometry.scrollOffset)) {
        // There are either no children, or we are past the end of all our children.
        final double max = layout.computeMaxScrollOffset(childManager.childCount);
549
        geometry = SliverGeometry(
550 551 552
          scrollExtent: max,
          maxPaintExtent: max,
        );
553
        childManager.didFinishLayout();
554 555 556 557 558 559 560
        return;
      }
    }

    RenderBox trailingChildWithLayout;

    for (int index = indexOf(firstChild) - 1; index >= firstIndex; --index) {
561
      final SliverGridGeometry gridGeometry = layout.getGeometryForChildIndex(index);
562
      final RenderBox child = insertAndLayoutLeadingChild(
Ian Hickson's avatar
Ian Hickson committed
563 564
        gridGeometry.getBoxConstraints(constraints),
      );
565
      final SliverGridParentData childParentData = child.parentData as SliverGridParentData;
566
      childParentData.layoutOffset = gridGeometry.scrollOffset;
567 568 569
      childParentData.crossAxisOffset = gridGeometry.crossAxisOffset;
      assert(childParentData.index == index);
      trailingChildWithLayout ??= child;
Adam Barth's avatar
Adam Barth committed
570
      trailingScrollOffset = math.max(trailingScrollOffset, gridGeometry.trailingScrollOffset);
571 572 573 574
    }

    if (trailingChildWithLayout == null) {
      firstChild.layout(firstChildGridGeometry.getBoxConstraints(constraints));
575
      final SliverGridParentData childParentData = firstChild.parentData as SliverGridParentData;
576
      childParentData.layoutOffset = firstChildGridGeometry.scrollOffset;
577 578 579 580
      childParentData.crossAxisOffset = firstChildGridGeometry.crossAxisOffset;
      trailingChildWithLayout = firstChild;
    }

581
    for (int index = indexOf(trailingChildWithLayout) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) {
582 583
      final SliverGridGeometry gridGeometry = layout.getGeometryForChildIndex(index);
      final BoxConstraints childConstraints = gridGeometry.getBoxConstraints(constraints);
584
      RenderBox child = childAfter(trailingChildWithLayout);
585
      if (child == null || indexOf(child) != index) {
586 587 588 589 590 591 592 593 594 595
        child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout);
        if (child == null) {
          // We have run out of children.
          break;
        }
      } else {
        child.layout(childConstraints);
      }
      trailingChildWithLayout = child;
      assert(child != null);
596
      final SliverGridParentData childParentData = child.parentData as SliverGridParentData;
597
      childParentData.layoutOffset = gridGeometry.scrollOffset;
598 599
      childParentData.crossAxisOffset = gridGeometry.crossAxisOffset;
      assert(childParentData.index == index);
Adam Barth's avatar
Adam Barth committed
600
      trailingScrollOffset = math.max(trailingScrollOffset, gridGeometry.trailingScrollOffset);
601 602 603 604
    }

    final int lastIndex = indexOf(lastChild);

605
    assert(childScrollOffset(firstChild) <= scrollOffset);
606 607
    assert(debugAssertChildListIsNonEmptyAndContiguous());
    assert(indexOf(firstChild) == firstIndex);
608
    assert(targetLastIndex == null || lastIndex <= targetLastIndex);
609 610 611 612 613 614 615 616 617

    final double estimatedTotalExtent = childManager.estimateMaxScrollOffset(
      constraints,
      firstIndex: firstIndex,
      lastIndex: lastIndex,
      leadingScrollOffset: leadingScrollOffset,
      trailingScrollOffset: trailingScrollOffset,
    );

Adam Barth's avatar
Adam Barth committed
618
    final double paintExtent = calculatePaintOffset(
619 620 621 622
      constraints,
      from: leadingScrollOffset,
      to: trailingScrollOffset,
    );
623 624 625 626 627
    final double cacheExtent = calculateCacheOffset(
      constraints,
      from: leadingScrollOffset,
      to: trailingScrollOffset,
    );
628

629
    geometry = SliverGeometry(
630
      scrollExtent: estimatedTotalExtent,
Adam Barth's avatar
Adam Barth committed
631
      paintExtent: paintExtent,
632
      maxPaintExtent: estimatedTotalExtent,
633
      cacheExtent: cacheExtent,
634 635 636 637
      // Conservative to avoid complexity.
      hasVisualOverflow: true,
    );

638 639 640 641
    // We may have started the layout while scrolled to the end, which
    // would not expose a new child.
    if (estimatedTotalExtent == trailingScrollOffset)
      childManager.setDidUnderflow(true);
642
    childManager.didFinishLayout();
643 644
  }
}