| // 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/foundation.dart'; |
| |
| import 'box.dart'; |
| import 'sliver.dart'; |
| import 'sliver_multi_box_adaptor.dart'; |
| |
| /// A sliver that contains multiple box children that have the same extent in |
| /// the main axis. |
| /// |
| /// [RenderSliverFixedExtentBoxAdaptor] places its children in a linear array |
| /// along the main axis. Each child is forced to have the [itemExtent] in the |
| /// main axis and the [SliverConstraints.crossAxisExtent] in the cross axis. |
| /// |
| /// Subclasses should override [itemExtent] to control the size of the children |
| /// in the main axis. For a concrete subclass with a configurable [itemExtent], |
| /// see [RenderSliverFixedExtentList]. |
| /// |
| /// [RenderSliverFixedExtentBoxAdaptor] is more efficient than |
| /// [RenderSliverList] because [RenderSliverFixedExtentBoxAdaptor] does not need |
| /// to perform layout on its children to obtain their extent in the main axis. |
| /// |
| /// See also: |
| /// |
| /// * [RenderSliverFixedExtentList], which has a configurable [itemExtent]. |
| /// * [RenderSliverFillViewport], which determines the [itemExtent] based on |
| /// [SliverConstraints.viewportMainAxisExtent]. |
| /// * [RenderSliverFillRemaining], which determines the [itemExtent] based on |
| /// [SliverConstraints.remainingPaintExtent]. |
| /// * [RenderSliverList], which does not require its children to have the same |
| /// extent in the main axis. |
| abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { |
| /// Creates a sliver that contains multiple box children that have the same |
| /// extent in the main axis. |
| /// |
| /// The [childManager] argument must not be null. |
| RenderSliverFixedExtentBoxAdaptor({ |
| @required RenderSliverBoxChildManager childManager, |
| }) : super(childManager: childManager); |
| |
| /// The main-axis extent of each item. |
| double get itemExtent; |
| |
| /// The layout offset for the child with the given index. |
| /// |
| /// This function is given the [itemExtent] as an argument to avoid |
| /// recomputing [itemExtent] repeatedly during layout. |
| /// |
| /// By default, places the children in order, without gaps, starting from |
| /// layout offset zero. |
| @protected |
| double indexToLayoutOffset(double itemExtent, int index) => itemExtent * index; |
| |
| /// The minimum child index that is visible at the given scroll offset. |
| /// |
| /// This function is given the [itemExtent] as an argument to avoid |
| /// recomputing [itemExtent] repeatedly during layout. |
| /// |
| /// By default, returns a value consistent with the children being placed in |
| /// order, without gaps, starting from layout offset zero. |
| @protected |
| int getMinChildIndexForScrollOffset(double scrollOffset, double itemExtent) { |
| if (itemExtent > 0.0) { |
| final double actual = scrollOffset / itemExtent; |
| final int round = actual.round(); |
| if ((actual - round).abs() < precisionErrorTolerance) { |
| return round; |
| } |
| return actual.floor(); |
| } |
| return 0; |
| } |
| |
| /// The maximum child index that is visible at the given scroll offset. |
| /// |
| /// This function is given the [itemExtent] as an argument to avoid |
| /// recomputing [itemExtent] repeatedly during layout. |
| /// |
| /// By default, returns a value consistent with the children being placed in |
| /// order, without gaps, starting from layout offset zero. |
| @protected |
| int getMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) { |
| return itemExtent > 0.0 ? math.max(0, (scrollOffset / itemExtent).ceil() - 1) : 0; |
| } |
| |
| /// Called to estimate the total scrollable extents of this object. |
| /// |
| /// Must return the total distance from the start of the child with the |
| /// earliest possible index to the end of the child with the last possible |
| /// index. |
| /// |
| /// By default, defers to [RenderSliverBoxChildManager.estimateMaxScrollOffset]. |
| /// |
| /// See also: |
| /// |
| /// * [computeMaxScrollOffset], which is similar but must provide a precise |
| /// value. |
| @protected |
| double estimateMaxScrollOffset( |
| SliverConstraints constraints, { |
| int firstIndex, |
| int lastIndex, |
| double leadingScrollOffset, |
| double trailingScrollOffset, |
| }) { |
| return childManager.estimateMaxScrollOffset( |
| constraints, |
| firstIndex: firstIndex, |
| lastIndex: lastIndex, |
| leadingScrollOffset: leadingScrollOffset, |
| trailingScrollOffset: trailingScrollOffset, |
| ); |
| } |
| |
| /// Called to obtain a precise measure of the total scrollable extents of this |
| /// object. |
| /// |
| /// Must return the precise total distance from the start of the child with |
| /// the earliest possible index to the end of the child with the last possible |
| /// index. |
| /// |
| /// This is used when no child is available for the index corresponding to the |
| /// current scroll offset, to determine the precise dimensions of the sliver. |
| /// It must return a precise value. It will not be called if the |
| /// [childManager] returns an infinite number of children for positive |
| /// indices. |
| /// |
| /// By default, multiplies the [itemExtent] by the number of children reported |
| /// by [RenderSliverBoxChildManager.childCount]. |
| /// |
| /// See also: |
| /// |
| /// * [estimateMaxScrollOffset], which is similar but may provide inaccurate |
| /// values. |
| @protected |
| double computeMaxScrollOffset(SliverConstraints constraints, double itemExtent) { |
| return childManager.childCount * itemExtent; |
| } |
| |
| int _calculateLeadingGarbage(int firstIndex) { |
| RenderBox walker = firstChild; |
| int leadingGarbage = 0; |
| while(walker != null && indexOf(walker) < firstIndex){ |
| leadingGarbage += 1; |
| walker = childAfter(walker); |
| } |
| return leadingGarbage; |
| } |
| |
| int _calculateTrailingGarbage(int targetLastIndex) { |
| RenderBox walker = lastChild; |
| int trailingGarbage = 0; |
| while(walker != null && indexOf(walker) > targetLastIndex){ |
| trailingGarbage += 1; |
| walker = childBefore(walker); |
| } |
| return trailingGarbage; |
| } |
| |
| @override |
| void performLayout() { |
| final SliverConstraints constraints = this.constraints; |
| childManager.didStartLayout(); |
| childManager.setDidUnderflow(false); |
| |
| final double itemExtent = this.itemExtent; |
| |
| final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin; |
| assert(scrollOffset >= 0.0); |
| final double remainingExtent = constraints.remainingCacheExtent; |
| assert(remainingExtent >= 0.0); |
| final double targetEndScrollOffset = scrollOffset + remainingExtent; |
| |
| final BoxConstraints childConstraints = constraints.asBoxConstraints( |
| minExtent: itemExtent, |
| maxExtent: itemExtent, |
| ); |
| |
| final int firstIndex = getMinChildIndexForScrollOffset(scrollOffset, itemExtent); |
| final int targetLastIndex = targetEndScrollOffset.isFinite ? |
| getMaxChildIndexForScrollOffset(targetEndScrollOffset, itemExtent) : null; |
| |
| if (firstChild != null) { |
| final int leadingGarbage = _calculateLeadingGarbage(firstIndex); |
| final int trailingGarbage = _calculateTrailingGarbage(targetLastIndex); |
| collectGarbage(leadingGarbage, trailingGarbage); |
| } else { |
| collectGarbage(0, 0); |
| } |
| |
| if (firstChild == null) { |
| if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(itemExtent, firstIndex))) { |
| // There are either no children, or we are past the end of all our children. |
| // If it is the latter, we will need to find the first available child. |
| double max; |
| if (childManager.childCount != null) { |
| max = computeMaxScrollOffset(constraints, itemExtent); |
| } else if (firstIndex <= 0) { |
| max = 0.0; |
| } else { |
| // We will have to find it manually. |
| int possibleFirstIndex = firstIndex - 1; |
| while ( |
| possibleFirstIndex > 0 && |
| !addInitialChild( |
| index: possibleFirstIndex, |
| layoutOffset: indexToLayoutOffset(itemExtent, possibleFirstIndex), |
| ) |
| ) { |
| possibleFirstIndex -= 1; |
| } |
| max = possibleFirstIndex * itemExtent; |
| } |
| geometry = SliverGeometry( |
| scrollExtent: max, |
| maxPaintExtent: max, |
| ); |
| childManager.didFinishLayout(); |
| return; |
| } |
| } |
| |
| RenderBox trailingChildWithLayout; |
| |
| for (int index = indexOf(firstChild) - 1; index >= firstIndex; --index) { |
| final RenderBox child = insertAndLayoutLeadingChild(childConstraints); |
| if (child == null) { |
| // Items before the previously first child are no longer present. |
| // Reset the scroll offset to offset all items prior and up to the |
| // missing item. Let parent re-layout everything. |
| geometry = SliverGeometry(scrollOffsetCorrection: index * itemExtent); |
| return; |
| } |
| final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData; |
| childParentData.layoutOffset = indexToLayoutOffset(itemExtent, index); |
| assert(childParentData.index == index); |
| trailingChildWithLayout ??= child; |
| } |
| |
| if (trailingChildWithLayout == null) { |
| firstChild.layout(childConstraints); |
| final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData; |
| childParentData.layoutOffset = indexToLayoutOffset(itemExtent, firstIndex); |
| trailingChildWithLayout = firstChild; |
| } |
| |
| double estimatedMaxScrollOffset = double.infinity; |
| for (int index = indexOf(trailingChildWithLayout) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) { |
| RenderBox child = childAfter(trailingChildWithLayout); |
| if (child == null || indexOf(child) != index) { |
| child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout); |
| if (child == null) { |
| // We have run out of children. |
| estimatedMaxScrollOffset = index * itemExtent; |
| break; |
| } |
| } else { |
| child.layout(childConstraints); |
| } |
| trailingChildWithLayout = child; |
| assert(child != null); |
| final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData; |
| assert(childParentData.index == index); |
| childParentData.layoutOffset = indexToLayoutOffset(itemExtent, childParentData.index); |
| } |
| |
| final int lastIndex = indexOf(lastChild); |
| final double leadingScrollOffset = indexToLayoutOffset(itemExtent, firstIndex); |
| final double trailingScrollOffset = indexToLayoutOffset(itemExtent, lastIndex + 1); |
| |
| assert(firstIndex == 0 || childScrollOffset(firstChild) - scrollOffset <= precisionErrorTolerance); |
| assert(debugAssertChildListIsNonEmptyAndContiguous()); |
| assert(indexOf(firstChild) == firstIndex); |
| assert(targetLastIndex == null || lastIndex <= targetLastIndex); |
| |
| estimatedMaxScrollOffset = math.min( |
| estimatedMaxScrollOffset, |
| estimateMaxScrollOffset( |
| constraints, |
| firstIndex: firstIndex, |
| lastIndex: lastIndex, |
| leadingScrollOffset: leadingScrollOffset, |
| trailingScrollOffset: trailingScrollOffset, |
| ), |
| ); |
| |
| final double paintExtent = calculatePaintOffset( |
| constraints, |
| from: leadingScrollOffset, |
| to: trailingScrollOffset, |
| ); |
| |
| final double cacheExtent = calculateCacheOffset( |
| constraints, |
| from: leadingScrollOffset, |
| to: trailingScrollOffset, |
| ); |
| |
| final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent; |
| final int targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite ? |
| getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint, itemExtent) : null; |
| geometry = SliverGeometry( |
| scrollExtent: estimatedMaxScrollOffset, |
| paintExtent: paintExtent, |
| cacheExtent: cacheExtent, |
| maxPaintExtent: estimatedMaxScrollOffset, |
| // Conservative to avoid flickering away the clip during scroll. |
| hasVisualOverflow: (targetLastIndexForPaint != null && lastIndex >= targetLastIndexForPaint) |
| || constraints.scrollOffset > 0.0, |
| ); |
| |
| // We may have started the layout while scrolled to the end, which would not |
| // expose a new child. |
| if (estimatedMaxScrollOffset == trailingScrollOffset) |
| childManager.setDidUnderflow(true); |
| childManager.didFinishLayout(); |
| } |
| } |
| |
| /// A sliver that places multiple box children with the same main axis extent in |
| /// a linear array. |
| /// |
| /// [RenderSliverFixedExtentList] places its children in a linear array along |
| /// the main axis starting at offset zero and without gaps. Each child is forced |
| /// to have the [itemExtent] in the main axis and the |
| /// [SliverConstraints.crossAxisExtent] in the cross axis. |
| /// |
| /// [RenderSliverFixedExtentList] is more efficient than [RenderSliverList] |
| /// because [RenderSliverFixedExtentList] does not need to perform layout on its |
| /// children to obtain their extent in the main axis. |
| /// |
| /// See also: |
| /// |
| /// * [RenderSliverList], which does not require its children to have the same |
| /// extent in the main axis. |
| /// * [RenderSliverFillViewport], which determines the [itemExtent] based on |
| /// [SliverConstraints.viewportMainAxisExtent]. |
| /// * [RenderSliverFillRemaining], which determines the [itemExtent] based on |
| /// [SliverConstraints.remainingPaintExtent]. |
| class RenderSliverFixedExtentList extends RenderSliverFixedExtentBoxAdaptor { |
| /// Creates a sliver that contains multiple box children that have a given |
| /// extent in the main axis. |
| /// |
| /// The [childManager] argument must not be null. |
| RenderSliverFixedExtentList({ |
| @required RenderSliverBoxChildManager childManager, |
| double itemExtent, |
| }) : _itemExtent = itemExtent, |
| super(childManager: childManager); |
| |
| @override |
| double get itemExtent => _itemExtent; |
| double _itemExtent; |
| set itemExtent(double value) { |
| assert(value != null); |
| if (_itemExtent == value) |
| return; |
| _itemExtent = value; |
| markNeedsLayout(); |
| } |
| } |