From 5efc0eafd5fca6c7f68f49073d17139cebe65a34 Mon Sep 17 00:00:00 2001 From: UnderKoen Date: Wed, 16 Jun 2021 22:45:11 +0200 Subject: [PATCH] Added the possibility to have multiple slivers as child, and even sticky header (currently one one inside one other works). Also solves #62 --- example/lib/examples/double_list.dart | 63 +++++ example/lib/examples/indented_header.dart | 89 +++++++ example/lib/main.dart | 10 + lib/src/rendering/sliver_sticky_header.dart | 273 +++++++++++++------- lib/src/widgets/sliver_sticky_header.dart | 46 +++- 5 files changed, 377 insertions(+), 104 deletions(-) create mode 100644 example/lib/examples/double_list.dart create mode 100644 example/lib/examples/indented_header.dart diff --git a/example/lib/examples/double_list.dart b/example/lib/examples/double_list.dart new file mode 100644 index 0000000..2d97e9c --- /dev/null +++ b/example/lib/examples/double_list.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +import '../common.dart'; + +class DoubleListExample extends StatelessWidget { + const DoubleListExample({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: 'List Example', + slivers: [ + _StickyHeaderList(index: 0), + _StickyHeaderList(index: 1), + _StickyHeaderList(index: 2), + _StickyHeaderList(index: 3), + ], + ); + } +} + +class _StickyHeaderList extends StatelessWidget { + const _StickyHeaderList({ + Key? key, + this.index, + }) : super(key: key); + + final int? index; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + header: Header(index: index), + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + leading: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + trailing: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 3, + ), + ), + ], + ); + } +} diff --git a/example/lib/examples/indented_header.dart b/example/lib/examples/indented_header.dart new file mode 100644 index 0000000..3a85724 --- /dev/null +++ b/example/lib/examples/indented_header.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +import '../common.dart'; + +class IndentedHeaderExample extends StatelessWidget { + const IndentedHeaderExample({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: 'List Example', + slivers: [ + _StickyHeaderList(index: 0), + _StickyHeaderList(index: 1), + _StickyHeaderList(index: 2), + _StickyHeaderList(index: 3), + ], + ); + } +} + +class _StickyHeaderList extends StatelessWidget { + const _StickyHeaderList({ + Key? key, + this.index, + }) : super(key: key); + + final int? index; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + header: Header(index: index), + slivers: [ + SliverStickyHeader( + header: Header(title: "Subheader #1"), + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + leading: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + ], + ), + SliverStickyHeader( + header: Header(title: "Subheader #2"), + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + leading: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + ], + ), + SliverStickyHeader( + header: Header(title: "Subheader #3"), + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + leading: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + ], + ), + ], + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index e1ddcc5..be9cedd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,3 +1,5 @@ +import 'package:example/examples/double_list.dart'; +import 'package:example/examples/indented_header.dart'; import 'package:flutter/material.dart'; import 'examples/animated_header.dart'; @@ -65,6 +67,14 @@ class _Home extends StatelessWidget { text: 'Mixing other slivers', builder: (_) => const MixSliversExample(), ), + _Item( + text: 'Multiple Slivers List Example', + builder: (_) => const DoubleListExample(), + ), + _Item( + text: 'Indented Slivers List Example', + builder: (_) => const IndentedHeaderExample(), + ), ], ), ); diff --git a/lib/src/rendering/sliver_sticky_header.dart b/lib/src/rendering/sliver_sticky_header.dart index c813340..bf63a04 100644 --- a/lib/src/rendering/sliver_sticky_header.dart +++ b/lib/src/rendering/sliver_sticky_header.dart @@ -9,11 +9,11 @@ import 'package:value_layout_builder/value_layout_builder.dart'; /// A sliver with a [RenderBox] as header and a [RenderSliver] as child. /// /// The [header] stays pinned when it hits the start of the viewport until -/// the [child] scrolls off the viewport. +/// the [children] scrolls off the viewport. class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { RenderSliverStickyHeader({ RenderObject? header, - RenderSliver? child, + List children = const [], bool overlapsContent: false, bool sticky: true, StickyHeaderController? controller, @@ -21,7 +21,7 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { _sticky = sticky, _controller = controller { this.header = header as RenderBox?; - this.child = child; + this.children = children; } SliverStickyHeaderState? _oldState; @@ -68,14 +68,36 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { if (_header != null) adoptChild(_header!); } - /// The render object's unique child - RenderSliver? get child => _child; - RenderSliver? _child; + /// The render object's children + List get children => _children; + List _children = []; + + set children(List value) { + _children.forEach((c) { + if (c != null) dropChild(c); + }); + _children = value; + _children.forEach((c) { + if (c != null) adoptChild(c); + }); + } + + void removeChild(int i) { + assert(children.length > i); + + var child = children[i]; + if (child == null) return; + dropChild(child); + children[i] = null; + } + + void addChild(int i, RenderSliver? newChild) { + assert(children.length > i); - set child(RenderSliver? value) { - if (_child != null) dropChild(_child!); - _child = value; - if (_child != null) adoptChild(_child!); + var child = children[i]; + if (child != null) dropChild(child); + children[i] = newChild; + if (newChild != null) adoptChild(newChild); } @override @@ -88,26 +110,30 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { void attach(PipelineOwner owner) { super.attach(owner); if (_header != null) _header!.attach(owner); - if (_child != null) _child!.attach(owner); + _children.forEach((c) => c?.attach(owner)); } @override void detach() { super.detach(); if (_header != null) _header!.detach(); - if (_child != null) _child!.detach(); + _children.forEach((c) => c?.detach()); } @override void redepthChildren() { if (_header != null) redepthChild(_header!); - if (_child != null) redepthChild(_child!); + _children.forEach((c) { + if (c != null) redepthChild(c); + }); } @override void visitChildren(RenderObjectVisitor visitor) { if (_header != null) visitor(_header!); - if (_child != null) visitor(_child!); + _children.forEach((c) { + if (c != null) visitor(c); + }); } @override @@ -116,9 +142,13 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { if (header != null) { result.add(header!.toDiagnosticsNode(name: 'header')); } - if (child != null) { - result.add(child!.toDiagnosticsNode(name: 'child')); + + var i = 0; + for (var c in children) { + if (c != null) result.add(c.toDiagnosticsNode(name: 'child$i')); + i++; } + return result; } @@ -135,9 +165,12 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { double? get headerLogicalExtent => overlapsContent ? 0.0 : _headerExtent; + bool get noRenderChilds => + children.isEmpty || !children.any((c) => c != null); + @override void performLayout() { - if (header == null && child == null) { + if (header == null && noRenderChilds) { geometry = SliverGeometry.zero; return; } @@ -164,7 +197,7 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { final double headerCacheExtent = calculateCacheOffset(constraints, from: 0.0, to: headerExtent); - if (child == null) { + if (noRenderChilds) { geometry = SliverGeometry( scrollExtent: headerExtent, maxPaintExtent: headerExtent, @@ -174,74 +207,125 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { hasVisualOverflow: headerExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0); } else { - child!.layout( - constraints.copyWith( - scrollOffset: math.max(0.0, constraints.scrollOffset - headerExtent), - cacheOrigin: math.min(0.0, constraints.cacheOrigin + headerExtent), - overlap: 0.0, - remainingPaintExtent: - constraints.remainingPaintExtent - headerPaintExtent, - remainingCacheExtent: - constraints.remainingCacheExtent - headerCacheExtent, - ), - parentUsesSize: true, - ); - final SliverGeometry childLayoutGeometry = child!.geometry!; - if (childLayoutGeometry.scrollOffsetCorrection != null) { - geometry = SliverGeometry( - scrollOffsetCorrection: childLayoutGeometry.scrollOffsetCorrection, - ); - return; + List geometries = []; + + for (var child in children) { + var prev = geometries.length > 0 + ? geometries + .map((g) => g.maxPaintExtent) + .reduce((d1, d2) => d1 + d2) + : 0; + if (child != null) { + child.layout( + constraints.copyWith( + scrollOffset: math.max( + 0.0, + constraints.scrollOffset - headerExtent - prev, + ), + cacheOrigin: math.min( + 0.0, + constraints.cacheOrigin + headerExtent + prev, + ), + overlap: math.min( + math.max( + 0.0, + constraints.scrollOffset - prev, + ), + headerExtent, + ), + remainingPaintExtent: math.max( + 0.0, + constraints.remainingPaintExtent - headerPaintExtent, + ), + remainingCacheExtent: math.max( + 0.0, + constraints.remainingCacheExtent - headerCacheExtent, + ), + ), + parentUsesSize: true, + ); + + final SliverGeometry childLayoutGeometry = child.geometry!; + if (childLayoutGeometry.scrollOffsetCorrection != null) { + geometry = SliverGeometry( + scrollOffsetCorrection: + childLayoutGeometry.scrollOffsetCorrection, + ); + return; + } + geometries.add(childLayoutGeometry); + } } + final double childPaintExtent = + geometries.map((c) => c.paintExtent).reduce((d1, d2) => d1 + d2); + final double childLayoutExtent = + geometries.map((c) => c.layoutExtent).reduce((d1, d2) => d1 + d2); + final double childScrollExtent = + geometries.map((c) => c.scrollExtent).reduce((d1, d2) => d1 + d2); + final double childCacheExtent = + geometries.map((c) => c.cacheExtent).reduce((d1, d2) => d1 + d2); + final double childMaxPaintExtent = + geometries.map((c) => c.maxPaintExtent).reduce((d1, d2) => d1 + d2); + final double childHitTestExtent = + geometries.map((c) => c.hitTestExtent).reduce((d1, d2) => d1 + d2); + final bool childHasVisualOverflow = + geometries.any((c) => c.hasVisualOverflow); + final double paintExtent = math.min( headerPaintExtent + - math.max(childLayoutGeometry.paintExtent, - childLayoutGeometry.layoutExtent), + math.max( + childPaintExtent, + childLayoutExtent, + ), constraints.remainingPaintExtent, ); geometry = SliverGeometry( - scrollExtent: headerExtent + childLayoutGeometry.scrollExtent, + scrollExtent: headerExtent + childScrollExtent, paintExtent: paintExtent, - layoutExtent: math.min( - headerPaintExtent + childLayoutGeometry.layoutExtent, paintExtent), - cacheExtent: math.min( - headerCacheExtent + childLayoutGeometry.cacheExtent, + layoutExtent: + math.min(headerPaintExtent + childLayoutExtent, paintExtent), + cacheExtent: math.min(headerCacheExtent + childCacheExtent, constraints.remainingCacheExtent), - maxPaintExtent: headerExtent + childLayoutGeometry.maxPaintExtent, - hitTestExtent: math.max( - headerPaintExtent + childLayoutGeometry.paintExtent, - headerPaintExtent + childLayoutGeometry.hitTestExtent), - hasVisualOverflow: childLayoutGeometry.hasVisualOverflow, + maxPaintExtent: headerExtent + childMaxPaintExtent, + hitTestExtent: math.max(headerPaintExtent + childPaintExtent, + headerPaintExtent + childHitTestExtent), + hasVisualOverflow: childHasVisualOverflow, ); - final SliverPhysicalParentData? childParentData = - child!.parentData as SliverPhysicalParentData?; - switch (axisDirection) { - case AxisDirection.up: - childParentData!.paintOffset = Offset.zero; - break; - case AxisDirection.right: - childParentData!.paintOffset = Offset( - calculatePaintOffset(constraints, from: 0.0, to: headerExtent), - 0.0); - break; - case AxisDirection.down: - childParentData!.paintOffset = Offset(0.0, - calculatePaintOffset(constraints, from: 0.0, to: headerExtent)); - break; - case AxisDirection.left: - childParentData!.paintOffset = Offset.zero; - break; + double i = headerExtent; + for (var child in children) { + final SliverPhysicalParentData? childParentData = + child?.parentData as SliverPhysicalParentData?; + + switch (axisDirection) { + case AxisDirection.right: + childParentData!.paintOffset = Offset( + calculatePaintOffset(constraints, from: 0.0, to: i), 0.0); + break; + case AxisDirection.down: + childParentData!.paintOffset = Offset( + 0.0, calculatePaintOffset(constraints, from: 0.0, to: i)); + break; + case AxisDirection.up: + case AxisDirection.left: + childParentData!.paintOffset = Offset.zero; + break; + } + i += child?.geometry?.maxPaintExtent ?? 0; } } if (header != null) { final SliverPhysicalParentData? headerParentData = header!.parentData as SliverPhysicalParentData?; - final double childScrollExtent = child?.geometry?.scrollExtent ?? 0.0; - final double headerPosition = sticky + final double childScrollExtent = noRenderChilds + ? constraints.overlap + : children + .map((c) => c?.geometry?.scrollExtent ?? 0.0) + .reduce((d1, d2) => d1 + d2); + double headerPosition = sticky ? math.min( constraints.overlap, childScrollExtent - @@ -260,6 +344,7 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { controller?.stickyHeaderScrollOffset = constraints.precedingScrollExtent; } + // second layout if scroll percentage changed and header is a // RenderStickyHeaderLayoutBuilder. if (header is RenderConstrainedLayoutBuilder< @@ -272,7 +357,7 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { _oldState = state; header!.layout( BoxValueConstraints( - value: _oldState!, + value: _oldState ?? SliverStickyHeaderState(0.0, false), constraints: constraints.asBoxConstraints(), ), parentUsesSize: true, @@ -305,23 +390,23 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { assert(geometry!.hitTestExtent > 0.0); if (header != null && mainAxisPosition - constraints.overlap <= _headerExtent!) { - return hitTestBoxChild( - BoxHitTestResult.wrap(SliverHitTestResult.wrap(result)), - header!, - mainAxisPosition: mainAxisPosition - constraints.overlap, - crossAxisPosition: crossAxisPosition, - ) || - (_overlapsContent && - child != null && - child!.geometry!.hitTestExtent > 0.0 && - child!.hitTest(result, - mainAxisPosition: - mainAxisPosition - childMainAxisPosition(child), - crossAxisPosition: crossAxisPosition)); - } else if (child != null && child!.geometry!.hitTestExtent > 0.0) { - return child!.hitTest(result, - mainAxisPosition: mainAxisPosition - childMainAxisPosition(child), - crossAxisPosition: crossAxisPosition); + var hitTestHeader = hitTestBoxChild( + BoxHitTestResult.wrap(SliverHitTestResult.wrap(result)), + header!, + mainAxisPosition: mainAxisPosition - constraints.overlap, + crossAxisPosition: crossAxisPosition, + ); + + if (hitTestHeader) return true; + if (!(_overlapsContent && !noRenderChilds)) return false; + } + + if (children.any((c) => (c?.geometry?.hitTestExtent ?? 0.0) > 0.0)) { + return children.any((c) => + c?.hitTest(result, + mainAxisPosition: mainAxisPosition - childMainAxisPosition(c), + crossAxisPosition: crossAxisPosition) ?? + false); } return false; } @@ -332,7 +417,7 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { return _isPinned ? 0.0 : -(constraints.scrollOffset + constraints.overlap); - if (child == this.child) + if (children.contains(child)) return calculatePaintOffset(constraints, from: 0.0, to: headerLogicalExtent!); return 0; @@ -341,7 +426,7 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { @override double? childScrollOffset(RenderObject child) { assert(child.parent == this); - if (child == this.child) { + if (this.children.contains(child)) { return _headerExtent; } else { return super.childScrollOffset(child); @@ -358,10 +443,14 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { @override void paint(PaintingContext context, Offset offset) { if (geometry!.visible) { - if (child != null && child!.geometry!.visible) { - final SliverPhysicalParentData childParentData = - child!.parentData as SliverPhysicalParentData; - context.paintChild(child!, offset + childParentData.paintOffset); + Offset prev = offset; + for (var child in children) { + if (child != null && child.geometry!.visible) { + final SliverPhysicalParentData childParentData = + child.parentData as SliverPhysicalParentData; + + context.paintChild(child, prev + childParentData.paintOffset); + } } // The header must be drawn over the sliver. diff --git a/lib/src/widgets/sliver_sticky_header.dart b/lib/src/widgets/sliver_sticky_header.dart index 48d0657..6c799eb 100644 --- a/lib/src/widgets/sliver_sticky_header.dart +++ b/lib/src/widgets/sliver_sticky_header.dart @@ -151,11 +151,14 @@ class SliverStickyHeader extends RenderObjectWidget { SliverStickyHeader({ Key? key, this.header, - this.sliver, + List? slivers, + Widget? sliver, this.overlapsContent: false, this.sticky = true, this.controller, - }) : super(key: key); + }) : assert((sliver == null) != (slivers == null)), + this.slivers = slivers ?? [sliver], + super(key: key); /// Creates a widget that builds the header of a [SliverStickyHeader] /// each time its scroll percentage changes. @@ -167,6 +170,7 @@ class SliverStickyHeader extends RenderObjectWidget { SliverStickyHeader.builder({ Key? key, required SliverStickyHeaderWidgetBuilder builder, + List? slivers, Widget? sliver, bool overlapsContent: false, bool sticky = true, @@ -177,6 +181,7 @@ class SliverStickyHeader extends RenderObjectWidget { builder: (context, constraints) => builder(context, constraints.value), ), + slivers: slivers, sliver: sliver, overlapsContent: overlapsContent, sticky: sticky, @@ -187,7 +192,7 @@ class SliverStickyHeader extends RenderObjectWidget { final Widget? header; /// The sliver to display after the header. - final Widget? sliver; + final List slivers; /// Whether the header should be drawn on top of the sliver /// instead of before. @@ -209,6 +214,7 @@ class SliverStickyHeader extends RenderObjectWidget { overlapsContent: overlapsContent, sticky: sticky, controller: controller ?? DefaultStickyHeaderController.of(context), + children: List.filled(slivers.length, null), ); } @@ -292,33 +298,41 @@ class SliverStickyHeaderBuilder extends StatelessWidget { class SliverStickyHeaderRenderObjectElement extends RenderObjectElement { /// Creates an element that uses the given widget as its configuration. SliverStickyHeaderRenderObjectElement(SliverStickyHeader widget) - : super(widget); + : _slivers = List.filled(widget.slivers.length, null), + super(widget); @override SliverStickyHeader get widget => super.widget as SliverStickyHeader; Element? _header; - Element? _sliver; + List _slivers; @override void visitChildren(ElementVisitor visitor) { if (_header != null) visitor(_header!); - if (_sliver != null) visitor(_sliver!); + + _slivers.forEach((s) { + if (s != null) visitor(s); + }); } @override void forgetChild(Element child) { super.forgetChild(child); if (child == _header) _header = null; - if (child == _sliver) _sliver = null; + if (_slivers.contains(child)) _slivers.remove(child); } @override void mount(Element? parent, dynamic newSlot) { super.mount(parent, newSlot); _header = updateChild(_header, widget.header, 0); - _sliver = updateChild(_sliver, widget.sliver, 1); + + var i = 0; + for (var sliver in _slivers) { + _slivers[i] = updateChild(sliver, widget.slivers[i], ++i); + } } @override @@ -326,7 +340,11 @@ class SliverStickyHeaderRenderObjectElement extends RenderObjectElement { super.update(newWidget); assert(widget == newWidget); _header = updateChild(_header, widget.header, 0); - _sliver = updateChild(_sliver, widget.sliver, 1); + + var i = 0; + for (var sliver in _slivers) { + _slivers[i] = updateChild(sliver, widget.slivers[i], ++i); + } } @override @@ -334,7 +352,9 @@ class SliverStickyHeaderRenderObjectElement extends RenderObjectElement { final RenderSliverStickyHeader renderObject = this.renderObject as RenderSliverStickyHeader; if (slot == 0) renderObject.header = child as RenderBox?; - if (slot == 1) renderObject.child = child as RenderSliver?; + if (slot != null && slot > 0 && slot <= _slivers.length) { + renderObject.addChild(slot - 1, child as RenderSliver); + } assert(renderObject == this.renderObject); } @@ -344,11 +364,13 @@ class SliverStickyHeaderRenderObjectElement extends RenderObjectElement { } @override - void removeRenderObjectChild(RenderObject child, slot) { + void removeRenderObjectChild(RenderObject child, int slot) { final RenderSliverStickyHeader renderObject = this.renderObject as RenderSliverStickyHeader; if (renderObject.header == child) renderObject.header = null; - if (renderObject.child == child) renderObject.child = null; + if (renderObject.children.contains(child)) { + renderObject.removeChild(slot - 1); + } assert(renderObject == this.renderObject); } }