import 'dart:math' as math; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; // Use a `SliverList` instead of multiple `SliverGrid` because having one `SliverGrid` per section does not scale up. // With the multiple `SliverGrid` solution, thumbnails at the beginning of each sections are built even though they are offscreen // because of `RenderSliverMultiBoxAdaptor.addInitialChild` called by `RenderSliverGrid.performLayout` (line 547), as of Flutter v1.17.0. // cf https://github.com/flutter/flutter/issues/49027 // adapted from `RenderSliverFixedExtentBoxAdaptor` class SectionedListSliver extends StatelessWidget { const SectionedListSliver(); @override Widget build(BuildContext context) { final sectionLayouts = context.watch>().sectionLayouts; final childCount = sectionLayouts.isEmpty ? 0 : sectionLayouts.last.lastIndex + 1; return _SliverKnownExtentList( sectionLayouts: sectionLayouts, delegate: SliverChildBuilderDelegate( (context, index) { if (index >= childCount) return null; final sectionLayout = sectionLayouts.firstWhereOrNull((section) => section.hasChild(index)); return sectionLayout?.builder(context, index) ?? const SizedBox.shrink(); }, childCount: childCount, addAutomaticKeepAlives: false, addRepaintBoundaries: false, ), ); } } class _SliverKnownExtentList extends SliverMultiBoxAdaptorWidget { final List sectionLayouts; const _SliverKnownExtentList({ Key? key, required SliverChildDelegate delegate, required this.sectionLayouts, }) : super(key: key, delegate: delegate); @override _RenderSliverKnownExtentBoxAdaptor createRenderObject(BuildContext context) { final element = context as SliverMultiBoxAdaptorElement; return _RenderSliverKnownExtentBoxAdaptor(childManager: element, sectionLayouts: sectionLayouts); } @override void updateRenderObject(BuildContext context, _RenderSliverKnownExtentBoxAdaptor renderObject) { renderObject.sectionLayouts = sectionLayouts; } } class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { List _sectionLayouts; List get sectionLayouts => _sectionLayouts; set sectionLayouts(List value) { if (_sectionLayouts == value) return; _sectionLayouts = value; markNeedsLayout(); } _RenderSliverKnownExtentBoxAdaptor({ required RenderSliverBoxChildManager childManager, required List sectionLayouts, }) : _sectionLayouts = sectionLayouts, super(childManager: childManager); SectionLayout? sectionAtIndex(int index) => sectionLayouts.firstWhereOrNull((section) => section.hasChild(index)); SectionLayout sectionAtOffset(double scrollOffset) => sectionLayouts.firstWhere((section) => section.hasChildAtOffset(scrollOffset), orElse: () => sectionLayouts.last); double indexToLayoutOffset(int index) { return (sectionAtIndex(index) ?? sectionLayouts.last).indexToLayoutOffset(index); } int getMinChildIndexForScrollOffset(double scrollOffset) { return sectionAtOffset(scrollOffset).getMinChildIndexForScrollOffset(scrollOffset); } int getMaxChildIndexForScrollOffset(double scrollOffset) { return sectionAtOffset(scrollOffset).getMaxChildIndexForScrollOffset(scrollOffset); } double estimateMaxScrollOffset( SliverConstraints constraints, { int? firstIndex, int? lastIndex, double? leadingScrollOffset, double? trailingScrollOffset, }) { return childManager.estimateMaxScrollOffset( constraints, firstIndex: firstIndex, lastIndex: lastIndex, leadingScrollOffset: leadingScrollOffset, trailingScrollOffset: trailingScrollOffset, ); } double computeMaxScrollOffset(SliverConstraints constraints) { return sectionLayouts.last.maxOffset; } int _calculateLeadingGarbage(int firstIndex) { var walker = firstChild; var leadingGarbage = 0; while (walker != null && indexOf(walker) < firstIndex) { leadingGarbage += 1; walker = childAfter(walker); } return leadingGarbage; } int _calculateTrailingGarbage(int targetLastIndex) { var walker = lastChild; var trailingGarbage = 0; while (walker != null && indexOf(walker) > targetLastIndex) { trailingGarbage += 1; walker = childBefore(walker); } return trailingGarbage; } @override void performLayout() { final constraints = this.constraints; childManager.didStartLayout(); childManager.setDidUnderflow(false); final scrollOffset = constraints.scrollOffset + constraints.cacheOrigin; assert(scrollOffset >= 0.0); final remainingExtent = constraints.remainingCacheExtent; assert(remainingExtent >= 0.0); final targetEndScrollOffset = scrollOffset + remainingExtent; final childConstraints = constraints.asBoxConstraints(); final firstIndex = getMinChildIndexForScrollOffset(scrollOffset); final targetLastIndex = targetEndScrollOffset.isFinite ? getMaxChildIndexForScrollOffset(targetEndScrollOffset) : null; if (firstChild != null) { final leadingGarbage = _calculateLeadingGarbage(firstIndex); final trailingGarbage = targetLastIndex != null ? _calculateTrailingGarbage(targetLastIndex) : 0; collectGarbage(leadingGarbage, trailingGarbage); } else { collectGarbage(0, 0); } if (firstChild == null) { if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(firstIndex))) { // There are either no children, or we are past the end of all our children. double max; if (firstIndex <= 0) { max = 0.0; } else { max = computeMaxScrollOffset(constraints); } geometry = SliverGeometry( scrollExtent: max, maxPaintExtent: max, ); childManager.didFinishLayout(); return; } } RenderBox? trailingChildWithLayout; for (var index = indexOf(firstChild!) - 1; index >= firstIndex; --index) { final 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. final layout = sectionAtIndex(index) ?? sectionLayouts.first; geometry = SliverGeometry(scrollOffsetCorrection: layout.indexToLayoutOffset(index)); return; } final childParentData = child.parentData as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = indexToLayoutOffset(index); assert(childParentData.index == index); trailingChildWithLayout ??= child; } if (trailingChildWithLayout == null) { firstChild!.layout(childConstraints); final childParentData = firstChild!.parentData as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = indexToLayoutOffset(firstIndex); trailingChildWithLayout = firstChild; } var estimatedMaxScrollOffset = double.infinity; for (var index = indexOf(trailingChildWithLayout!) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) { var child = childAfter(trailingChildWithLayout!); if (child == null || indexOf(child) != index) { child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout); if (child == null) { // We have run out of children. final layout = sectionAtIndex(index) ?? sectionLayouts.last; estimatedMaxScrollOffset = layout.maxOffset; break; } } else { child.layout(childConstraints); } trailingChildWithLayout = child; final childParentData = child.parentData as SliverMultiBoxAdaptorParentData; assert(childParentData.index == index); childParentData.layoutOffset = indexToLayoutOffset(childParentData.index!); } final lastIndex = indexOf(lastChild!); final leadingScrollOffset = indexToLayoutOffset(firstIndex); final trailingScrollOffset = indexToLayoutOffset(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 paintExtent = calculatePaintOffset( constraints, from: math.min(constraints.scrollOffset, leadingScrollOffset), to: trailingScrollOffset, ); final cacheExtent = calculateCacheOffset( constraints, from: leadingScrollOffset, to: trailingScrollOffset, ); final targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent; final targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite ? getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint) : null; geometry = SliverGeometry( scrollExtent: estimatedMaxScrollOffset, paintExtent: math.min(paintExtent, estimatedMaxScrollOffset), 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(); } }