From b1b353ec8f5c6bfbcb533c6f23afb8c84db8507c Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Tue, 30 Jun 2020 12:12:37 +0300 Subject: [PATCH 01/26] added base render object with overlay --- lib/render_new.dart | 355 ++++++++++++++++++++++++++++++++++++++++++++ pubspec.lock | 68 +++++++-- 2 files changed, 410 insertions(+), 13 deletions(-) create mode 100644 lib/render_new.dart diff --git a/lib/render_new.dart b/lib/render_new.dart new file mode 100644 index 0000000..a558e6c --- /dev/null +++ b/lib/render_new.dart @@ -0,0 +1,355 @@ +import 'dart:async'; +import 'dart:math' show max, min; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import './state.dart'; + +enum HeaderPositionAxis { + mainAxis, + crossAxis, +} + +class StickyListItemRenderObject extends RenderStack { + ScrollableState _scrollable; + StreamSink> _streamSink; + I _itemIndex; + MinOffsetProvider _minOffsetProvider; + bool _overlayContent; + HeaderPositionAxis _headerPositionAxis; + + double _lastOffset; + bool _headerOverflow = false; + + StickyListItemRenderObject({ + @required ScrollableState scrollable, + @required I itemIndex, + MinOffsetProvider minOffsetProvider, + StreamSink> streamSink, + AlignmentGeometry alignment, + TextDirection textDirection, + Overflow overflow, + bool overlayContent, + HeaderPositionAxis headerPositionAxis, + }) : _scrollable = scrollable, + _streamSink = streamSink, + _itemIndex = itemIndex, + _minOffsetProvider = minOffsetProvider, + _overlayContent = overlayContent, + _headerPositionAxis = headerPositionAxis, + super( + alignment: alignment, + textDirection: textDirection, + fit: StackFit.loose, + overflow: overflow, + ); + + StreamSink> get streamSink => _streamSink; + + set streamSink(StreamSink> sink) { + _streamSink = sink; + markNeedsPaint(); + } + + I get itemIndex => _itemIndex; + + set itemIndex(I index) { + _itemIndex = index; + markNeedsPaint(); + } + + MinOffsetProvider get minOffsetProvider => + _minOffsetProvider ?? (state) => null; + + set minOffsetProvider(MinOffsetProvider offsetProvider) { + _minOffsetProvider = offsetProvider; + markNeedsPaint(); + } + + set overlayContent(bool overlayContent) { + _overlayContent = overlayContent; + markNeedsPaint(); + } + + set headerPositionAxis(HeaderPositionAxis headerPositionAxis) { + _headerPositionAxis = headerPositionAxis; + markNeedsPaint(); + } + + ScrollableState get scrollable => _scrollable; + + set scrollable(ScrollableState newScrollable) { + assert(newScrollable != null); + + final ScrollableState oldScrollable = _scrollable; + _scrollable = newScrollable; + + markNeedsPaint(); + + if (attached) { + oldScrollable.position.removeListener(markNeedsPaint); + newScrollable.position.addListener(markNeedsPaint); + } + } + + RenderBox get _headerBox => lastChild; + RenderBox get _contentBox => firstChild; + + RenderAbstractViewport get _viewport => RenderAbstractViewport.of(this); + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + scrollable.position.addListener(markNeedsPaint); + } + + @override + void detach() { + scrollable.position.removeListener(markNeedsPaint); + super.detach(); + } + + @override + Rect describeApproximatePaintClip(RenderObject child) => + _headerOverflow ? Offset.zero & size : null; + + @override + void paint(PaintingContext context, Offset paintOffset) { + updateHeaderOffset(); + + if (overflow == Overflow.clip && _headerOverflow) { + context.pushClipRect( + needsCompositing, paintOffset, Offset.zero & size, paintStack); + } else { + paintStack(context, paintOffset); + } + } + + @override + void performLayout() { + final BoxConstraints constraints = this.constraints; + final RenderBox content = _contentBox; + + content.layout(constraints.loosen(), parentUsesSize: true); + + final Size childSize = content.size; + + double width = max(constraints.minWidth, childSize.width); + double height = max(constraints.minHeight, childSize.height); + + if (!_overlayContent) { + final RenderBox header = _headerBox; + final Size headerSize = header.size; + + switch (_headerPositionAxis) { + case HeaderPositionAxis.mainAxis: + if (_scrollDirectionVertical) { + height += max(constraints.minHeight, headerSize.height); + } else { + width += max(constraints.minWidth, headerSize.width); + } + + break; + + case HeaderPositionAxis.crossAxis: + if (_scrollDirectionVertical) { + width += max(constraints.minWidth, headerSize.width); + } else { + height += max(constraints.minHeight, headerSize.height); + } + + break; + } + } + + size = Size(width, height); + assert(size.width == constraints.constrainWidth(width)); + assert(size.height == constraints.constrainHeight(height)); + + assert(size.isFinite); + + final StackParentData childParentData = content.parentData as StackParentData; + + childParentData.offset = Offset.zero; + } + + void updateHeaderOffset() { + _headerOverflow = false; + + final double stuckOffset = _stuckOffset; + + final StackParentData parentData = _headerBox.parentData; + final double contentSize = _getContentDirectionSize(); + final double headerSize = _getHeaderDirectionSize(); + + final double offset = _getStateOffset(stuckOffset, contentSize); + final double position = offset / contentSize; + + final StickyState state = StickyState( + itemIndex, + position: position, + offset: offset, + contentSize: contentSize, + ); + + final double headerOffset = _getHeaderOffset( + contentSize, + stuckOffset, + headerSize, + minOffsetProvider(state) + ); + + parentData.offset = _getDirectionalOffset( + parentData.offset, + headerOffset + ); + + _headerOverflow = _isHeaderOverflow(headerOffset, headerSize, contentSize); + + if (_lastOffset != offset) { + _lastOffset = offset; + + streamSink?.add(state.copyWith( + sticky: _isSticky( + state, + headerOffset, + _getHeaderOffset( + contentSize, + stuckOffset, + headerSize + ) + ) + )); + } + } + + bool get _scrollDirectionVertical => + [AxisDirection.up, AxisDirection.down].contains(scrollable.axisDirection); + + bool get _alignmentStart { + if (_scrollDirectionVertical) { + return [ + AlignmentDirectional.topStart, + AlignmentDirectional.topCenter, + AlignmentDirectional.topEnd, + ].contains(alignment); + } + + return [ + AlignmentDirectional.topStart, + AlignmentDirectional.bottomStart, + AlignmentDirectional.centerStart, + ].contains(alignment); + } + + double get _scrollableSize { + final viewportContainer = _viewport; + + double viewportSize; + + if (viewportContainer is RenderBox) { + final RenderBox viewportBox = viewportContainer as RenderBox; + + viewportSize = _scrollDirectionVertical + ? viewportBox.size.height + : viewportBox.size.width; + } + + assert(viewportSize != null, 'Can\'t define view port size'); + + double anchor = 0; + + if (viewportContainer is RenderViewport) { + anchor = viewportContainer.anchor; + } + + if (_alignmentStart) { + return -viewportSize * anchor; + } + + return viewportSize - viewportSize * anchor; + } + + double get _stuckOffset { + return _viewport.getOffsetToReveal(this, 0).offset - _scrollable.position.pixels - _scrollableSize; + } + + double _getContentDirectionSize() { + return _scrollDirectionVertical + ? _contentBox.size.height + : _contentBox.size.width; + } + + double _getHeaderDirectionSize() { + return _scrollDirectionVertical + ? _headerBox.size.height + : _headerBox.size.width; + } + + Offset _getDirectionalOffset(Offset originalOffset, double offset) { + if (_scrollDirectionVertical) { + return Offset( + originalOffset.dx, + offset + ); + } + + return Offset( + offset, + originalOffset.dy + ); + } + + double _getStateOffset(double stuckOffset, double contentSize) { + double offset = _getOffset(stuckOffset, 0, contentSize); + + if (_alignmentStart) { + return offset; + } + + return contentSize - offset; + } + + double _getHeaderOffset( + double contentSize, + double stuckOffset, + double headerSize, + [double providedMinOffset = 0] + ) { + final double minOffset = _getMinOffset(contentSize, providedMinOffset); + + if (_alignmentStart) { + return _getOffset(stuckOffset, 0, minOffset); + } + + return _getOffset(stuckOffset, minOffset, contentSize) - headerSize; + } + + double _getOffset(double current, double minPosition, double maxPosition) { + return max(minPosition, min(-current, maxPosition)); + } + + double _getMinOffset(double contentSize, double minOffset) { + if (_alignmentStart) { + return contentSize - minOffset; + } + + return minOffset; + } + + bool _isHeaderOverflow(double headerOffset, double headerSize, double contentSize) { + return headerOffset < 0 || headerOffset + headerSize > contentSize; + } + + bool _isSticky( + StickyState state, + double actualHeaderOffset, + double headerOffset + ) { + return ( + actualHeaderOffset == headerOffset && + state.position > 0 && + state.position < 1 + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index aebdafa..97283e8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,34 +1,62 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.4.1" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "2.0.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.3" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.11" + version: "1.14.12" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" flutter: dependency: "direct main" description: flutter @@ -39,20 +67,27 @@ packages: description: flutter source: sdk version: "0.0.0" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.12" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.5" + version: "0.12.6" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.7" + version: "1.1.8" path: dependency: transitive description: @@ -60,20 +95,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.4" - pedantic: + petitparser: dependency: transitive description: - name: pedantic + name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "1.8.0+1" + version: "2.4.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.3" sky_engine: dependency: transitive description: flutter @@ -85,7 +120,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.7.0" stack_trace: dependency: transitive description: @@ -120,7 +155,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.5" + version: "0.2.15" typed_data: dependency: transitive description: @@ -135,5 +170,12 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.1" sdks: - dart: ">=2.2.2 <3.0.0" + dart: ">=2.6.0 <3.0.0" From fc13402eaf5838001c2f8d3ee9b0bd48d34e6a9e Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Tue, 30 Jun 2020 13:56:08 +0300 Subject: [PATCH 02/26] added alignment resolution --- lib/render_new.dart | 91 +++++++++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/lib/render_new.dart b/lib/render_new.dart index a558e6c..f5b80d2 100644 --- a/lib/render_new.dart +++ b/lib/render_new.dart @@ -10,6 +10,17 @@ enum HeaderPositionAxis { crossAxis, } +enum HeaderMainAxisAlignment { + start, + end, +} + +enum HeaderCrossAxisAlignment { + start, + center, + end, +} + class StickyListItemRenderObject extends RenderStack { ScrollableState _scrollable; StreamSink> _streamSink; @@ -17,6 +28,8 @@ class StickyListItemRenderObject extends RenderStack { MinOffsetProvider _minOffsetProvider; bool _overlayContent; HeaderPositionAxis _headerPositionAxis; + HeaderMainAxisAlignment _mainAxisAlignment; + HeaderCrossAxisAlignment _crossAxisAlignment; double _lastOffset; bool _headerOverflow = false; @@ -26,19 +39,22 @@ class StickyListItemRenderObject extends RenderStack { @required I itemIndex, MinOffsetProvider minOffsetProvider, StreamSink> streamSink, - AlignmentGeometry alignment, TextDirection textDirection, Overflow overflow, bool overlayContent, HeaderPositionAxis headerPositionAxis, + HeaderMainAxisAlignment mainAxisAlignment, + HeaderCrossAxisAlignment crossAxisAlignment, }) : _scrollable = scrollable, _streamSink = streamSink, _itemIndex = itemIndex, _minOffsetProvider = minOffsetProvider, _overlayContent = overlayContent, _headerPositionAxis = headerPositionAxis, + _mainAxisAlignment = mainAxisAlignment, + _crossAxisAlignment = crossAxisAlignment, super( - alignment: alignment, + alignment: _headerAlignment(scrollable, mainAxisAlignment, crossAxisAlignment), textDirection: textDirection, fit: StackFit.loose, overflow: overflow, @@ -76,6 +92,16 @@ class StickyListItemRenderObject extends RenderStack { markNeedsPaint(); } + set mainAxisAlignment(HeaderMainAxisAlignment axisAlignment) { + _mainAxisAlignment = axisAlignment; + alignment = _headerAlignment(scrollable, _mainAxisAlignment, _crossAxisAlignment); + } + + set crossAxisAlignment(HeaderCrossAxisAlignment axisAlignment) { + _crossAxisAlignment = axisAlignment; + alignment = _headerAlignment(scrollable, _mainAxisAlignment, _crossAxisAlignment); + } + ScrollableState get scrollable => _scrollable; set scrollable(ScrollableState newScrollable) { @@ -129,6 +155,7 @@ class StickyListItemRenderObject extends RenderStack { void performLayout() { final BoxConstraints constraints = this.constraints; final RenderBox content = _contentBox; + final RenderBox header = _headerBox; content.layout(constraints.loosen(), parentUsesSize: true); @@ -138,7 +165,6 @@ class StickyListItemRenderObject extends RenderStack { double height = max(constraints.minHeight, childSize.height); if (!_overlayContent) { - final RenderBox header = _headerBox; final Size headerSize = header.size; switch (_headerPositionAxis) { @@ -168,9 +194,13 @@ class StickyListItemRenderObject extends RenderStack { assert(size.isFinite); - final StackParentData childParentData = content.parentData as StackParentData; + final StackParentData contentParentData = content.parentData as StackParentData; + + contentParentData.offset = Offset.zero; - childParentData.offset = Offset.zero; + final StackParentData headerParentData = _headerBox.parentData as StackParentData; + + RenderStack.layoutPositionedChild(_headerBox, headerParentData, size, alignment); } void updateHeaderOffset() { @@ -192,7 +222,7 @@ class StickyListItemRenderObject extends RenderStack { contentSize: contentSize, ); - final double headerOffset = _getHeaderOffset( + final double headerOffset = _calculateHeaderOffset( contentSize, stuckOffset, headerSize, @@ -213,7 +243,7 @@ class StickyListItemRenderObject extends RenderStack { sticky: _isSticky( state, headerOffset, - _getHeaderOffset( + _calculateHeaderOffset( contentSize, stuckOffset, headerSize @@ -226,21 +256,7 @@ class StickyListItemRenderObject extends RenderStack { bool get _scrollDirectionVertical => [AxisDirection.up, AxisDirection.down].contains(scrollable.axisDirection); - bool get _alignmentStart { - if (_scrollDirectionVertical) { - return [ - AlignmentDirectional.topStart, - AlignmentDirectional.topCenter, - AlignmentDirectional.topEnd, - ].contains(alignment); - } - - return [ - AlignmentDirectional.topStart, - AlignmentDirectional.bottomStart, - AlignmentDirectional.centerStart, - ].contains(alignment); - } + bool get _alignmentStart => _mainAxisAlignment == HeaderMainAxisAlignment.start; double get _scrollableSize { final viewportContainer = _viewport; @@ -310,7 +326,7 @@ class StickyListItemRenderObject extends RenderStack { return contentSize - offset; } - double _getHeaderOffset( + double _calculateHeaderOffset( double contentSize, double stuckOffset, double headerSize, @@ -352,4 +368,33 @@ class StickyListItemRenderObject extends RenderStack { state.position < 1 ); } + + static AlignmentGeometry _headerAlignment(ScrollableState scrollable, HeaderMainAxisAlignment mainAxisAlignment, HeaderCrossAxisAlignment crossAxisAlignment) { + final bool vertical = [AxisDirection.up, AxisDirection.down].contains(scrollable.axisDirection); + + switch (crossAxisAlignment) { + + case HeaderCrossAxisAlignment.end: + if (mainAxisAlignment == HeaderMainAxisAlignment.end) { + return Alignment.bottomRight; + } + + return vertical ? Alignment.bottomLeft : Alignment.topRight; + + case HeaderCrossAxisAlignment.center: + if (mainAxisAlignment == HeaderMainAxisAlignment.start) { + return vertical ? Alignment.centerLeft : Alignment.topCenter; + } + + return vertical ? Alignment.centerRight : Alignment.bottomCenter; + + case HeaderCrossAxisAlignment.start: + default: + if (mainAxisAlignment == HeaderMainAxisAlignment.start) { + return Alignment.topLeft; + } + + return vertical ? Alignment.topRight : Alignment.bottomLeft; + } + } } From c15b98b4739a892522a9ee53c01c80a7137030b9 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Tue, 30 Jun 2020 15:01:54 +0300 Subject: [PATCH 03/26] added models folder with types definitions --- lib/models/alignments.dart | 52 ++++++++++++++++++++++++++++ lib/models/sticky_state.dart | 66 ++++++++++++++++++++++++++++++++++++ lib/models/types.dart | 8 +++++ 3 files changed, 126 insertions(+) create mode 100644 lib/models/alignments.dart create mode 100644 lib/models/sticky_state.dart create mode 100644 lib/models/types.dart diff --git a/lib/models/alignments.dart b/lib/models/alignments.dart new file mode 100644 index 0000000..75327da --- /dev/null +++ b/lib/models/alignments.dart @@ -0,0 +1,52 @@ +/// Header position axis for content without header overflow +enum HeaderPositionAxis { + /// Align against main axis direction + mainAxis, + + /// Align against cross axis direction + crossAxis, +} + +/// Main axis direction alignment +enum HeaderMainAxisAlignment { + /// Start position against main axis + /// + /// For Horizontal scroll header will be places at the left, + /// for vertical - at the top side + start, + + /// End alignment + /// + /// For Horizontal scroll header will be places at the right, + /// for vertical - at the bottom side + end, +} + +/// Cross axis header alignment +enum HeaderCrossAxisAlignment { + /// Start position against cross axis + /// + /// For Horizontal scroll header will be places at top, + /// for vertical - at the left side + start, + + /// Center position against cross axis + /// + /// This value can be used only with overlay headers, + /// or with relative header and [HeaderPositionAxis.mainAxis] + center, + + /// End position against cross axis + /// + /// For Horizontal scroll header will be places at the bottom, + /// for vertical - at the right side + end, +} + +enum InfiniteListDirection { + /// Render only positive infinite list + single, + + /// Render both positive and negative infinite lists + multi, +} \ No newline at end of file diff --git a/lib/models/sticky_state.dart b/lib/models/sticky_state.dart new file mode 100644 index 0000000..157efd7 --- /dev/null +++ b/lib/models/sticky_state.dart @@ -0,0 +1,66 @@ +/// Sticky state object +/// that describes header position and content height +class StickyState { + /// Position, that header already passed + /// + /// Value can be between 0.0 and 1.0 + /// + /// If it's `0.0` - sticky in max start position + /// + /// `1.0` - max end position + /// + /// If [InfiniteListItem.initialHeaderBuild] is true, initial + /// header render will be with position = 0 + final double position; + + /// Number of pixels, that outside of viewport + /// + /// If [InfiniteListItem.initialHeaderBuild] is true, initial + /// header render will be with offset = 0 + /// + /// For header bottom positions (or right positions for horizontal) + /// offset value also will be amount of pixels that was scrolled away + final double offset; + + /// Item index + final I index; + + /// If header is in sticky state + /// + /// If [InfiniteListItem.minOffsetProvider] is defined, + /// it could be that header builder will be emitted with new state + /// on scroll, but [sticky] will be false, if offset already passed + /// min offset value + /// + /// WHen [InfiniteListItem.minOffsetProvider] is called, [sticky] + /// will always be `false`. Since for min offset calculation + /// offset itself not defined yet + final bool sticky; + + /// Scroll item height. + /// + /// If [InfiniteListItem.initialHeaderBuild] is true, initial + /// header render will be called without this value + final double contentSize; + + StickyState(this.index, { + this.position = 0, + this.offset = 0, + this.sticky = false, + this.contentSize + }); + + /// Create state duplicate, with optional state options override + StickyState copyWith({ + double position, + double offset, + bool sticky, + double contentHeight + }) => StickyState( + index, + position: position ?? this.position, + offset: offset ?? this.offset, + sticky: sticky ?? this.sticky, + contentSize: contentHeight ?? this.contentSize, + ); +} \ No newline at end of file diff --git a/lib/models/types.dart b/lib/models/types.dart new file mode 100644 index 0000000..2690c6b --- /dev/null +++ b/lib/models/types.dart @@ -0,0 +1,8 @@ +import 'package:flutter/widgets.dart'; + +import 'sticky_state.dart'; + +typedef Widget ContentBuilder(BuildContext context); +typedef Widget HeaderStateBuilder(BuildContext context, StickyState state); +typedef Widget HeaderBuilder(BuildContext context); +typedef double MinOffsetProvider(StickyState state); \ No newline at end of file From f6fe78a23169ec626617c0d5cbe367042ba2d2da Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Tue, 30 Jun 2020 15:02:06 +0300 Subject: [PATCH 04/26] rename methods --- lib/render_new.dart | 120 ++++++++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 55 deletions(-) diff --git a/lib/render_new.dart b/lib/render_new.dart index f5b80d2..eb2fda8 100644 --- a/lib/render_new.dart +++ b/lib/render_new.dart @@ -3,23 +3,10 @@ import 'dart:math' show max, min; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import './state.dart'; -enum HeaderPositionAxis { - mainAxis, - crossAxis, -} - -enum HeaderMainAxisAlignment { - start, - end, -} - -enum HeaderCrossAxisAlignment { - start, - center, - end, -} +import 'models/alignments.dart'; +import 'models/sticky_state.dart'; +import 'models/types.dart'; class StickyListItemRenderObject extends RenderStack { ScrollableState _scrollable; @@ -164,28 +151,54 @@ class StickyListItemRenderObject extends RenderStack { double width = max(constraints.minWidth, childSize.width); double height = max(constraints.minHeight, childSize.height); + final StackParentData contentParentData = content.parentData as StackParentData; + if (!_overlayContent) { final Size headerSize = header.size; + double headerHeight = max(constraints.minHeight, headerSize.height); + double headerWidth = max(constraints.minWidth, headerSize.width); switch (_headerPositionAxis) { case HeaderPositionAxis.mainAxis: if (_scrollDirectionVertical) { - height += max(constraints.minHeight, headerSize.height); - } else { - width += max(constraints.minWidth, headerSize.width); + height += headerHeight; + + if (_mainAxisAlignment == HeaderMainAxisAlignment.start) { + contentParentData.offset = Offset(0, headerHeight); + } + + break; + } + + width += headerWidth; + + if (_mainAxisAlignment == HeaderMainAxisAlignment.start) { + contentParentData.offset = Offset(headerWidth, 0); } break; case HeaderPositionAxis.crossAxis: if (_scrollDirectionVertical) { - width += max(constraints.minWidth, headerSize.width); - } else { - height += max(constraints.minHeight, headerSize.height); + width += headerWidth; + + if (_crossAxisAlignment == HeaderCrossAxisAlignment.start) { + contentParentData.offset = Offset(headerWidth, 0); + } + + break; + } + + height += headerHeight; + + if (_crossAxisAlignment == HeaderCrossAxisAlignment.start) { + contentParentData.offset = Offset(0, headerHeight); } break; } + } else { + contentParentData.offset = Offset.zero; } size = Size(width, height); @@ -194,10 +207,6 @@ class StickyListItemRenderObject extends RenderStack { assert(size.isFinite); - final StackParentData contentParentData = content.parentData as StackParentData; - - contentParentData.offset = Offset.zero; - final StackParentData headerParentData = _headerBox.parentData as StackParentData; RenderStack.layoutPositionedChild(_headerBox, headerParentData, size, alignment); @@ -209,10 +218,10 @@ class StickyListItemRenderObject extends RenderStack { final double stuckOffset = _stuckOffset; final StackParentData parentData = _headerBox.parentData; - final double contentSize = _getContentDirectionSize(); - final double headerSize = _getHeaderDirectionSize(); + final double contentSize = _contentDirectionSize; + final double headerSize = _headerDirectionSize; - final double offset = _getStateOffset(stuckOffset, contentSize); + final double offset = _calculateStateOffset(stuckOffset, contentSize); final double position = offset / contentSize; final StickyState state = StickyState( @@ -229,7 +238,7 @@ class StickyListItemRenderObject extends RenderStack { minOffsetProvider(state) ); - parentData.offset = _getDirectionalOffset( + parentData.offset = _headerDirectionalOffset( parentData.offset, headerOffset ); @@ -253,8 +262,7 @@ class StickyListItemRenderObject extends RenderStack { } } - bool get _scrollDirectionVertical => - [AxisDirection.up, AxisDirection.down].contains(scrollable.axisDirection); + bool get _scrollDirectionVertical => _scrollableAxisVertical(scrollable.axisDirection); bool get _alignmentStart => _mainAxisAlignment == HeaderMainAxisAlignment.start; @@ -290,19 +298,15 @@ class StickyListItemRenderObject extends RenderStack { return _viewport.getOffsetToReveal(this, 0).offset - _scrollable.position.pixels - _scrollableSize; } - double _getContentDirectionSize() { - return _scrollDirectionVertical - ? _contentBox.size.height - : _contentBox.size.width; - } + double get _contentDirectionSize => _scrollDirectionVertical + ? _contentBox.size.height + : _contentBox.size.width; - double _getHeaderDirectionSize() { - return _scrollDirectionVertical - ? _headerBox.size.height - : _headerBox.size.width; - } + double get _headerDirectionSize => _scrollDirectionVertical + ? _headerBox.size.height + : _headerBox.size.width; - Offset _getDirectionalOffset(Offset originalOffset, double offset) { + Offset _headerDirectionalOffset(Offset originalOffset, double offset) { if (_scrollDirectionVertical) { return Offset( originalOffset.dx, @@ -316,8 +320,8 @@ class StickyListItemRenderObject extends RenderStack { ); } - double _getStateOffset(double stuckOffset, double contentSize) { - double offset = _getOffset(stuckOffset, 0, contentSize); + double _calculateStateOffset(double stuckOffset, double contentSize) { + double offset = _calculateOffset(stuckOffset, 0, contentSize); if (_alignmentStart) { return offset; @@ -330,22 +334,26 @@ class StickyListItemRenderObject extends RenderStack { double contentSize, double stuckOffset, double headerSize, - [double providedMinOffset = 0] + [double providedMinOffset] ) { - final double minOffset = _getMinOffset(contentSize, providedMinOffset); + if (providedMinOffset == null) { + providedMinOffset = headerSize; + } + + final double minOffset = _calculateMinOffset(contentSize, providedMinOffset); if (_alignmentStart) { - return _getOffset(stuckOffset, 0, minOffset); + return _calculateOffset(stuckOffset, 0, minOffset); } - return _getOffset(stuckOffset, minOffset, contentSize) - headerSize; + return _calculateOffset(stuckOffset, minOffset, contentSize) - headerSize; } - double _getOffset(double current, double minPosition, double maxPosition) { + double _calculateOffset(double current, double minPosition, double maxPosition) { return max(minPosition, min(-current, maxPosition)); } - double _getMinOffset(double contentSize, double minOffset) { + double _calculateMinOffset(double contentSize, double minOffset) { if (_alignmentStart) { return contentSize - minOffset; } @@ -370,7 +378,7 @@ class StickyListItemRenderObject extends RenderStack { } static AlignmentGeometry _headerAlignment(ScrollableState scrollable, HeaderMainAxisAlignment mainAxisAlignment, HeaderCrossAxisAlignment crossAxisAlignment) { - final bool vertical = [AxisDirection.up, AxisDirection.down].contains(scrollable.axisDirection); + final bool vertical = _scrollableAxisVertical(scrollable.axisDirection); switch (crossAxisAlignment) { @@ -379,14 +387,14 @@ class StickyListItemRenderObject extends RenderStack { return Alignment.bottomRight; } - return vertical ? Alignment.bottomLeft : Alignment.topRight; + return vertical ? Alignment.topRight : Alignment.bottomLeft; case HeaderCrossAxisAlignment.center: if (mainAxisAlignment == HeaderMainAxisAlignment.start) { return vertical ? Alignment.centerLeft : Alignment.topCenter; } - return vertical ? Alignment.centerRight : Alignment.bottomCenter; + return vertical ? Alignment.bottomCenter : Alignment.centerRight; case HeaderCrossAxisAlignment.start: default: @@ -394,7 +402,9 @@ class StickyListItemRenderObject extends RenderStack { return Alignment.topLeft; } - return vertical ? Alignment.topRight : Alignment.bottomLeft; + return vertical ? Alignment.bottomLeft : Alignment.topRight; } } + + static bool _scrollableAxisVertical(AxisDirection direction) => [AxisDirection.up, AxisDirection.down].contains(direction); } From 06a58b9e8a7baf40293065d4a3e9156648192b83 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Tue, 30 Jun 2020 19:19:07 +0300 Subject: [PATCH 05/26] fixed overlay calculation issues --- example/example.dart | 21 ++-- lib/render_new.dart | 164 +++++++++++++++++++++++++------- lib/sticky_infinite_list.dart | 4 +- lib/widget.dart | 174 +++++++++++++++++++++++----------- 4 files changed, 261 insertions(+), 102 deletions(-) diff --git a/example/example.dart b/example/example.dart index 2850b71..0710a7d 100644 --- a/example/example.dart +++ b/example/example.dart @@ -35,11 +35,6 @@ class Example extends StatelessWidget { builder: (BuildContext context, int index) { /// Builder requires [InfiniteList] to be returned return InfiniteListItem( - /// If header should be build during initial render. - /// - /// Will be ignored with just [headerBuilder] builder specified - initialHeaderBuild: true, - /// Header builder with state /// /// Will be invoked each time header changes it's position @@ -92,15 +87,21 @@ class Example extends StatelessWidget { /// of container minOffsetProvider: (StickyState state) => 50, - /// Header alignment + /// Header alignment against main axis /// - /// Currently it supports top left, - /// top right, bottom left and bottom right alignments + /// By default [HeaderMainAxisAlignment.start] + mainAxisAlignment: HeaderMainAxisAlignment.start, + + /// Header alignment against main axis /// - /// By default [HeaderAlignment.topLeft] - headerAlignment: HeaderAlignment.topLeft, + /// By default [HeaderCrossAxisAlignment.start] + crossAxisAlignment: HeaderCrossAxisAlignment.start, + /// Header alignment placement against scroll axis + /// + /// By default [HeaderPositionAxis.mainAxis] + positionAxis: HeaderPositionAxis.mainAxis, ); } ); diff --git a/lib/render_new.dart b/lib/render_new.dart index eb2fda8..cde9537 100644 --- a/lib/render_new.dart +++ b/lib/render_new.dart @@ -8,13 +8,14 @@ import 'models/alignments.dart'; import 'models/sticky_state.dart'; import 'models/types.dart'; +///todo: rename file, remove old render object class StickyListItemRenderObject extends RenderStack { ScrollableState _scrollable; StreamSink> _streamSink; I _itemIndex; MinOffsetProvider _minOffsetProvider; bool _overlayContent; - HeaderPositionAxis _headerPositionAxis; + HeaderPositionAxis _positionAxis; HeaderMainAxisAlignment _mainAxisAlignment; HeaderCrossAxisAlignment _crossAxisAlignment; @@ -29,15 +30,15 @@ class StickyListItemRenderObject extends RenderStack { TextDirection textDirection, Overflow overflow, bool overlayContent, - HeaderPositionAxis headerPositionAxis, - HeaderMainAxisAlignment mainAxisAlignment, - HeaderCrossAxisAlignment crossAxisAlignment, + HeaderPositionAxis positionAxis = HeaderPositionAxis.mainAxis, + HeaderMainAxisAlignment mainAxisAlignment = HeaderMainAxisAlignment.start, + HeaderCrossAxisAlignment crossAxisAlignment = HeaderCrossAxisAlignment.start, }) : _scrollable = scrollable, _streamSink = streamSink, _itemIndex = itemIndex, _minOffsetProvider = minOffsetProvider, _overlayContent = overlayContent, - _headerPositionAxis = headerPositionAxis, + _positionAxis = positionAxis, _mainAxisAlignment = mainAxisAlignment, _crossAxisAlignment = crossAxisAlignment, super( @@ -71,12 +72,18 @@ class StickyListItemRenderObject extends RenderStack { set overlayContent(bool overlayContent) { _overlayContent = overlayContent; - markNeedsPaint(); + + if (_overlayContent != overlayContent) { + markNeedsLayout(); + } } - set headerPositionAxis(HeaderPositionAxis headerPositionAxis) { - _headerPositionAxis = headerPositionAxis; - markNeedsPaint(); + set positionAxis(HeaderPositionAxis positionAxis) { + _positionAxis = positionAxis; + + if (_positionAxis != positionAxis) { + markNeedsLayout(); + } } set mainAxisAlignment(HeaderMainAxisAlignment axisAlignment) { @@ -144,61 +151,94 @@ class StickyListItemRenderObject extends RenderStack { final RenderBox content = _contentBox; final RenderBox header = _headerBox; - content.layout(constraints.loosen(), parentUsesSize: true); + final BoxConstraints containerConstraints = constraints.loosen(); - final Size childSize = content.size; - - double width = max(constraints.minWidth, childSize.width); - double height = max(constraints.minHeight, childSize.height); + header.layout(containerConstraints, parentUsesSize: true); final StackParentData contentParentData = content.parentData as StackParentData; + contentParentData.offset = Offset.zero; + + double width = constraints.minWidth; + double height = constraints.minHeight; + /// todo: rewrite this block if (!_overlayContent) { final Size headerSize = header.size; - double headerHeight = max(constraints.minHeight, headerSize.height); - double headerWidth = max(constraints.minWidth, headerSize.width); - switch (_headerPositionAxis) { + switch (_positionAxis) { case HeaderPositionAxis.mainAxis: if (_scrollDirectionVertical) { - height += headerHeight; - if (_mainAxisAlignment == HeaderMainAxisAlignment.start) { - contentParentData.offset = Offset(0, headerHeight); + contentParentData.offset = Offset(0, headerSize.height); } + content.layout(containerConstraints.copyWith( + maxHeight: containerConstraints.maxHeight - headerSize.height + ), parentUsesSize: true); + + Size contentSize = content.size; + + width = contentSize.width; + height = contentSize.height + headerSize.height; + break; } - width += headerWidth; - if (_mainAxisAlignment == HeaderMainAxisAlignment.start) { - contentParentData.offset = Offset(headerWidth, 0); + contentParentData.offset = Offset(headerSize.width, 0); } + content.layout(containerConstraints.copyWith( + maxWidth: containerConstraints.maxWidth - headerSize.width + ), parentUsesSize: true); + + Size contentSize = content.size; + + width = contentSize.width + headerSize.width; + height = contentSize.height; + break; case HeaderPositionAxis.crossAxis: if (_scrollDirectionVertical) { - width += headerWidth; - if (_crossAxisAlignment == HeaderCrossAxisAlignment.start) { - contentParentData.offset = Offset(headerWidth, 0); + contentParentData.offset = Offset(headerSize.width, 0); } + content.layout(containerConstraints.copyWith( + maxWidth: containerConstraints.maxWidth - headerSize.width + ), parentUsesSize: true); + + Size contentSize = content.size; + + width = contentSize.width + headerSize.width; + height = contentSize.height; + break; } - height += headerHeight; - if (_crossAxisAlignment == HeaderCrossAxisAlignment.start) { - contentParentData.offset = Offset(0, headerHeight); + contentParentData.offset = Offset(0, headerSize.height); } + content.layout(containerConstraints.copyWith( + maxHeight: containerConstraints.maxHeight - headerSize.height + ), parentUsesSize: true); + + Size contentSize = content.size; + + width = contentSize.width; + height = contentSize.height + headerSize.height; + break; } } else { - contentParentData.offset = Offset.zero; + content.layout(containerConstraints, parentUsesSize: true); + + Size contentSize = content.size; + + width = contentSize.width; + height = contentSize.height; } size = Size(width, height); @@ -207,9 +247,11 @@ class StickyListItemRenderObject extends RenderStack { assert(size.isFinite); - final StackParentData headerParentData = _headerBox.parentData as StackParentData; + final StackParentData headerParentData = header.parentData as StackParentData; + + headerParentData.offset = alignment.resolve(textDirection).alongOffset(size - header.size as Offset); - RenderStack.layoutPositionedChild(_headerBox, headerParentData, size, alignment); + //RenderStack.layoutPositionedChild(header, headerParentData, size, alignment); } void updateHeaderOffset() { @@ -262,6 +304,58 @@ class StickyListItemRenderObject extends RenderStack { } } + @override + double computeMinIntrinsicWidth(double height) { + if ( + _overlayContent || + _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis || + !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis + ) { + return _contentBox.getMinIntrinsicWidth(height); + } + + return _contentBox.getMinIntrinsicWidth(height) + _headerBox.getMinIntrinsicWidth(height); + } + + @override + double computeMaxIntrinsicWidth(double height) { + if ( + _overlayContent || + _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis || + !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis + ) { + return _contentBox.getMaxIntrinsicWidth(height); + } + + return _contentBox.getMaxIntrinsicWidth(height) + _headerBox.getMaxIntrinsicWidth(height); + } + + @override + double computeMinIntrinsicHeight(double width) { + if ( + _overlayContent || + _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis || + !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis + ) { + return _contentBox.getMinIntrinsicHeight(width); + } + + return _contentBox.getMinIntrinsicHeight(width) + _headerBox.getMinIntrinsicHeight(width); + } + + @override + double computeMaxIntrinsicHeight(double width) { + if ( + _overlayContent || + _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis || + !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis + ) { + return _contentBox.getMinIntrinsicHeight(width); + } + + return _contentBox.getMinIntrinsicHeight(width) + _headerBox.getMinIntrinsicHeight(width); + } + bool get _scrollDirectionVertical => _scrollableAxisVertical(scrollable.axisDirection); bool get _alignmentStart => _mainAxisAlignment == HeaderMainAxisAlignment.start; @@ -299,8 +393,8 @@ class StickyListItemRenderObject extends RenderStack { } double get _contentDirectionSize => _scrollDirectionVertical - ? _contentBox.size.height - : _contentBox.size.width; + ? size.height + : size.width; double get _headerDirectionSize => _scrollDirectionVertical ? _headerBox.size.height @@ -391,7 +485,7 @@ class StickyListItemRenderObject extends RenderStack { case HeaderCrossAxisAlignment.center: if (mainAxisAlignment == HeaderMainAxisAlignment.start) { - return vertical ? Alignment.centerLeft : Alignment.topCenter; + return vertical ? Alignment.topCenter : Alignment.centerLeft; } return vertical ? Alignment.bottomCenter : Alignment.centerRight; diff --git a/lib/sticky_infinite_list.dart b/lib/sticky_infinite_list.dart index 9b0df2a..76685b6 100644 --- a/lib/sticky_infinite_list.dart +++ b/lib/sticky_infinite_list.dart @@ -2,4 +2,6 @@ library sticky_infinite_list; export './widget.dart'; export './render.dart'; -export './state.dart'; +export './models/alignments.dart'; +export './models/sticky_state.dart'; +export './models/types.dart'; diff --git a/lib/widget.dart b/lib/widget.dart index db025de..b57e8f2 100644 --- a/lib/widget.dart +++ b/lib/widget.dart @@ -2,8 +2,11 @@ import 'dart:async'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import './state.dart'; -import './render.dart'; + +import './render_new.dart'; +import 'models/sticky_state.dart'; +import 'models/types.dart'; +import 'models/alignments.dart'; typedef InfiniteListItem ItemBuilder(BuildContext context, I index); @@ -13,16 +16,14 @@ typedef InfiniteListItem ItemBuilder(BuildContext context, I index); /// /// This class build item header and content class InfiniteListItem { + /// Header builder based on [StickyState] final HeaderStateBuilder headerStateBuilder; + + /// Header builder final HeaderBuilder headerBuilder; - final ContentBuilder contentBuilder; - /// Header alignment - /// - /// By default [HeaderAlignment.topLeft] - /// - /// For more option take a look in [HeaderAlignment] - final HeaderAlignment headerAlignment; + /// Content builder + final ContentBuilder contentBuilder; /// Function, that provides min offset. /// @@ -55,14 +56,45 @@ class InfiniteListItem { /// this property will be ignored final bool initialHeaderBuild; + /// Header alignment against main axis direction + /// + /// See [HeaderMainAxisAlignment] for more info + final HeaderMainAxisAlignment mainAxisAlignment; + + /// Header alignment against cross axis direction + /// + /// See [HeaderCrossAxisAlignment] for more info + final HeaderCrossAxisAlignment crossAxisAlignment; + + /// Header position against scroll axis for relative positioned headers + /// + /// See [HeaderPositionAxis] for more info + final HeaderPositionAxis positionAxis; + + final bool _overlayContent; + InfiniteListItem({ @required this.contentBuilder, this.headerBuilder, this.headerStateBuilder, this.minOffsetProvider, - this.headerAlignment = HeaderAlignment.topLeft, + this.mainAxisAlignment = HeaderMainAxisAlignment.start, + this.crossAxisAlignment = HeaderCrossAxisAlignment.start, + this.positionAxis = HeaderPositionAxis.mainAxis, + }): _overlayContent = false, + initialHeaderBuild = true; + + InfiniteListItem.overlay({ + @required this.contentBuilder, + this.headerBuilder, + this.headerStateBuilder, + this.minOffsetProvider, this.initialHeaderBuild = false, - }); + this.mainAxisAlignment = HeaderMainAxisAlignment.start, + this.crossAxisAlignment = HeaderCrossAxisAlignment.start, + }) + : positionAxis = HeaderPositionAxis.mainAxis, + _overlayContent = true; bool get hasStickyHeader => headerBuilder != null || headerStateBuilder != null; @@ -97,7 +129,7 @@ class InfiniteListItem { @mustCallSuper void dispose() {} - Widget _getHeader(BuildContext context, Stream> stream, I index) { + Widget _buildHeader(BuildContext context, Stream> stream, I index) { assert(hasStickyHeader, "At least one builder should be provided"); if (!watchStickyState) { @@ -213,7 +245,7 @@ class _InfiniteListState extends State { SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) => - _getListItem(context, (index + 1) * -1), + _buildListItem(context, (index + 1) * -1), childCount: _reverseChildCount, ), ); @@ -221,13 +253,13 @@ class _InfiniteListState extends State { SliverList get _forwardList => SliverList( delegate: SliverChildBuilderDelegate( - _getListItem, + _buildListItem, childCount: widget.maxChildCount, ), key: widget._centerKey, ); - Widget _getListItem(BuildContext context, int index) => + Widget _buildListItem(BuildContext context, int index) => _StickySliverListItem( streamController: _streamController, index: index, @@ -287,39 +319,7 @@ class _StickySliverListItem extends StatefulWidget { }) : super(key: key); @override - State<_StickySliverListItem> createState() => - _StickySliverListItemState(); - - /// Maps sticky header alignment values - /// to [AlignmentDirectional] variant - AlignmentDirectional get alignment { - switch (listItem.headerAlignment) { - case HeaderAlignment.centerLeft: - return AlignmentDirectional.centerStart; - - case HeaderAlignment.centerRight: - return AlignmentDirectional.centerEnd; - - case HeaderAlignment.bottomLeft: - return AlignmentDirectional.bottomStart; - - case HeaderAlignment.bottomCenter: - return AlignmentDirectional.bottomCenter; - - case HeaderAlignment.bottomRight: - return AlignmentDirectional.bottomEnd; - - case HeaderAlignment.bottomCenter: - return AlignmentDirectional.bottomCenter; - - case HeaderAlignment.topRight: - return AlignmentDirectional.topEnd; - - case HeaderAlignment.topLeft: - default: - return AlignmentDirectional.topStart; - } - } + State<_StickySliverListItem> createState() => _StickySliverListItemState(); } class _StickySliverListItemState extends State<_StickySliverListItem> { @@ -338,17 +338,35 @@ class _StickySliverListItemState extends State<_StickySliverListItem> { return content; } + if (widget.listItem._overlayContent) { + return StickyListItem.overlay( + itemIndex: widget.index, + streamSink: widget.streamController.sink, + header: widget.listItem._buildHeader( + context, + widget._stream, + widget.index + ), + content: content, + minOffsetProvider: widget.listItem.minOffsetProvider, + mainAxisAlignment: widget.listItem.mainAxisAlignment, + crossAxisAlignment: widget.listItem.crossAxisAlignment, + ); + } + return StickyListItem( itemIndex: widget.index, streamSink: widget.streamController.sink, - header: widget.listItem._getHeader( + header: widget.listItem._buildHeader( context, widget._stream, widget.index ), content: content, minOffsetProvider: widget.listItem.minOffsetProvider, - alignment: widget.alignment, + mainAxisAlignment: widget.listItem.mainAxisAlignment, + crossAxisAlignment: widget.listItem.crossAxisAlignment, + positionAxis: widget.listItem.positionAxis, ); } @@ -378,18 +396,56 @@ class StickyListItem extends Stack { /// Callback function that tells when header to stick to the bottom final MinOffsetProvider minOffsetProvider; + /// Header alignment against main axis direction + /// + /// See [HeaderMainAxisAlignment] for more info + final HeaderMainAxisAlignment mainAxisAlignment; + + /// Header alignment against cross axis direction + /// + /// See [HeaderCrossAxisAlignment] for more info + final HeaderCrossAxisAlignment crossAxisAlignment; + + /// Header position against scroll axis for relative positioned headers + /// + /// See [HeaderPositionAxis] for more info + final HeaderPositionAxis positionAxis; + + final bool _overlayContent; + StickyListItem({ @required Widget header, @required Widget content, @required this.itemIndex, this.minOffsetProvider, this.streamSink, - AlignmentDirectional alignment, + this.mainAxisAlignment = HeaderMainAxisAlignment.start, + this.crossAxisAlignment = HeaderCrossAxisAlignment.start, + this.positionAxis = HeaderPositionAxis.mainAxis, + Key key, + }) + : _overlayContent = false, + super( + key: key, + children: [content, header], + overflow: Overflow.clip, + ); + + StickyListItem.overlay({ + @required Widget header, + @required Widget content, + @required this.itemIndex, + this.minOffsetProvider, + this.streamSink, + this.mainAxisAlignment = HeaderMainAxisAlignment.start, + this.crossAxisAlignment = HeaderCrossAxisAlignment.start, Key key, - }) : super( + }) + : _overlayContent = true, + positionAxis = HeaderPositionAxis.mainAxis, + super( key: key, children: [content, header], - alignment: alignment ?? AlignmentDirectional.topStart, overflow: Overflow.clip, ); @@ -400,9 +456,11 @@ class StickyListItem extends Stack { RenderStack createRenderObject(BuildContext context) => StickyListItemRenderObject( scrollable: _getScrollableState(context), - alignment: alignment, + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, + positionAxis: positionAxis, textDirection: textDirection ?? Directionality.of(context), - fit: fit, + overlayContent: _overlayContent, overflow: overflow, itemIndex: itemIndex, streamSink: streamSink, @@ -419,7 +477,11 @@ class StickyListItem extends Stack { ..scrollable = _getScrollableState(context) ..itemIndex = itemIndex ..streamSink = streamSink - ..minOffsetProvider = minOffsetProvider; + ..minOffsetProvider = minOffsetProvider + ..mainAxisAlignment = mainAxisAlignment + ..crossAxisAlignment = crossAxisAlignment + ..positionAxis = positionAxis + ..overlayContent = _overlayContent; } } } From a6263c0c025ae78ed9df0bc31ece90b3192a69fd Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Tue, 30 Jun 2020 19:31:39 +0300 Subject: [PATCH 06/26] added assert for alignment positioning --- lib/widget.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/widget.dart b/lib/widget.dart index b57e8f2..2d92977 100644 --- a/lib/widget.dart +++ b/lib/widget.dart @@ -73,7 +73,7 @@ class InfiniteListItem { final bool _overlayContent; - InfiniteListItem({ + const InfiniteListItem({ @required this.contentBuilder, this.headerBuilder, this.headerStateBuilder, @@ -84,7 +84,7 @@ class InfiniteListItem { }): _overlayContent = false, initialHeaderBuild = true; - InfiniteListItem.overlay({ + const InfiniteListItem.overlay({ @required this.contentBuilder, this.headerBuilder, this.headerStateBuilder, @@ -425,6 +425,10 @@ class StickyListItem extends Stack { Key key, }) : _overlayContent = false, + assert( + positionAxis == HeaderPositionAxis.mainAxis || crossAxisAlignment != HeaderCrossAxisAlignment.center, + 'Center cross axis alignment can\'t be used with Cross axis positioning' + ), super( key: key, children: [content, header], From 07e7acd223101399e3632a81a46fc6209858c3d8 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Tue, 30 Jun 2020 23:48:14 +0300 Subject: [PATCH 07/26] added padding to list item --- lib/render_new.dart | 1 + lib/widget.dart | 53 +++++++++++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/lib/render_new.dart b/lib/render_new.dart index cde9537..40af640 100644 --- a/lib/render_new.dart +++ b/lib/render_new.dart @@ -156,6 +156,7 @@ class StickyListItemRenderObject extends RenderStack { header.layout(containerConstraints, parentUsesSize: true); final StackParentData contentParentData = content.parentData as StackParentData; + contentParentData.offset = Offset.zero; double width = constraints.minWidth; diff --git a/lib/widget.dart b/lib/widget.dart index 2d92977..503e86d 100644 --- a/lib/widget.dart +++ b/lib/widget.dart @@ -71,6 +71,9 @@ class InfiniteListItem { /// See [HeaderPositionAxis] for more info final HeaderPositionAxis positionAxis; + /// List item padding, see [EdgeInsets] for more info + final EdgeInsets padding; + final bool _overlayContent; const InfiniteListItem({ @@ -81,6 +84,7 @@ class InfiniteListItem { this.mainAxisAlignment = HeaderMainAxisAlignment.start, this.crossAxisAlignment = HeaderCrossAxisAlignment.start, this.positionAxis = HeaderPositionAxis.mainAxis, + this.padding, }): _overlayContent = false, initialHeaderBuild = true; @@ -92,6 +96,7 @@ class InfiniteListItem { this.initialHeaderBuild = false, this.mainAxisAlignment = HeaderMainAxisAlignment.start, this.crossAxisAlignment = HeaderCrossAxisAlignment.start, + this.padding, }) : positionAxis = HeaderPositionAxis.mainAxis, _overlayContent = true; @@ -136,18 +141,16 @@ class InfiniteListItem { return buildHeader(context); } - return Positioned( - child: StreamBuilder>( - stream: stream, - initialData: initialHeaderBuild ? StickyState(index) : null, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Container(); - } - - return buildHeader(context, snapshot.data); - }, - ), + return StreamBuilder>( + stream: stream, + initialData: initialHeaderBuild ? StickyState(index) : null, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Container(); + } + + return buildHeader(context, snapshot.data); + }, ); } } @@ -331,7 +334,26 @@ class _StickySliverListItemState extends State<_StickySliverListItem> { } @override + Widget build(BuildContext context) { + if (widget.listItem.padding == null) { + return _buildItem(context); + } + + return Padding( + padding: widget.listItem.padding, + child: _buildItem(context), + ); + } + + @override + void dispose() { + super.dispose(); + + widget.listItem.dispose(); + } + + Widget _buildItem(BuildContext context) { final Widget content = widget.listItem.buildContent(context); if (!widget.listItem.hasStickyHeader) { @@ -369,13 +391,6 @@ class _StickySliverListItemState extends State<_StickySliverListItem> { positionAxis: widget.listItem.positionAxis, ); } - - @override - void dispose() { - super.dispose(); - - widget.listItem.dispose(); - } } /// Sticky list item that provides header offset calculation From 922f91fb0bfd17e0cbc4d1543fa0ddfadbfb683b Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 16:06:47 +0300 Subject: [PATCH 08/26] refactored content layout --- lib/render_new.dart | 163 +++++++++++++++------------------- lib/sticky_infinite_list.dart | 2 +- 2 files changed, 72 insertions(+), 93 deletions(-) diff --git a/lib/render_new.dart b/lib/render_new.dart index 40af640..8411746 100644 --- a/lib/render_new.dart +++ b/lib/render_new.dart @@ -148,111 +148,22 @@ class StickyListItemRenderObject extends RenderStack { @override void performLayout() { final BoxConstraints constraints = this.constraints; - final RenderBox content = _contentBox; final RenderBox header = _headerBox; final BoxConstraints containerConstraints = constraints.loosen(); header.layout(containerConstraints, parentUsesSize: true); - final StackParentData contentParentData = content.parentData as StackParentData; - - contentParentData.offset = Offset.zero; - - double width = constraints.minWidth; - double height = constraints.minHeight; - - /// todo: rewrite this block - if (!_overlayContent) { - final Size headerSize = header.size; - - switch (_positionAxis) { - case HeaderPositionAxis.mainAxis: - if (_scrollDirectionVertical) { - if (_mainAxisAlignment == HeaderMainAxisAlignment.start) { - contentParentData.offset = Offset(0, headerSize.height); - } - - content.layout(containerConstraints.copyWith( - maxHeight: containerConstraints.maxHeight - headerSize.height - ), parentUsesSize: true); - - Size contentSize = content.size; - - width = contentSize.width; - height = contentSize.height + headerSize.height; - - break; - } - - if (_mainAxisAlignment == HeaderMainAxisAlignment.start) { - contentParentData.offset = Offset(headerSize.width, 0); - } - - content.layout(containerConstraints.copyWith( - maxWidth: containerConstraints.maxWidth - headerSize.width - ), parentUsesSize: true); - - Size contentSize = content.size; - - width = contentSize.width + headerSize.width; - height = contentSize.height; - - break; - - case HeaderPositionAxis.crossAxis: - if (_scrollDirectionVertical) { - if (_crossAxisAlignment == HeaderCrossAxisAlignment.start) { - contentParentData.offset = Offset(headerSize.width, 0); - } - - content.layout(containerConstraints.copyWith( - maxWidth: containerConstraints.maxWidth - headerSize.width - ), parentUsesSize: true); - - Size contentSize = content.size; - - width = contentSize.width + headerSize.width; - height = contentSize.height; - - break; - } - - if (_crossAxisAlignment == HeaderCrossAxisAlignment.start) { - contentParentData.offset = Offset(0, headerSize.height); - } - - content.layout(containerConstraints.copyWith( - maxHeight: containerConstraints.maxHeight - headerSize.height - ), parentUsesSize: true); - - Size contentSize = content.size; - - width = contentSize.width; - height = contentSize.height + headerSize.height; - - break; - } - } else { - content.layout(containerConstraints, parentUsesSize: true); - - Size contentSize = content.size; - - width = contentSize.width; - height = contentSize.height; - } + size = _layoutContent(containerConstraints, header.size); - size = Size(width, height); - assert(size.width == constraints.constrainWidth(width)); - assert(size.height == constraints.constrainHeight(height)); + assert(size.width == constraints.constrainWidth(size.width)); + assert(size.height == constraints.constrainHeight(size.height)); assert(size.isFinite); final StackParentData headerParentData = header.parentData as StackParentData; headerParentData.offset = alignment.resolve(textDirection).alongOffset(size - header.size as Offset); - - //RenderStack.layoutPositionedChild(header, headerParentData, size, alignment); } void updateHeaderOffset() { @@ -472,6 +383,74 @@ class StickyListItemRenderObject extends RenderStack { ); } + Size _layoutContent(BoxConstraints constraints, Size headerSize) { + final RenderBox content = _contentBox; + final StackParentData contentParentData = content.parentData as StackParentData; + + if (!_overlayContent) { + final bool alignmentStart = _mainAxisAlignment == + HeaderMainAxisAlignment.start || + _crossAxisAlignment == HeaderCrossAxisAlignment.start; + + if ( + ( + _positionAxis == HeaderPositionAxis.crossAxis && + _scrollDirectionVertical + ) || + ( + _positionAxis == HeaderPositionAxis.mainAxis && + !_scrollDirectionVertical + ) + ) { + content.layout(constraints.copyWith( + maxWidth: constraints.maxWidth - headerSize.width + ), parentUsesSize: true); + + if (alignmentStart) { + contentParentData.offset = Offset(headerSize.width, 0); + } + + final Size contentSize = content.size; + + return Size( + contentSize.width + headerSize.width, + contentSize.height + ); + } + + if ( + ( + _positionAxis == HeaderPositionAxis.mainAxis && + _scrollDirectionVertical + ) || + ( + _positionAxis == HeaderPositionAxis.crossAxis && + !_scrollDirectionVertical + ) + ) { + content.layout(constraints.copyWith( + maxHeight: constraints.maxHeight - headerSize.height + ), parentUsesSize: true); + + if (alignmentStart) { + contentParentData.offset = Offset(0, headerSize.height); + } + + final Size contentSize = content.size; + + return Size( + contentSize.width, + contentSize.height + headerSize.height + ); + } + } + + content.layout(constraints, parentUsesSize: true); + contentParentData.offset = Offset.zero; + + return content.size; + } + static AlignmentGeometry _headerAlignment(ScrollableState scrollable, HeaderMainAxisAlignment mainAxisAlignment, HeaderCrossAxisAlignment crossAxisAlignment) { final bool vertical = _scrollableAxisVertical(scrollable.axisDirection); diff --git a/lib/sticky_infinite_list.dart b/lib/sticky_infinite_list.dart index 76685b6..43170b6 100644 --- a/lib/sticky_infinite_list.dart +++ b/lib/sticky_infinite_list.dart @@ -1,7 +1,7 @@ library sticky_infinite_list; export './widget.dart'; -export './render.dart'; +export './render_new.dart'; export './models/alignments.dart'; export './models/sticky_state.dart'; export './models/types.dart'; From 61a30218bc96e3306716de890e49f74510136536 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 16:28:54 +0300 Subject: [PATCH 09/26] added documentation --- lib/render_new.dart | 2 ++ lib/widget.dart | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/render_new.dart b/lib/render_new.dart index 8411746..0891b01 100644 --- a/lib/render_new.dart +++ b/lib/render_new.dart @@ -9,6 +9,8 @@ import 'models/sticky_state.dart'; import 'models/types.dart'; ///todo: rename file, remove old render object +/// +/// Sticky item render object based on [RenderStack] class StickyListItemRenderObject extends RenderStack { ScrollableState _scrollable; StreamSink> _streamSink; diff --git a/lib/widget.dart b/lib/widget.dart index 503e86d..04f99ca 100644 --- a/lib/widget.dart +++ b/lib/widget.dart @@ -54,6 +54,12 @@ class InfiniteListItem { /// /// For case when only [headerBuilder] is defined, /// this property will be ignored + /// + /// If it's relative header positioning ([InfiniteListItem.overlay] constructor is used), + /// this property always will be `true`, which means that with relative + /// positioning, header will be built with basic [StickyState] object. + /// + /// It's required due to layout container and define it's actual dimensions final bool initialHeaderBuild; /// Header alignment against main axis direction @@ -74,8 +80,10 @@ class InfiniteListItem { /// List item padding, see [EdgeInsets] for more info final EdgeInsets padding; + /// If header should overlay content or not final bool _overlayContent; + /// Default list item constructor with relative header positioning const InfiniteListItem({ @required this.contentBuilder, this.headerBuilder, @@ -88,6 +96,7 @@ class InfiniteListItem { }): _overlayContent = false, initialHeaderBuild = true; + /// List item constructor with overlayed header positioning const InfiniteListItem.overlay({ @required this.contentBuilder, this.headerBuilder, @@ -101,9 +110,13 @@ class InfiniteListItem { : positionAxis = HeaderPositionAxis.mainAxis, _overlayContent = true; + /// Defines if list item has Header bool get hasStickyHeader => headerBuilder != null || headerStateBuilder != null; + /// Defines if list item should watch header position state changes. + /// + /// It's true if [headerStateBuilder] was provided instead of [headerBuilder] bool get watchStickyState => headerStateBuilder != null; /// Header item builder @@ -408,7 +421,9 @@ class StickyListItem extends Stack { /// during stream event emit final I itemIndex; - /// Callback function that tells when header to stick to the bottom + /// Callback function that tells when header to stick to the bottom. + /// + /// If it returns `null` or callback not provided - min offset will be header height final MinOffsetProvider minOffsetProvider; /// Header alignment against main axis direction @@ -426,8 +441,10 @@ class StickyListItem extends Stack { /// See [HeaderPositionAxis] for more info final HeaderPositionAxis positionAxis; + /// Defines if header should overlay content final bool _overlayContent; + /// Default sticky item constructor with relative header positioning StickyListItem({ @required Widget header, @required Widget content, @@ -450,6 +467,9 @@ class StickyListItem extends Stack { overflow: Overflow.clip, ); + /// Default sticky item constructor with overlayed header positioning. + /// + /// Header position axis in this case will be against main axis always. StickyListItem.overlay({ @required Widget header, @required Widget content, From 7a773e6a2ca2a1053910987f6a25a1f5cfe33b6d Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 16:29:55 +0300 Subject: [PATCH 10/26] rename render file --- lib/render.dart | 290 ++++++++++++++++---- lib/render_new.dart | 486 ---------------------------------- lib/sticky_infinite_list.dart | 2 +- lib/widget.dart | 2 +- 4 files changed, 246 insertions(+), 534 deletions(-) delete mode 100644 lib/render_new.dart diff --git a/lib/render.dart b/lib/render.dart index a639a66..56b81b5 100644 --- a/lib/render.dart +++ b/lib/render.dart @@ -3,13 +3,22 @@ import 'dart:math' show max, min; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import './state.dart'; +import 'models/alignments.dart'; +import 'models/sticky_state.dart'; +import 'models/types.dart'; + + +/// Sticky item render object based on [RenderStack] class StickyListItemRenderObject extends RenderStack { ScrollableState _scrollable; StreamSink> _streamSink; I _itemIndex; MinOffsetProvider _minOffsetProvider; + bool _overlayContent; + HeaderPositionAxis _positionAxis; + HeaderMainAxisAlignment _mainAxisAlignment; + HeaderCrossAxisAlignment _crossAxisAlignment; double _lastOffset; bool _headerOverflow = false; @@ -19,18 +28,24 @@ class StickyListItemRenderObject extends RenderStack { @required I itemIndex, MinOffsetProvider minOffsetProvider, StreamSink> streamSink, - AlignmentGeometry alignment, TextDirection textDirection, - StackFit fit, Overflow overflow, + bool overlayContent, + HeaderPositionAxis positionAxis = HeaderPositionAxis.mainAxis, + HeaderMainAxisAlignment mainAxisAlignment = HeaderMainAxisAlignment.start, + HeaderCrossAxisAlignment crossAxisAlignment = HeaderCrossAxisAlignment.start, }) : _scrollable = scrollable, _streamSink = streamSink, _itemIndex = itemIndex, _minOffsetProvider = minOffsetProvider, + _overlayContent = overlayContent, + _positionAxis = positionAxis, + _mainAxisAlignment = mainAxisAlignment, + _crossAxisAlignment = crossAxisAlignment, super( - alignment: alignment, + alignment: _headerAlignment(scrollable, mainAxisAlignment, crossAxisAlignment), textDirection: textDirection, - fit: fit, + fit: StackFit.loose, overflow: overflow, ); @@ -49,13 +64,39 @@ class StickyListItemRenderObject extends RenderStack { } MinOffsetProvider get minOffsetProvider => - _minOffsetProvider ?? (state) => 0; + _minOffsetProvider ?? (state) => null; set minOffsetProvider(MinOffsetProvider offsetProvider) { _minOffsetProvider = offsetProvider; markNeedsPaint(); } + set overlayContent(bool overlayContent) { + _overlayContent = overlayContent; + + if (_overlayContent != overlayContent) { + markNeedsLayout(); + } + } + + set positionAxis(HeaderPositionAxis positionAxis) { + _positionAxis = positionAxis; + + if (_positionAxis != positionAxis) { + markNeedsLayout(); + } + } + + set mainAxisAlignment(HeaderMainAxisAlignment axisAlignment) { + _mainAxisAlignment = axisAlignment; + alignment = _headerAlignment(scrollable, _mainAxisAlignment, _crossAxisAlignment); + } + + set crossAxisAlignment(HeaderCrossAxisAlignment axisAlignment) { + _crossAxisAlignment = axisAlignment; + alignment = _headerAlignment(scrollable, _mainAxisAlignment, _crossAxisAlignment); + } + ScrollableState get scrollable => _scrollable; set scrollable(ScrollableState newScrollable) { @@ -105,16 +146,37 @@ class StickyListItemRenderObject extends RenderStack { } } + @override + void performLayout() { + final BoxConstraints constraints = this.constraints; + final RenderBox header = _headerBox; + + final BoxConstraints containerConstraints = constraints.loosen(); + + header.layout(containerConstraints, parentUsesSize: true); + + size = _layoutContent(containerConstraints, header.size); + + assert(size.width == constraints.constrainWidth(size.width)); + assert(size.height == constraints.constrainHeight(size.height)); + + assert(size.isFinite); + + final StackParentData headerParentData = header.parentData as StackParentData; + + headerParentData.offset = alignment.resolve(textDirection).alongOffset(size - header.size as Offset); + } + void updateHeaderOffset() { _headerOverflow = false; final double stuckOffset = _stuckOffset; final StackParentData parentData = _headerBox.parentData; - final double contentSize = _getContentDirectionSize(); - final double headerSize = _getHeaderDirectionSize(); + final double contentSize = _contentDirectionSize; + final double headerSize = _headerDirectionSize; - final double offset = _getStateOffset(stuckOffset, contentSize); + final double offset = _calculateStateOffset(stuckOffset, contentSize); final double position = offset / contentSize; final StickyState state = StickyState( @@ -124,14 +186,14 @@ class StickyListItemRenderObject extends RenderStack { contentSize: contentSize, ); - final double headerOffset = _getHeaderOffset( + final double headerOffset = _calculateHeaderOffset( contentSize, stuckOffset, headerSize, minOffsetProvider(state) ); - parentData.offset = _getDirectionalOffset( + parentData.offset = _headerDirectionalOffset( parentData.offset, headerOffset ); @@ -145,7 +207,7 @@ class StickyListItemRenderObject extends RenderStack { sticky: _isSticky( state, headerOffset, - _getHeaderOffset( + _calculateHeaderOffset( contentSize, stuckOffset, headerSize @@ -155,25 +217,62 @@ class StickyListItemRenderObject extends RenderStack { } } - bool get _scrollDirectionVertical => - [AxisDirection.up, AxisDirection.down].contains(scrollable.axisDirection); + @override + double computeMinIntrinsicWidth(double height) { + if ( + _overlayContent || + _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis || + !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis + ) { + return _contentBox.getMinIntrinsicWidth(height); + } - bool get _alignmentStart { - if (_scrollDirectionVertical) { - return [ - AlignmentDirectional.topStart, - AlignmentDirectional.topCenter, - AlignmentDirectional.topEnd, - ].contains(alignment); + return _contentBox.getMinIntrinsicWidth(height) + _headerBox.getMinIntrinsicWidth(height); + } + + @override + double computeMaxIntrinsicWidth(double height) { + if ( + _overlayContent || + _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis || + !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis + ) { + return _contentBox.getMaxIntrinsicWidth(height); + } + + return _contentBox.getMaxIntrinsicWidth(height) + _headerBox.getMaxIntrinsicWidth(height); + } + + @override + double computeMinIntrinsicHeight(double width) { + if ( + _overlayContent || + _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis || + !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis + ) { + return _contentBox.getMinIntrinsicHeight(width); + } + + return _contentBox.getMinIntrinsicHeight(width) + _headerBox.getMinIntrinsicHeight(width); + } + + @override + double computeMaxIntrinsicHeight(double width) { + if ( + _overlayContent || + _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis || + !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis + ) { + return _contentBox.getMinIntrinsicHeight(width); } - return [ - AlignmentDirectional.topStart, - AlignmentDirectional.bottomStart, - AlignmentDirectional.centerStart, - ].contains(alignment); + return _contentBox.getMinIntrinsicHeight(width) + _headerBox.getMinIntrinsicHeight(width); } + bool get _scrollDirectionVertical => _scrollableAxisVertical(scrollable.axisDirection); + + bool get _alignmentStart => _mainAxisAlignment == HeaderMainAxisAlignment.start; + double get _scrollableSize { final viewportContainer = _viewport; @@ -206,19 +305,15 @@ class StickyListItemRenderObject extends RenderStack { return _viewport.getOffsetToReveal(this, 0).offset - _scrollable.position.pixels - _scrollableSize; } - double _getContentDirectionSize() { - return _scrollDirectionVertical - ? _contentBox.size.height - : _contentBox.size.width; - } + double get _contentDirectionSize => _scrollDirectionVertical + ? size.height + : size.width; - double _getHeaderDirectionSize() { - return _scrollDirectionVertical - ? _headerBox.size.height - : _headerBox.size.width; - } + double get _headerDirectionSize => _scrollDirectionVertical + ? _headerBox.size.height + : _headerBox.size.width; - Offset _getDirectionalOffset(Offset originalOffset, double offset) { + Offset _headerDirectionalOffset(Offset originalOffset, double offset) { if (_scrollDirectionVertical) { return Offset( originalOffset.dx, @@ -232,8 +327,8 @@ class StickyListItemRenderObject extends RenderStack { ); } - double _getStateOffset(double stuckOffset, double contentSize) { - double offset = _getOffset(stuckOffset, 0, contentSize); + double _calculateStateOffset(double stuckOffset, double contentSize) { + double offset = _calculateOffset(stuckOffset, 0, contentSize); if (_alignmentStart) { return offset; @@ -242,26 +337,30 @@ class StickyListItemRenderObject extends RenderStack { return contentSize - offset; } - double _getHeaderOffset( + double _calculateHeaderOffset( double contentSize, double stuckOffset, double headerSize, - [double providedMinOffset = 0] + [double providedMinOffset] ) { - final double minOffset = _getMinOffset(contentSize, providedMinOffset); + if (providedMinOffset == null) { + providedMinOffset = headerSize; + } + + final double minOffset = _calculateMinOffset(contentSize, providedMinOffset); if (_alignmentStart) { - return _getOffset(stuckOffset, 0, minOffset); + return _calculateOffset(stuckOffset, 0, minOffset); } - return _getOffset(stuckOffset, minOffset, contentSize) - headerSize; + return _calculateOffset(stuckOffset, minOffset, contentSize) - headerSize; } - double _getOffset(double current, double minPosition, double maxPosition) { + double _calculateOffset(double current, double minPosition, double maxPosition) { return max(minPosition, min(-current, maxPosition)); } - double _getMinOffset(double contentSize, double minOffset) { + double _calculateMinOffset(double contentSize, double minOffset) { if (_alignmentStart) { return contentSize - minOffset; } @@ -284,4 +383,103 @@ class StickyListItemRenderObject extends RenderStack { state.position < 1 ); } + + Size _layoutContent(BoxConstraints constraints, Size headerSize) { + final RenderBox content = _contentBox; + final StackParentData contentParentData = content.parentData as StackParentData; + + if (!_overlayContent) { + final bool alignmentStart = _mainAxisAlignment == + HeaderMainAxisAlignment.start || + _crossAxisAlignment == HeaderCrossAxisAlignment.start; + + if ( + ( + _positionAxis == HeaderPositionAxis.crossAxis && + _scrollDirectionVertical + ) || + ( + _positionAxis == HeaderPositionAxis.mainAxis && + !_scrollDirectionVertical + ) + ) { + content.layout(constraints.copyWith( + maxWidth: constraints.maxWidth - headerSize.width + ), parentUsesSize: true); + + if (alignmentStart) { + contentParentData.offset = Offset(headerSize.width, 0); + } + + final Size contentSize = content.size; + + return Size( + contentSize.width + headerSize.width, + contentSize.height + ); + } + + if ( + ( + _positionAxis == HeaderPositionAxis.mainAxis && + _scrollDirectionVertical + ) || + ( + _positionAxis == HeaderPositionAxis.crossAxis && + !_scrollDirectionVertical + ) + ) { + content.layout(constraints.copyWith( + maxHeight: constraints.maxHeight - headerSize.height + ), parentUsesSize: true); + + if (alignmentStart) { + contentParentData.offset = Offset(0, headerSize.height); + } + + final Size contentSize = content.size; + + return Size( + contentSize.width, + contentSize.height + headerSize.height + ); + } + } + + content.layout(constraints, parentUsesSize: true); + contentParentData.offset = Offset.zero; + + return content.size; + } + + static AlignmentGeometry _headerAlignment(ScrollableState scrollable, HeaderMainAxisAlignment mainAxisAlignment, HeaderCrossAxisAlignment crossAxisAlignment) { + final bool vertical = _scrollableAxisVertical(scrollable.axisDirection); + + switch (crossAxisAlignment) { + + case HeaderCrossAxisAlignment.end: + if (mainAxisAlignment == HeaderMainAxisAlignment.end) { + return Alignment.bottomRight; + } + + return vertical ? Alignment.topRight : Alignment.bottomLeft; + + case HeaderCrossAxisAlignment.center: + if (mainAxisAlignment == HeaderMainAxisAlignment.start) { + return vertical ? Alignment.topCenter : Alignment.centerLeft; + } + + return vertical ? Alignment.bottomCenter : Alignment.centerRight; + + case HeaderCrossAxisAlignment.start: + default: + if (mainAxisAlignment == HeaderMainAxisAlignment.start) { + return Alignment.topLeft; + } + + return vertical ? Alignment.bottomLeft : Alignment.topRight; + } + } + + static bool _scrollableAxisVertical(AxisDirection direction) => [AxisDirection.up, AxisDirection.down].contains(direction); } diff --git a/lib/render_new.dart b/lib/render_new.dart deleted file mode 100644 index 0891b01..0000000 --- a/lib/render_new.dart +++ /dev/null @@ -1,486 +0,0 @@ -import 'dart:async'; -import 'dart:math' show max, min; - -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; - -import 'models/alignments.dart'; -import 'models/sticky_state.dart'; -import 'models/types.dart'; - -///todo: rename file, remove old render object -/// -/// Sticky item render object based on [RenderStack] -class StickyListItemRenderObject extends RenderStack { - ScrollableState _scrollable; - StreamSink> _streamSink; - I _itemIndex; - MinOffsetProvider _minOffsetProvider; - bool _overlayContent; - HeaderPositionAxis _positionAxis; - HeaderMainAxisAlignment _mainAxisAlignment; - HeaderCrossAxisAlignment _crossAxisAlignment; - - double _lastOffset; - bool _headerOverflow = false; - - StickyListItemRenderObject({ - @required ScrollableState scrollable, - @required I itemIndex, - MinOffsetProvider minOffsetProvider, - StreamSink> streamSink, - TextDirection textDirection, - Overflow overflow, - bool overlayContent, - HeaderPositionAxis positionAxis = HeaderPositionAxis.mainAxis, - HeaderMainAxisAlignment mainAxisAlignment = HeaderMainAxisAlignment.start, - HeaderCrossAxisAlignment crossAxisAlignment = HeaderCrossAxisAlignment.start, - }) : _scrollable = scrollable, - _streamSink = streamSink, - _itemIndex = itemIndex, - _minOffsetProvider = minOffsetProvider, - _overlayContent = overlayContent, - _positionAxis = positionAxis, - _mainAxisAlignment = mainAxisAlignment, - _crossAxisAlignment = crossAxisAlignment, - super( - alignment: _headerAlignment(scrollable, mainAxisAlignment, crossAxisAlignment), - textDirection: textDirection, - fit: StackFit.loose, - overflow: overflow, - ); - - StreamSink> get streamSink => _streamSink; - - set streamSink(StreamSink> sink) { - _streamSink = sink; - markNeedsPaint(); - } - - I get itemIndex => _itemIndex; - - set itemIndex(I index) { - _itemIndex = index; - markNeedsPaint(); - } - - MinOffsetProvider get minOffsetProvider => - _minOffsetProvider ?? (state) => null; - - set minOffsetProvider(MinOffsetProvider offsetProvider) { - _minOffsetProvider = offsetProvider; - markNeedsPaint(); - } - - set overlayContent(bool overlayContent) { - _overlayContent = overlayContent; - - if (_overlayContent != overlayContent) { - markNeedsLayout(); - } - } - - set positionAxis(HeaderPositionAxis positionAxis) { - _positionAxis = positionAxis; - - if (_positionAxis != positionAxis) { - markNeedsLayout(); - } - } - - set mainAxisAlignment(HeaderMainAxisAlignment axisAlignment) { - _mainAxisAlignment = axisAlignment; - alignment = _headerAlignment(scrollable, _mainAxisAlignment, _crossAxisAlignment); - } - - set crossAxisAlignment(HeaderCrossAxisAlignment axisAlignment) { - _crossAxisAlignment = axisAlignment; - alignment = _headerAlignment(scrollable, _mainAxisAlignment, _crossAxisAlignment); - } - - ScrollableState get scrollable => _scrollable; - - set scrollable(ScrollableState newScrollable) { - assert(newScrollable != null); - - final ScrollableState oldScrollable = _scrollable; - _scrollable = newScrollable; - - markNeedsPaint(); - - if (attached) { - oldScrollable.position.removeListener(markNeedsPaint); - newScrollable.position.addListener(markNeedsPaint); - } - } - - RenderBox get _headerBox => lastChild; - RenderBox get _contentBox => firstChild; - - RenderAbstractViewport get _viewport => RenderAbstractViewport.of(this); - - @override - void attach(PipelineOwner owner) { - super.attach(owner); - scrollable.position.addListener(markNeedsPaint); - } - - @override - void detach() { - scrollable.position.removeListener(markNeedsPaint); - super.detach(); - } - - @override - Rect describeApproximatePaintClip(RenderObject child) => - _headerOverflow ? Offset.zero & size : null; - - @override - void paint(PaintingContext context, Offset paintOffset) { - updateHeaderOffset(); - - if (overflow == Overflow.clip && _headerOverflow) { - context.pushClipRect( - needsCompositing, paintOffset, Offset.zero & size, paintStack); - } else { - paintStack(context, paintOffset); - } - } - - @override - void performLayout() { - final BoxConstraints constraints = this.constraints; - final RenderBox header = _headerBox; - - final BoxConstraints containerConstraints = constraints.loosen(); - - header.layout(containerConstraints, parentUsesSize: true); - - size = _layoutContent(containerConstraints, header.size); - - assert(size.width == constraints.constrainWidth(size.width)); - assert(size.height == constraints.constrainHeight(size.height)); - - assert(size.isFinite); - - final StackParentData headerParentData = header.parentData as StackParentData; - - headerParentData.offset = alignment.resolve(textDirection).alongOffset(size - header.size as Offset); - } - - void updateHeaderOffset() { - _headerOverflow = false; - - final double stuckOffset = _stuckOffset; - - final StackParentData parentData = _headerBox.parentData; - final double contentSize = _contentDirectionSize; - final double headerSize = _headerDirectionSize; - - final double offset = _calculateStateOffset(stuckOffset, contentSize); - final double position = offset / contentSize; - - final StickyState state = StickyState( - itemIndex, - position: position, - offset: offset, - contentSize: contentSize, - ); - - final double headerOffset = _calculateHeaderOffset( - contentSize, - stuckOffset, - headerSize, - minOffsetProvider(state) - ); - - parentData.offset = _headerDirectionalOffset( - parentData.offset, - headerOffset - ); - - _headerOverflow = _isHeaderOverflow(headerOffset, headerSize, contentSize); - - if (_lastOffset != offset) { - _lastOffset = offset; - - streamSink?.add(state.copyWith( - sticky: _isSticky( - state, - headerOffset, - _calculateHeaderOffset( - contentSize, - stuckOffset, - headerSize - ) - ) - )); - } - } - - @override - double computeMinIntrinsicWidth(double height) { - if ( - _overlayContent || - _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis || - !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis - ) { - return _contentBox.getMinIntrinsicWidth(height); - } - - return _contentBox.getMinIntrinsicWidth(height) + _headerBox.getMinIntrinsicWidth(height); - } - - @override - double computeMaxIntrinsicWidth(double height) { - if ( - _overlayContent || - _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis || - !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis - ) { - return _contentBox.getMaxIntrinsicWidth(height); - } - - return _contentBox.getMaxIntrinsicWidth(height) + _headerBox.getMaxIntrinsicWidth(height); - } - - @override - double computeMinIntrinsicHeight(double width) { - if ( - _overlayContent || - _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis || - !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis - ) { - return _contentBox.getMinIntrinsicHeight(width); - } - - return _contentBox.getMinIntrinsicHeight(width) + _headerBox.getMinIntrinsicHeight(width); - } - - @override - double computeMaxIntrinsicHeight(double width) { - if ( - _overlayContent || - _scrollDirectionVertical && _positionAxis == HeaderPositionAxis.crossAxis || - !_scrollDirectionVertical && _positionAxis == HeaderPositionAxis.mainAxis - ) { - return _contentBox.getMinIntrinsicHeight(width); - } - - return _contentBox.getMinIntrinsicHeight(width) + _headerBox.getMinIntrinsicHeight(width); - } - - bool get _scrollDirectionVertical => _scrollableAxisVertical(scrollable.axisDirection); - - bool get _alignmentStart => _mainAxisAlignment == HeaderMainAxisAlignment.start; - - double get _scrollableSize { - final viewportContainer = _viewport; - - double viewportSize; - - if (viewportContainer is RenderBox) { - final RenderBox viewportBox = viewportContainer as RenderBox; - - viewportSize = _scrollDirectionVertical - ? viewportBox.size.height - : viewportBox.size.width; - } - - assert(viewportSize != null, 'Can\'t define view port size'); - - double anchor = 0; - - if (viewportContainer is RenderViewport) { - anchor = viewportContainer.anchor; - } - - if (_alignmentStart) { - return -viewportSize * anchor; - } - - return viewportSize - viewportSize * anchor; - } - - double get _stuckOffset { - return _viewport.getOffsetToReveal(this, 0).offset - _scrollable.position.pixels - _scrollableSize; - } - - double get _contentDirectionSize => _scrollDirectionVertical - ? size.height - : size.width; - - double get _headerDirectionSize => _scrollDirectionVertical - ? _headerBox.size.height - : _headerBox.size.width; - - Offset _headerDirectionalOffset(Offset originalOffset, double offset) { - if (_scrollDirectionVertical) { - return Offset( - originalOffset.dx, - offset - ); - } - - return Offset( - offset, - originalOffset.dy - ); - } - - double _calculateStateOffset(double stuckOffset, double contentSize) { - double offset = _calculateOffset(stuckOffset, 0, contentSize); - - if (_alignmentStart) { - return offset; - } - - return contentSize - offset; - } - - double _calculateHeaderOffset( - double contentSize, - double stuckOffset, - double headerSize, - [double providedMinOffset] - ) { - if (providedMinOffset == null) { - providedMinOffset = headerSize; - } - - final double minOffset = _calculateMinOffset(contentSize, providedMinOffset); - - if (_alignmentStart) { - return _calculateOffset(stuckOffset, 0, minOffset); - } - - return _calculateOffset(stuckOffset, minOffset, contentSize) - headerSize; - } - - double _calculateOffset(double current, double minPosition, double maxPosition) { - return max(minPosition, min(-current, maxPosition)); - } - - double _calculateMinOffset(double contentSize, double minOffset) { - if (_alignmentStart) { - return contentSize - minOffset; - } - - return minOffset; - } - - bool _isHeaderOverflow(double headerOffset, double headerSize, double contentSize) { - return headerOffset < 0 || headerOffset + headerSize > contentSize; - } - - bool _isSticky( - StickyState state, - double actualHeaderOffset, - double headerOffset - ) { - return ( - actualHeaderOffset == headerOffset && - state.position > 0 && - state.position < 1 - ); - } - - Size _layoutContent(BoxConstraints constraints, Size headerSize) { - final RenderBox content = _contentBox; - final StackParentData contentParentData = content.parentData as StackParentData; - - if (!_overlayContent) { - final bool alignmentStart = _mainAxisAlignment == - HeaderMainAxisAlignment.start || - _crossAxisAlignment == HeaderCrossAxisAlignment.start; - - if ( - ( - _positionAxis == HeaderPositionAxis.crossAxis && - _scrollDirectionVertical - ) || - ( - _positionAxis == HeaderPositionAxis.mainAxis && - !_scrollDirectionVertical - ) - ) { - content.layout(constraints.copyWith( - maxWidth: constraints.maxWidth - headerSize.width - ), parentUsesSize: true); - - if (alignmentStart) { - contentParentData.offset = Offset(headerSize.width, 0); - } - - final Size contentSize = content.size; - - return Size( - contentSize.width + headerSize.width, - contentSize.height - ); - } - - if ( - ( - _positionAxis == HeaderPositionAxis.mainAxis && - _scrollDirectionVertical - ) || - ( - _positionAxis == HeaderPositionAxis.crossAxis && - !_scrollDirectionVertical - ) - ) { - content.layout(constraints.copyWith( - maxHeight: constraints.maxHeight - headerSize.height - ), parentUsesSize: true); - - if (alignmentStart) { - contentParentData.offset = Offset(0, headerSize.height); - } - - final Size contentSize = content.size; - - return Size( - contentSize.width, - contentSize.height + headerSize.height - ); - } - } - - content.layout(constraints, parentUsesSize: true); - contentParentData.offset = Offset.zero; - - return content.size; - } - - static AlignmentGeometry _headerAlignment(ScrollableState scrollable, HeaderMainAxisAlignment mainAxisAlignment, HeaderCrossAxisAlignment crossAxisAlignment) { - final bool vertical = _scrollableAxisVertical(scrollable.axisDirection); - - switch (crossAxisAlignment) { - - case HeaderCrossAxisAlignment.end: - if (mainAxisAlignment == HeaderMainAxisAlignment.end) { - return Alignment.bottomRight; - } - - return vertical ? Alignment.topRight : Alignment.bottomLeft; - - case HeaderCrossAxisAlignment.center: - if (mainAxisAlignment == HeaderMainAxisAlignment.start) { - return vertical ? Alignment.topCenter : Alignment.centerLeft; - } - - return vertical ? Alignment.bottomCenter : Alignment.centerRight; - - case HeaderCrossAxisAlignment.start: - default: - if (mainAxisAlignment == HeaderMainAxisAlignment.start) { - return Alignment.topLeft; - } - - return vertical ? Alignment.bottomLeft : Alignment.topRight; - } - } - - static bool _scrollableAxisVertical(AxisDirection direction) => [AxisDirection.up, AxisDirection.down].contains(direction); -} diff --git a/lib/sticky_infinite_list.dart b/lib/sticky_infinite_list.dart index 43170b6..76685b6 100644 --- a/lib/sticky_infinite_list.dart +++ b/lib/sticky_infinite_list.dart @@ -1,7 +1,7 @@ library sticky_infinite_list; export './widget.dart'; -export './render_new.dart'; +export './render.dart'; export './models/alignments.dart'; export './models/sticky_state.dart'; export './models/types.dart'; diff --git a/lib/widget.dart b/lib/widget.dart index 04f99ca..b35a503 100644 --- a/lib/widget.dart +++ b/lib/widget.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import './render_new.dart'; +import './render.dart'; import 'models/sticky_state.dart'; import 'models/types.dart'; import 'models/alignments.dart'; From 298dbd5cc1869a7ddfbe96a0662b8073501ef4e8 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 17:48:27 +0300 Subject: [PATCH 11/26] update readme, added documentation, make overlay flag public --- README.md | 116 ++++++++++++++++++++++++++--------- lib/models/sticky_state.dart | 13 ++-- lib/widget.dart | 18 +++--- 3 files changed, 105 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 6bccbf8..5883793 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,38 @@ class Example extends StatelessWidget { } ``` +Or with header overlay content +```dart + +import 'package:sticky_infinite_list/sticky_infinite_list.dart'; + +class Example extends StatelessWidget { + + @override + Widget build(BuildContext context) { + return InfiniteList( + builder: (BuildContext context, int index) { + /// Builder requires [InfiniteList] to be returned + return InfiniteListItem.overlay( + /// Header builder + headerBuilder: (BuildContext context) { + return Container( + ///... + ); + }, + /// Content builder + contentBuilder: (BuildContext context) { + return Container( + ///... + ); + }, + ); + } + ); + } +} +``` + ### State When min offset callback invoked or header builder is invoked @@ -230,18 +262,32 @@ InfiniteListItem( /// to define when header should be stick to the bottom of /// content. /// - /// If this method not provided or it returns `0`, + /// If this method returns `0`, /// header will be in sticky state until list item /// will be visible inside view port + /// + /// If this method not provided or it returns null, header + /// will be sticky until offset equals to + /// header size minOffsetProvider: (StickyState state) {}, - /// Header alignment - /// - /// Use [HeaderAlignment] to align header to left, - /// right, top or bottom side - /// - /// Optional. Default value [HeaderAlignment.topLeft] - headerAlignment: HeaderAlignment.topLeft, + /// Header alignment against main axis direction + /// + /// See [HeaderMainAxisAlignment] for more info + HeaderMainAxisAlignment mainAxisAlignment: HeaderMainAxisAlignment.start, + + /// Header alignment against cross axis direction + /// + /// See [HeaderCrossAxisAlignment] for more info + HeaderCrossAxisAlignment crossAxisAlignment: HeaderCrossAxisAlignment.start, + + /// Header position against scroll axis for relative positioned headers + /// + /// Only for relative header positioning + HeaderPositionAxis positionAxis: HeaderPositionAxis.mainAxis, + + /// List item padding, see [EdgeInsets] for more info + EdgeInsets padding: const EdgeInsets.all(8.0), /// Scroll direction /// @@ -330,14 +376,30 @@ Luckily you can extend and override base `InfiniteListItem` class ```dart /// Generic `I` is index type, by default list item uses `int` -class SomeCustomListItem extends InfiniteListItem { - /// Header alignment - /// - /// Supports all sides alignment, see [HeaderAlignment] for more info - /// - /// By default [HeaderAlignment.topLeft] - final HeaderAlignment headerAlignment; - + +class SomeCustomListItem extends InfiniteListItem { + /// Header alignment against main axis direction + /// + /// See [HeaderMainAxisAlignment] for more info + @override + final HeaderMainAxisAlignment mainAxisAlignment = HeaderMainAxisAlignment.start; + + /// Header alignment against cross axis direction + /// + /// See [HeaderCrossAxisAlignment] for more info + @override + final HeaderCrossAxisAlignment crossAxisAlignment = HeaderCrossAxisAlignment.start; + + /// Header position against scroll axis for relative positioned headers + /// + /// See [HeaderPositionAxis] for more info + @override + final HeaderPositionAxis positionAxis = HeaderPositionAxis.mainAxis; + + /// If header should overlay content or not + @override + final bool overlayContent = false; + /// Let item builder know if it should watch /// header position changes /// @@ -345,7 +407,7 @@ class SomeCustomListItem extends InfiniteListItem { /// each time header position changes @override bool get watchStickyState => true; - + /// Let item builder know that this class /// provides header /// @@ -353,7 +415,7 @@ class SomeCustomListItem extends InfiniteListItem { /// and never called @override bool get hasStickyHeader => true; - + /// This methods builds header /// /// If [watchStickyState] is `true`, @@ -366,24 +428,24 @@ class SomeCustomListItem extends InfiniteListItem { /// Also in that case `state` will be `null` @override Widget buildHeader(BuildContext context, [StickyState state]) {} - + /// Content item builder /// /// This method invoked only once @override - Widget buildContent(BuildContext context) => {} + Widget buildContent(BuildContext context) {} - /// Called during init state (see Statefull widget [State.initState]) + /// Called during init state (see [Statefull] widget [State.initState]) /// - /// For additional information about Statefull widget `initState` + /// For additional information about [Statefull] widget `initState` /// lifecycle - see Flutter docs @protected @mustCallSuper void initState() {} - /// Called during item dispose (see Statefull widget [State.dispose]) + /// Called during item dispose (see [Statefull] widget [State.dispose]) /// - /// For additional information about Statefull widget `dispose` + /// For additional information about [Statefull] widget `dispose` /// lifecycle - see Flutter docs @protected @mustCallSuper @@ -393,11 +455,9 @@ class SomeCustomListItem extends InfiniteListItem { #### Need more override?.. -**If you get any problems with this type of override, - please create an issue** - Alongside with list item override, to use inside `InfiniteList` builder, -you can also use `StickyListItem`, that exposed by this package too, independently. +you can also use or extend `StickyListItem`, that exposed by this +package too, independently. This class uses `Stream` to inform it's parent about header position changes diff --git a/lib/models/sticky_state.dart b/lib/models/sticky_state.dart index 157efd7..6fdd26a 100644 --- a/lib/models/sticky_state.dart +++ b/lib/models/sticky_state.dart @@ -9,13 +9,15 @@ class StickyState { /// /// `1.0` - max end position /// - /// If [InfiniteListItem.initialHeaderBuild] is true, initial - /// header render will be with position = 0 + /// If [InfiniteListItem.initialHeaderBuild] is true with [InfiniteListItem.overlay], + /// or default [InfiniteListItem] constructor is used, + /// initial header render will be with position = 0 final double position; /// Number of pixels, that outside of viewport /// - /// If [InfiniteListItem.initialHeaderBuild] is true, initial + /// If [InfiniteListItem.initialHeaderBuild] is true with [InfiniteListItem.overlay], + /// or default [InfiniteListItem] constructor is used, /// header render will be with offset = 0 /// /// For header bottom positions (or right positions for horizontal) @@ -39,8 +41,9 @@ class StickyState { /// Scroll item height. /// - /// If [InfiniteListItem.initialHeaderBuild] is true, initial - /// header render will be called without this value + /// If [InfiniteListItem.initialHeaderBuild] is true with [InfiniteListItem.overlay], + /// or default [InfiniteListItem] constructor is used, + /// initial header render will be called without this value final double contentSize; StickyState(this.index, { diff --git a/lib/widget.dart b/lib/widget.dart index b35a503..3d74d7e 100644 --- a/lib/widget.dart +++ b/lib/widget.dart @@ -81,7 +81,7 @@ class InfiniteListItem { final EdgeInsets padding; /// If header should overlay content or not - final bool _overlayContent; + final bool overlayContent; /// Default list item constructor with relative header positioning const InfiniteListItem({ @@ -93,7 +93,7 @@ class InfiniteListItem { this.crossAxisAlignment = HeaderCrossAxisAlignment.start, this.positionAxis = HeaderPositionAxis.mainAxis, this.padding, - }): _overlayContent = false, + }): overlayContent = false, initialHeaderBuild = true; /// List item constructor with overlayed header positioning @@ -108,7 +108,7 @@ class InfiniteListItem { this.padding, }) : positionAxis = HeaderPositionAxis.mainAxis, - _overlayContent = true; + overlayContent = true; /// Defines if list item has Header bool get hasStickyHeader => @@ -373,7 +373,7 @@ class _StickySliverListItemState extends State<_StickySliverListItem> { return content; } - if (widget.listItem._overlayContent) { + if (widget.listItem.overlayContent) { return StickyListItem.overlay( itemIndex: widget.index, streamSink: widget.streamController.sink, @@ -442,7 +442,7 @@ class StickyListItem extends Stack { final HeaderPositionAxis positionAxis; /// Defines if header should overlay content - final bool _overlayContent; + final bool overlayContent; /// Default sticky item constructor with relative header positioning StickyListItem({ @@ -456,7 +456,7 @@ class StickyListItem extends Stack { this.positionAxis = HeaderPositionAxis.mainAxis, Key key, }) - : _overlayContent = false, + : overlayContent = false, assert( positionAxis == HeaderPositionAxis.mainAxis || crossAxisAlignment != HeaderCrossAxisAlignment.center, 'Center cross axis alignment can\'t be used with Cross axis positioning' @@ -480,7 +480,7 @@ class StickyListItem extends Stack { this.crossAxisAlignment = HeaderCrossAxisAlignment.start, Key key, }) - : _overlayContent = true, + : overlayContent = true, positionAxis = HeaderPositionAxis.mainAxis, super( key: key, @@ -499,7 +499,7 @@ class StickyListItem extends Stack { crossAxisAlignment: crossAxisAlignment, positionAxis: positionAxis, textDirection: textDirection ?? Directionality.of(context), - overlayContent: _overlayContent, + overlayContent: overlayContent, overflow: overflow, itemIndex: itemIndex, streamSink: streamSink, @@ -520,7 +520,7 @@ class StickyListItem extends Stack { ..mainAxisAlignment = mainAxisAlignment ..crossAxisAlignment = crossAxisAlignment ..positionAxis = positionAxis - ..overlayContent = _overlayContent; + ..overlayContent = overlayContent; } } } From 9338a32e31afaeada7ca5a5ac648ec6a73c1795b Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 17:59:24 +0300 Subject: [PATCH 12/26] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0194eb1..031a98c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: >- Can be customized or with config options or with override. -version: 1.3.0 +version: 2.0.0 author: TatsuUkraine homepage: https://github.com/TatsuUkraine/flutter_sticky_infinite_list repository: https://github.com/TatsuUkraine/flutter_sticky_infinite_list From f18d27dbcf22e51699195a6cc86f0eb3d427c318 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 18:12:48 +0300 Subject: [PATCH 13/26] remove unused file, remove legacy params from readme --- README.md | 5 ++- lib/state.dart | 110 ------------------------------------------------- 2 files changed, 3 insertions(+), 112 deletions(-) delete mode 100644 lib/state.dart diff --git a/README.md b/README.md index 5883793..0e5d81b 100644 --- a/README.md +++ b/README.md @@ -317,7 +317,7 @@ But same result can be achieved with defining `anchor = 1` and `maxChildCount = 0`. In that way viewport center will be stick to the bottom and positive list won't render anything. -Additionally you can specify `headerAlignment` to any side. +Additionally you can specify header alignment to any side. ```dart import 'package:sticky_infinite_list/sticky_infinite_list.dart'; @@ -337,7 +337,8 @@ class Example extends StatelessWidget { /// Builder requires [InfiniteList] to be returned return InfiniteListItem( - headerAlignment: HeaderAlignment.bottomLeft, + mainAxisAlignment: HeaderMainAxisAlignment.end, + crossAxisAlignment: HeaderCrossAxisAlignment.start, /// Header builder headerBuilder: (BuildContext context) { diff --git a/lib/state.dart b/lib/state.dart deleted file mode 100644 index 26adc67..0000000 --- a/lib/state.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:flutter/widgets.dart'; - -typedef Widget ContentBuilder(BuildContext context); -typedef Widget HeaderStateBuilder( - BuildContext context, StickyState state); -typedef Widget HeaderBuilder(BuildContext context); -typedef double MinOffsetProvider(StickyState state); - -/// List direction variants -enum InfiniteListDirection { - /// Render only positive infinite list - single, - - /// Render both positive and negative infinite lists - multi, -} - -/// Alignment options -/// -/// [HeaderAlignment.bottomLeft], [HeaderAlignment.bottomRight] and -/// [HeaderAlignment.bottomCenter] header will be positioned -/// against content bottom edge for vertical scroll -/// -/// [HeaderAlignment.topRight], [HeaderAlignment.bottomRight] and -/// [HeaderAlignment.canterRight] header will be positioned -/// against content right edge for horizontal scroll -/// -/// Which also means that headers will become sticky, when content -/// bottom edge (or right edge for horizontal) will -/// go outside of ViewPort bottom (right for horizontal) edge -/// -/// It also affects on [StickyState.offset] value, since in that case -/// hidden size will be calculated against bottom edges -enum HeaderAlignment { - topLeft, - topCenter, - topRight, - bottomLeft, - bottomCenter, - bottomRight, - centerLeft, - centerRight, -} - -/// Sticky state object -/// that describes header position and content height -class StickyState { - /// Position, that header already passed - /// - /// Value can be between 0.0 and 1.0 - /// - /// If it's `0.0` - sticky in max start position - /// - /// `1.0` - max end position - /// - /// If [InfiniteListItem.initialHeaderBuild] is true, initial - /// header render will be with position = 0 - final double position; - - /// Number of pixels, that outside of viewport - /// - /// If [InfiniteListItem.initialHeaderBuild] is true, initial - /// header render will be with offset = 0 - /// - /// For header bottom positions (or right positions for horizontal) - /// offset value also will be amount of pixels that was scrolled away - final double offset; - - /// Item index - final I index; - - /// If header is in sticky state - /// - /// If [InfiniteListItem.minOffsetProvider] is defined, - /// it could be that header builder will be emitted with new state - /// on scroll, but [sticky] will be false, if offset already passed - /// min offset value - /// - /// WHen [InfiniteListItem.minOffsetProvider] is called, [sticky] - /// will always be `false`. Since for min offset calculation - /// offset itself not defined yet - final bool sticky; - - /// Scroll item height. - /// - /// If [InfiniteListItem.initialHeaderBuild] is true, initial - /// header render will be called without this value - final double contentSize; - - StickyState(this.index, { - this.position = 0, - this.offset = 0, - this.sticky = false, - this.contentSize - }); - - /// Create state duplicate, with optional state options override - StickyState copyWith({ - double position, - double offset, - bool sticky, - double contentHeight - }) => StickyState( - index, - position: position ?? this.position, - offset: offset ?? this.offset, - sticky: sticky ?? this.sticky, - contentSize: contentHeight ?? this.contentSize, - ); -} From 1f90fb096c6fa7ce8bb8e9c66fc5a7fe8ac5e2d2 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 18:22:32 +0300 Subject: [PATCH 14/26] fixed content offset calculation --- lib/render.dart | 64 ++++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/lib/render.dart b/lib/render.dart index 56b81b5..6e5965f 100644 --- a/lib/render.dart +++ b/lib/render.dart @@ -387,67 +387,81 @@ class StickyListItemRenderObject extends RenderStack { Size _layoutContent(BoxConstraints constraints, Size headerSize) { final RenderBox content = _contentBox; final StackParentData contentParentData = content.parentData as StackParentData; + contentParentData.offset = Offset.zero; if (!_overlayContent) { - final bool alignmentStart = _mainAxisAlignment == - HeaderMainAxisAlignment.start || - _crossAxisAlignment == HeaderCrossAxisAlignment.start; - if ( - ( + ( _positionAxis == HeaderPositionAxis.crossAxis && - _scrollDirectionVertical - ) || - ( - _positionAxis == HeaderPositionAxis.mainAxis && - !_scrollDirectionVertical - ) + _scrollDirectionVertical + ) || + ( + _positionAxis == HeaderPositionAxis.mainAxis && + !_scrollDirectionVertical + ) ) { content.layout(constraints.copyWith( maxWidth: constraints.maxWidth - headerSize.width ), parentUsesSize: true); - if (alignmentStart) { + if ( + ( + _crossAxisAlignment == HeaderCrossAxisAlignment.start && + _scrollDirectionVertical + ) || + ( + _mainAxisAlignment == HeaderMainAxisAlignment.start && + !_scrollDirectionVertical + ) + ) { contentParentData.offset = Offset(headerSize.width, 0); } final Size contentSize = content.size; return Size( - contentSize.width + headerSize.width, - contentSize.height + contentSize.width + headerSize.width, + contentSize.height ); } if ( - ( + ( _positionAxis == HeaderPositionAxis.mainAxis && - _scrollDirectionVertical - ) || - ( - _positionAxis == HeaderPositionAxis.crossAxis && - !_scrollDirectionVertical - ) + _scrollDirectionVertical + ) || + ( + _positionAxis == HeaderPositionAxis.crossAxis && + !_scrollDirectionVertical + ) ) { content.layout(constraints.copyWith( maxHeight: constraints.maxHeight - headerSize.height ), parentUsesSize: true); - if (alignmentStart) { + if ( + ( + _mainAxisAlignment == HeaderMainAxisAlignment.start && + _scrollDirectionVertical + ) || + ( + _crossAxisAlignment == HeaderCrossAxisAlignment.start && + !_scrollDirectionVertical + ) + ) { contentParentData.offset = Offset(0, headerSize.height); } final Size contentSize = content.size; return Size( - contentSize.width, - contentSize.height + headerSize.height + contentSize.width, + contentSize.height + headerSize.height ); } } content.layout(constraints, parentUsesSize: true); - contentParentData.offset = Offset.zero; return content.size; } From 8df27dc0b63819b7e4192f32779d36ebc61171a7 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 18:37:32 +0300 Subject: [PATCH 15/26] rename min/max count props to pos/neg --- README.md | 16 +++++++--------- example/example.dart | 4 ++-- lib/widget.dart | 18 ++++++------------ 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 0e5d81b..7517670 100644 --- a/README.md +++ b/README.md @@ -195,21 +195,19 @@ InfiniteList( /// If you need infinite list in both directions use `InfiniteListDirection.multi` direction: InfiniteListDirection.multi, - /// Min child count. + /// Negative max child count. /// /// Will be used only when `direction: InfiniteListDirection.multi` /// - /// Accepts negative values only - /// /// If it's not provided, scroll will be infinite in negative direction - minChildCount: -100, + negChildCount: 100, - /// Max child count + /// Positive max child count /// /// Specifies number of elements for forward list /// /// If it's not provided, scroll will be infinite in positive direction - maxChildCount: 100, + posChildCount: 100, /// ScrollView anchor value. anchor: 0.0, @@ -314,8 +312,8 @@ InfiniteListItem( Currently package doesn't support `CustomScrollView.reverse` option. But same result can be achieved with defining `anchor = 1` and -`maxChildCount = 0`. In that way viewport center will be stick -to the bottom and positive list won't render anything. +`posChildCount = 0`. In that way viewport center will be stick to the +bottom and positive list won't render anything. Additionally you can specify header alignment to any side. @@ -331,7 +329,7 @@ class Example extends StatelessWidget { direction: InfiniteListDirection.multi, - maxChildCount: 0, + posChildCount: 0, builder: (BuildContext context, int index) { /// Builder requires [InfiniteList] to be returned diff --git a/example/example.dart b/example/example.dart index 0710a7d..768f61e 100644 --- a/example/example.dart +++ b/example/example.dart @@ -19,14 +19,14 @@ class Example extends StatelessWidget { /// Will be ignored if [direction] is forward /// /// If it's `null`, list will be infinite - minChildCount: -100, + negChildCount: 100, /// Render 100 elements in positive direction. `Optional` /// /// If it's not provided, scroll will be infinite in positive direction /// /// If it's `null`, list will be infinite - maxChildCount: 100, + posChildCount: 100, /// ViewPort anchor value. See [ScrollView] docs for more info anchor: 0.0, diff --git a/lib/widget.dart b/lib/widget.dart index 3d74d7e..35594aa 100644 --- a/lib/widget.dart +++ b/lib/widget.dart @@ -194,15 +194,12 @@ class InfiniteList extends StatefulWidget { final InfiniteListDirection direction; /// Max child count for positive direction list - final int maxChildCount; + final int posChildCount; /// Max child count for negative list direction /// /// Ignored when [direction] is [InfiniteListDirection.single] - /// - /// This value should have negative value in order to provide right calculation - /// for negative list - final int minChildCount; + final int negChildCount; /// Proxy property for [ScrollView.reverse] /// @@ -236,8 +233,8 @@ class InfiniteList extends StatefulWidget { @required this.builder, this.controller, this.direction = InfiniteListDirection.single, - this.maxChildCount, - this.minChildCount, + this.posChildCount, + this.negChildCount, //this.reverse = false, this.anchor = 0.0, this.cacheExtent, @@ -254,15 +251,12 @@ class _InfiniteListState extends State { StreamController _streamController = StreamController>.broadcast(); - int get _reverseChildCount => - widget.minChildCount == null ? null : widget.minChildCount * -1; - SliverList get _reverseList => SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) => _buildListItem(context, (index + 1) * -1), - childCount: _reverseChildCount, + childCount: widget.negChildCount, ), ); @@ -270,7 +264,7 @@ class _InfiniteListState extends State { SliverList( delegate: SliverChildBuilderDelegate( _buildListItem, - childCount: widget.maxChildCount, + childCount: widget.posChildCount, ), key: widget._centerKey, ); From 2bac1cf768e7eeda336f434425d253ee04c77e4f Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 18:53:31 +0300 Subject: [PATCH 16/26] use static text direction --- lib/render.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/render.dart b/lib/render.dart index 6e5965f..074ab8c 100644 --- a/lib/render.dart +++ b/lib/render.dart @@ -164,7 +164,7 @@ class StickyListItemRenderObject extends RenderStack { final StackParentData headerParentData = header.parentData as StackParentData; - headerParentData.offset = alignment.resolve(textDirection).alongOffset(size - header.size as Offset); + headerParentData.offset = alignment.resolve(TextDirection.ltr).alongOffset(size - header.size as Offset); } void updateHeaderOffset() { From 0c98b0085f7bd0e06d7f27f466a8154f9db03868 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 19:07:49 +0300 Subject: [PATCH 17/26] update class docs --- lib/models/alignments.dart | 11 +++++++++++ lib/widget.dart | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/models/alignments.dart b/lib/models/alignments.dart index 75327da..4c9dd0d 100644 --- a/lib/models/alignments.dart +++ b/lib/models/alignments.dart @@ -1,13 +1,24 @@ /// Header position axis for content without header overflow enum HeaderPositionAxis { /// Align against main axis direction + /// + /// For vertical scroll column direction will be used, for + /// horizontal scroll - row mainAxis, /// Align against cross axis direction + /// + /// For vertical scroll row direction will be used, for + /// horizontal scroll - column crossAxis, } /// Main axis direction alignment +/// +/// For vertical scroll, header will be aligned to the top and bottom edges. +/// For horizontal scroll header will be aligned to the left and right edges +/// +/// It also affect side where sticky header will be sticked. enum HeaderMainAxisAlignment { /// Start position against main axis /// diff --git a/lib/widget.dart b/lib/widget.dart index 35594aa..259aa7d 100644 --- a/lib/widget.dart +++ b/lib/widget.dart @@ -62,7 +62,9 @@ class InfiniteListItem { /// It's required due to layout container and define it's actual dimensions final bool initialHeaderBuild; - /// Header alignment against main axis direction + /// Header alignment against main axis direction. + /// + /// Affects header stick side. /// /// See [HeaderMainAxisAlignment] for more info final HeaderMainAxisAlignment mainAxisAlignment; @@ -422,6 +424,8 @@ class StickyListItem extends Stack { /// Header alignment against main axis direction /// + /// Affects header stick side. + /// /// See [HeaderMainAxisAlignment] for more info final HeaderMainAxisAlignment mainAxisAlignment; From 0de45d897f9c8a6ad4b00f57c5315c12c7fecc3f Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 19:17:14 +0300 Subject: [PATCH 18/26] added migration guide --- MIGRATION.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 5 +++++ 2 files changed, 52 insertions(+) create mode 100644 MIGRATION.md diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..2d4dbb9 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,47 @@ +# Migration guide + +## Migration From v1.x.x to v2.x.x + +### Child count params + +In `InfiniteList` next params for max child count was renamed: +- `minChildCount` was renamed to `negChildCount` and now it works with + positive numbers +- `maxChildCount` was renamed to `posChildCount` and, as before, it + works with positive numbers + +### Header alignment + +Param `headerAlignment` in `InfiniteListItem` was replaced with 2 +params: `mainAxisAlignment` and `crossAxisAlignment` + +Main axis is placed with scroll direction: vertical or horizontal. + +With `mainAxisAlignment: HeaderMainAxisAlignment.start` and vertical +scroll header will stick to the top edge, with horizontal scroll - to +the left edge. Similar with `mainAxisAlignment: +HeaderMainAxisAlignment.end` - bottom and right side respectively. + +`crossAxisAlignment` doesn't affect stick side. It just places header to +the left or right side for the vertical scroll, and top or bottom - for +horizontal scroll. + +New parameter was added for relative positioning: `positionAxis` which +defines what direction should be used during layout - column or row. + +### List item layout + +Comparing to v1, v2 by default uses relative positioning. + +To make header overlay content use constructor `overlay`. It's available +in both `InfiniteListItem` and `StickyListItem` widgets. + +### Initial header render + +In default constructor param `initialHeaderBuild` was removed. + +Since default constructor uses relative positioning, header is required +to calculate appropriate item size. + +`initialHeaderBuild` is still available in `overlay` constructors and +affects header render like it was before in v1.x.x diff --git a/README.md b/README.md index 7517670..9c5b81b 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,11 @@ benefits for performance that Flutter provides. - dynamic header build on content scroll - dynamic min offset calculation on content scroll +## Migration guide + +If you using older MAJOR versions, please +[visit this migration guide](https://github.com/TatsuUkraine/flutter_sticky_infinite_list/blob/master/MIGRATION.md) + ## Demo From dad452f21669a76572a4d521e8e369849393b22d Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 22:40:55 +0300 Subject: [PATCH 19/26] update base example link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c5b81b..f3a163c 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ If you using older MAJOR versions, please ## Demo - + ## Getting Started From 6ed40afb4917e96acf35bf6eb921c820ec5ecd84 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 23:08:32 +0300 Subject: [PATCH 20/26] update positioning examples --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f3a163c..e1927d7 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,17 @@ InfiniteListItem( #### Header alignment demo - +Relative positioning + + + +Relative cross axis positioning + + + +Overlay positioning + + #### Horizontal scroll demo From d8121b02d358b4900b903310c2b02a161524f458 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 23:29:53 +0300 Subject: [PATCH 21/26] update examples --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1927d7..df2cc68 100644 --- a/README.md +++ b/README.md @@ -320,7 +320,7 @@ Overlay positioning #### Horizontal scroll demo - + ### Reverse infinite scroll From 4d3d77db96f64072b53f9443d0f516151ce11401 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 23:35:33 +0300 Subject: [PATCH 22/26] update examples --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df2cc68..fbb9d67 100644 --- a/README.md +++ b/README.md @@ -374,7 +374,7 @@ class Example extends StatelessWidget { #### Demo - + For more info take a look at [Example](https://github.com/TatsuUkraine/flutter_sticky_infinite_list_example) project From 4f0c02dcb8cfb7129e724f8a72a57c28877e1933 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Wed, 1 Jul 2020 23:41:22 +0300 Subject: [PATCH 23/26] update single scroll examples --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fbb9d67..c97f375 100644 --- a/README.md +++ b/README.md @@ -493,12 +493,24 @@ Widget build(BuildContext context) { child: Placeholder(), ), StickyListItem( + streamSink: _headerStream.sink, /// stream to update header during scroll header: Container( - height: 30, + height: _headerHeight, width: double.infinity, color: Colors.orange, child: Center( - child: Text('Sticky Header') + child: StreamBuilder>( + stream: _headerStream.stream, /// stream to update header during scroll + builder: (_, snapshot) { + if (!snapshot.hasData) { + return Container(); + } + + final position = (snapshot.data.position * 100).round(); + + return Text('Positioned relative. Position: $position%'); + }, + ), ), ), content: Container( @@ -506,7 +518,35 @@ Widget build(BuildContext context) { color: Colors.blueAccent, child: Placeholder(), ), - itemIndex: 'single-child-index', + itemIndex: "single-child", + ), + StickyListItem.overlay( + streamSink: _headerOverlayStream.sink, /// stream to update header during scroll + header: Container( + height: _headerHeight, + width: double.infinity, + color: Colors.orange, + child: Center( + child: StreamBuilder>( + stream: _headerOverlayStream.stream, /// stream to update header during scroll + builder: (_, snapshot) { + if (!snapshot.hasData) { + return Container(); + } + + final position = (snapshot.data.position * 100).round(); + + return Text('Positioned overlay. Position: $position%'); + }, + ), + ), + ), + content: Container( + height: height, + color: Colors.lightBlueAccent, + child: Placeholder(), + ), + itemIndex: "single-overlayed-child", ), Container( height: height, @@ -519,12 +559,12 @@ Widget build(BuildContext context) { } ``` -This code will render single child scroll -with 3 widgets. Middle one - item with sticky header. +This code will render single child scroll with 4 widgets. Two middle +items - items with sticky header. **Demo** - + For more complex example please take a look at "Single Example" page in [Example project](https://github.com/TatsuUkraine/flutter_sticky_infinite_list_example) From 54d5b4d12ee31d691e40a5253ab6d544ec65d957 Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Thu, 2 Jul 2020 00:27:36 +0300 Subject: [PATCH 24/26] added release description to changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a46380..a91a8a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [2.0.0] - 2020-07-02 + +**Features** +- refactor sticky item layout calculation +- split alignment param on main and cross axis alignment +- added relative positioning for header + [#19](https://github.com/TatsuUkraine/flutter_sticky_infinite_list/issues/19) + +**Release contains breaking changes, see MIGRATION.md for more details** + ## [1.3.0] - 2020-04-28 **Features** From 335ad3140a35e5f50fa2b5a9ed6589b089a04a0f Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Thu, 2 Jul 2020 00:29:02 +0300 Subject: [PATCH 25/26] updated release description to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a91a8a4..c85d60e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ **Features** - refactor sticky item layout calculation - split alignment param on main and cross axis alignment +- added padding property to `InfiniteListItem` - added relative positioning for header [#19](https://github.com/TatsuUkraine/flutter_sticky_infinite_list/issues/19) From 340610a8c617522fe8207d4eb17781f6873be06d Mon Sep 17 00:00:00 2001 From: Denis Beketsky Date: Thu, 2 Jul 2020 00:49:39 +0300 Subject: [PATCH 26/26] remove author from pubspec --- pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 031a98c..1085c22 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,6 @@ description: >- Can be customized or with config options or with override. version: 2.0.0 -author: TatsuUkraine homepage: https://github.com/TatsuUkraine/flutter_sticky_infinite_list repository: https://github.com/TatsuUkraine/flutter_sticky_infinite_list issue_tracker: https://github.com/TatsuUkraine/flutter_sticky_infinite_list/issues