From 93ad744857310eb3fd114bb35f0338c3917a0c5b Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Fri, 15 May 2020 10:27:33 +0200 Subject: [PATCH] Add stickToVisibleContent Adds an option to start scrolling the header once the bottom of the body is visible rather than remaining sticky until the header no longer fits in the visible content. --- lib/sticky_headers/render.dart | 25 +++++++++++++++++++++---- lib/sticky_headers/widget.dart | 13 ++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/sticky_headers/render.dart b/lib/sticky_headers/render.dart index 3e49dff..bc0edef 100644 --- a/lib/sticky_headers/render.dart +++ b/lib/sticky_headers/render.dart @@ -28,17 +28,20 @@ class RenderStickyHeader extends RenderBox RenderStickyHeaderCallback _callback; ScrollableState _scrollable; bool _overlapHeaders; + bool _stickToVisibleContent; RenderStickyHeader({ @required ScrollableState scrollable, RenderStickyHeaderCallback callback, bool overlapHeaders: false, + bool stickToVisibleContent: false, RenderBox header, RenderBox content, }) : assert(scrollable != null), _scrollable = scrollable, _callback = callback, - _overlapHeaders = overlapHeaders { + _overlapHeaders = overlapHeaders, + _stickToVisibleContent = stickToVisibleContent { if (content != null) add(content); if (header != null) add(header); } @@ -73,6 +76,14 @@ class RenderStickyHeader extends RenderBox markNeedsLayout(); } + set stickToVisibleContent(bool newValue) { + if (_stickToVisibleContent == newValue) { + return; + } + _stickToVisibleContent = newValue; + markNeedsLayout(); + } + @override void attach(PipelineOwner owner) { super.attach(owner); @@ -117,7 +128,7 @@ class RenderStickyHeader extends RenderBox contentParentData.offset = new Offset(0.0, _overlapHeaders ? 0.0 : headerHeight); // determine by how much the header should be stuck to the top - final double stuckOffset = determineStuckOffset(); + final double stuckOffset = determineStuckOffset(height); // place header over content relative to scroll offset final double maxOffset = height - headerHeight; @@ -131,11 +142,17 @@ class RenderStickyHeader extends RenderBox } } - double determineStuckOffset() { + double determineStuckOffset(double height) { final scrollBox = _scrollable.context.findRenderObject(); if (scrollBox?.attached ?? false) { try { - return localToGlobal(Offset.zero, ancestor: scrollBox).dy; + double scrollOffset = localToGlobal(Offset.zero, ancestor: scrollBox).dy; + + if (_stickToVisibleContent && scrollBox is RenderRepaintBoundary) { + scrollOffset = max(scrollOffset, scrollBox.constraints.maxHeight - height); + } + + return scrollOffset; } catch (e) { // ignore and fall-through and return 0.0 } diff --git a/lib/sticky_headers/widget.dart b/lib/sticky_headers/widget.dart index 333fe2e..57f145c 100644 --- a/lib/sticky_headers/widget.dart +++ b/lib/sticky_headers/widget.dart @@ -34,6 +34,7 @@ class StickyHeader extends MultiChildRenderObjectWidget { @required this.header, @required this.content, this.overlapHeaders: false, + this.stickToVisibleContent: false, this.callback, }) : super( key: key, @@ -50,6 +51,9 @@ class StickyHeader extends MultiChildRenderObjectWidget { /// If true, the header will overlap the Content. final bool overlapHeaders; + /// If true, the header starts scrolling once the bottom of the content is visible. + final bool stickToVisibleContent; + /// Optional callback with stickyness value. If you think you need this, then you might want to /// consider using [StickyHeaderBuilder] instead. final RenderStickyHeaderCallback callback; @@ -62,6 +66,7 @@ class StickyHeader extends MultiChildRenderObjectWidget { scrollable: scrollable, callback: this.callback, overlapHeaders: this.overlapHeaders, + stickToVisibleContent: this.stickToVisibleContent, ); } @@ -70,7 +75,8 @@ class StickyHeader extends MultiChildRenderObjectWidget { renderObject ..scrollable = Scrollable.of(context) ..callback = this.callback - ..overlapHeaders = this.overlapHeaders; + ..overlapHeaders = this.overlapHeaders + ..stickToVisibleContent = this.stickToVisibleContent; } } @@ -88,6 +94,7 @@ class StickyHeaderBuilder extends StatefulWidget { @required this.builder, this.content, this.overlapHeaders: false, + this.stickToVisibleContent: false, }) : super(key: key); /// Called when the sticky amount changes for the header. @@ -100,6 +107,9 @@ class StickyHeaderBuilder extends StatefulWidget { /// If true, the header will overlap the Content. final bool overlapHeaders; + /// If true, the header starts scrolling once the bottom of the content is visible. + final bool stickToVisibleContent; + @override _StickyHeaderBuilderState createState() => new _StickyHeaderBuilderState(); } @@ -111,6 +121,7 @@ class _StickyHeaderBuilderState extends State { Widget build(BuildContext context) { return new StickyHeader( overlapHeaders: widget.overlapHeaders, + stickToVisibleContent: widget.stickToVisibleContent, header: new LayoutBuilder( builder: (context, _) => widget.builder(context, _stuckAmount ?? 0.0), ),