// Package infinite_listview: // https://pub.dartlang.org/packages/infinite_listview import 'dart:math' as math; import 'package:flutter_web/rendering.dart'; import 'package:flutter_web/widgets.dart'; /// Infinite ListView /// /// ListView that builds its children with to an infinite extent. /// class InfiniteListView extends StatelessWidget { /// See [ListView.builder] InfiniteListView.builder({ Key key, this.scrollDirection = Axis.vertical, this.reverse = false, InfiniteScrollController controller, this.physics, this.padding, this.itemExtent, @required IndexedWidgetBuilder itemBuilder, int itemCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, this.cacheExtent, }) : positiveChildrenDelegate = SliverChildBuilderDelegate( itemBuilder, childCount: itemCount, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, ), negativeChildrenDelegate = SliverChildBuilderDelegate( (BuildContext context, int index) => itemBuilder(context, -1 - index), childCount: itemCount, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, ), controller = controller ?? InfiniteScrollController(), super(key: key); /// See [ListView.separated] InfiniteListView.separated({ Key key, this.scrollDirection = Axis.vertical, this.reverse = false, InfiniteScrollController controller, this.physics, this.padding, @required IndexedWidgetBuilder itemBuilder, @required IndexedWidgetBuilder separatorBuilder, int itemCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, this.cacheExtent, }) : assert(itemBuilder != null), assert(separatorBuilder != null), itemExtent = null, positiveChildrenDelegate = SliverChildBuilderDelegate( (BuildContext context, int index) { final itemIndex = index ~/ 2; return index.isEven ? itemBuilder(context, itemIndex) : separatorBuilder(context, itemIndex); }, childCount: itemCount != null ? math.max(0, itemCount * 2 - 1) : null, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, ), negativeChildrenDelegate = SliverChildBuilderDelegate( (BuildContext context, int index) { final itemIndex = (-1 - index) ~/ 2; return index.isOdd ? itemBuilder(context, itemIndex) : separatorBuilder(context, itemIndex); }, childCount: itemCount, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, ), controller = controller ?? InfiniteScrollController(), super(key: key); /// See: [ScrollView.scrollDirection] final Axis scrollDirection; /// See: [ScrollView.reverse] final bool reverse; /// See: [ScrollView.controller] final InfiniteScrollController controller; /// See: [ScrollView.physics] final ScrollPhysics physics; /// See: [BoxScrollView.padding] final EdgeInsets padding; /// See: [ListView.itemExtent] final double itemExtent; /// See: [ScrollView.cacheExtent] final double cacheExtent; /// See: [ListView.childrenDelegate] final SliverChildDelegate negativeChildrenDelegate; /// See: [ListView.childrenDelegate] final SliverChildDelegate positiveChildrenDelegate; @override Widget build(BuildContext context) { final List slivers = _buildSlivers(context, negative: false); final List negativeSlivers = _buildSlivers(context, negative: true); final AxisDirection axisDirection = _getDirection(context); final scrollPhysics = AlwaysScrollableScrollPhysics(parent: physics); return Scrollable( axisDirection: axisDirection, controller: controller, physics: scrollPhysics, viewportBuilder: (BuildContext context, ViewportOffset offset) { return Builder(builder: (BuildContext context) { /// Build negative [ScrollPosition] for the negative scrolling [Viewport]. final state = Scrollable.of(context); final negativeOffset = _InfiniteScrollPosition( physics: scrollPhysics, context: state, initialPixels: -offset.pixels, keepScrollOffset: controller.keepScrollOffset, ); /// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition]. offset.addListener(() { negativeOffset._forceNegativePixels(offset.pixels); }); /// Stack the two [Viewport]s on top of each other so they move in sync. return Stack( children: [ Viewport( axisDirection: flipAxisDirection(axisDirection), anchor: 1.0, offset: negativeOffset, slivers: negativeSlivers, cacheExtent: cacheExtent, ), Viewport( axisDirection: axisDirection, offset: offset, slivers: slivers, cacheExtent: cacheExtent, ), ], ); }); }, ); } AxisDirection _getDirection(BuildContext context) { return getAxisDirectionFromAxisReverseAndDirectionality( context, scrollDirection, reverse); } List _buildSlivers(BuildContext context, {bool negative = false}) { Widget sliver; if (itemExtent != null) { sliver = SliverFixedExtentList( delegate: negative ? negativeChildrenDelegate : positiveChildrenDelegate, itemExtent: itemExtent, ); } else { sliver = SliverList( delegate: negative ? negativeChildrenDelegate : positiveChildrenDelegate); } if (padding != null) { sliver = new SliverPadding( padding: negative ? padding - EdgeInsets.only(bottom: padding.bottom) : padding - EdgeInsets.only(top: padding.top), sliver: sliver, ); } return [sliver]; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(new EnumProperty('scrollDirection', scrollDirection)); properties.add(new FlagProperty('reverse', value: reverse, ifTrue: 'reversed', showName: true)); properties.add(new DiagnosticsProperty( 'controller', controller, showName: false, defaultValue: null)); properties.add(new DiagnosticsProperty('physics', physics, showName: false, defaultValue: null)); properties.add(new DiagnosticsProperty( 'padding', padding, defaultValue: null)); properties .add(new DoubleProperty('itemExtent', itemExtent, defaultValue: null)); properties.add( new DoubleProperty('cacheExtent', cacheExtent, defaultValue: null)); } } /// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds. class InfiniteScrollController extends ScrollController { /// Creates a new [InfiniteScrollController] InfiniteScrollController({ double initialScrollOffset = 0.0, bool keepScrollOffset = true, String debugLabel, }) : super( initialScrollOffset: initialScrollOffset, keepScrollOffset: keepScrollOffset, debugLabel: debugLabel, ); @override ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) { return new _InfiniteScrollPosition( physics: physics, context: context, initialPixels: initialScrollOffset, keepScrollOffset: keepScrollOffset, oldPosition: oldPosition, debugLabel: debugLabel, ); } } class _InfiniteScrollPosition extends ScrollPositionWithSingleContext { _InfiniteScrollPosition({ @required ScrollPhysics physics, @required ScrollContext context, double initialPixels = 0.0, bool keepScrollOffset = true, ScrollPosition oldPosition, String debugLabel, }) : super( physics: physics, context: context, initialPixels: initialPixels, keepScrollOffset: keepScrollOffset, oldPosition: oldPosition, debugLabel: debugLabel, ); void _forceNegativePixels(double value) { super.forcePixels(-value); } @override double get minScrollExtent => double.negativeInfinity; @override double get maxScrollExtent => double.infinity; }