diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d9f016..1754786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 +* [feature] - added `toggleExpanded` in the Sticker Header component +* [feature] - added `isExpanded`,`isPinned`,`isDisabled` in the Sticker Header component to set and get these properties + ## 2.0.7 * [chore] - regenerate example with Flutter 3.24 for Android Studio Ladybug compatible diff --git a/README.md b/README.md index be86b43..55f7467 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ A Sliver implementation of sticky collapsable panel, with a box header rebuild o - Support iOS style sticky header, just like iOS's system contact app (with `iOSStyleSticky = true` parameter). - Support add padding for sliver child (with `paddingBeforeCollapse` parameter). - Support add padding after the header even the panel collapsed (with `paddingAfterCollapse` parameter). +- Support setting/getting the collapsable panel expansion, pinned, and disabled status using `isExpanded`, `isPinned`, and `isDisabled` properties of the controller. --- ## Getting started @@ -30,7 +31,7 @@ A Sliver implementation of sticky collapsable panel, with a box header rebuild o ```yaml dependencies: - sliver_sticky_collapsable_panel: ^2.0.7 + sliver_sticky_collapsable_panel: ^2.1.0 ``` - In your library add the following import: @@ -89,6 +90,13 @@ A Sliver implementation of sticky collapsable panel, with a box header rebuild o --- ## More Advanced Feature: +- You can use the controller to set/get the status of the panel through `isExpanded`,`isPinned`,`isDisabled`. + ```dart + final StickyCollapsablePanelController panelController = StickyCollapsablePanelController(key:'key_1'); + ... + panelController.isExpanded = true; // or panelController.toggleExpanded(); + ``` + - You can disable collapse for any sliver you wanted, just add `disableCollapsable = true`. ```dart CustomScrollView( diff --git a/lib/rendering/render_sliver_sticky_collapsable_panel.dart b/lib/rendering/render_sliver_sticky_collapsable_panel.dart index 631a601..e0dec4e 100644 --- a/lib/rendering/render_sliver_sticky_collapsable_panel.dart +++ b/lib/rendering/render_sliver_sticky_collapsable_panel.dart @@ -2,6 +2,7 @@ import 'dart:math' as math; import 'package:flutter/physics.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import '../utils/slot.dart'; @@ -12,7 +13,9 @@ import '../utils/utils.dart'; /// The [headerChild] stays pinned when it hits the start of the viewport until /// the [panelChild] scrolls off the viewport. class RenderSliverStickyCollapsablePanel extends RenderSliver - with SlottedContainerRenderObjectMixin, RenderSliverHelpers { + with + SlottedContainerRenderObjectMixin, + RenderSliverHelpers { RenderSliverStickyCollapsablePanel({ required bool overlapsContent, required bool sticky, @@ -36,7 +39,10 @@ class RenderSliverStickyCollapsablePanel extends RenderSliver late bool _isPinned; void updateIsPinned() { - _isPinned = _sticky && geometry!.visible && constraints.scrollOffset > 0 && constraints.overlap == 0; + _isPinned = _sticky && + geometry!.visible && + constraints.scrollOffset > 0 && + constraints.overlap == 0; } bool _overlapsContent; @@ -60,6 +66,7 @@ class RenderSliverStickyCollapsablePanel extends RenderSliver set controller(StickyCollapsablePanelController value) { if (_controller == value) return; value.precedingScrollExtent = _controller.precedingScrollExtent; + value.isExpanded = _controller.isExpanded; _controller = value; } @@ -125,37 +132,45 @@ class RenderSliverStickyCollapsablePanel extends RenderSliver @override void performLayout() { final SliverConstraints constraints = this.constraints; - final axisDirection = applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection); + final axisDirection = applyGrowthDirectionToAxisDirection( + constraints.axisDirection, constraints.growthDirection); //layout header first(but not compute paint offset), so we can compute constraints of sliver child if (_headerSize == null) { headerChild.layout( BoxValueConstraints( - value: _oldStatus ?? SliverStickyCollapsablePanelStatus(0, false, _isExpanded), + value: _oldStatus ?? + SliverStickyCollapsablePanelStatus(0, false, _isExpanded), constraints: constraints.asBoxConstraints(), ), parentUsesSize: true, ); } _headerExtent = computeHeaderExtent(); - final double headerAndOverlapPaintExtent = - calculatePaintOffset(constraints, from: 0, to: childScrollOffset(panelChild)); - final double headerAndOverlapCacheExtent = - calculateCacheOffset(constraints, from: 0, to: childScrollOffset(panelChild)); + final double headerAndOverlapPaintExtent = calculatePaintOffset(constraints, + from: 0, to: childScrollOffset(panelChild)); + final double headerAndOverlapCacheExtent = calculateCacheOffset(constraints, + from: 0, to: childScrollOffset(panelChild)); //layout sliver child, and compute paint offset panelChild.layout( constraints.copyWith( - scrollOffset: math.max(0, constraints.scrollOffset - childScrollOffset(panelChild)), - cacheOrigin: math.min(0, constraints.cacheOrigin + childScrollOffset(panelChild)), + scrollOffset: math.max( + 0, constraints.scrollOffset - childScrollOffset(panelChild)), + cacheOrigin: math.min( + 0, constraints.cacheOrigin + childScrollOffset(panelChild)), overlap: 0, - remainingPaintExtent: math.max(0, constraints.remainingPaintExtent - headerAndOverlapPaintExtent), - remainingCacheExtent: math.max(0, constraints.remainingCacheExtent - headerAndOverlapCacheExtent), - precedingScrollExtent: math.max(0, constraints.precedingScrollExtent + childScrollOffset(panelChild)), + remainingPaintExtent: math.max( + 0, constraints.remainingPaintExtent - headerAndOverlapPaintExtent), + remainingCacheExtent: math.max( + 0, constraints.remainingCacheExtent - headerAndOverlapCacheExtent), + precedingScrollExtent: math.max(0, + constraints.precedingScrollExtent + childScrollOffset(panelChild)), ), parentUsesSize: true, ); final SliverGeometry panelChildGeometry = panelChild.geometry!; if (panelChildGeometry.scrollOffsetCorrection != null) { - geometry = SliverGeometry(scrollOffsetCorrection: panelChildGeometry.scrollOffsetCorrection); + geometry = SliverGeometry( + scrollOffsetCorrection: panelChildGeometry.scrollOffsetCorrection); return; } final paintExtent = math.min( @@ -164,14 +179,18 @@ class RenderSliverStickyCollapsablePanel extends RenderSliver ); geometry = SliverGeometry( paintOrigin: panelChildGeometry.paintOrigin, - scrollExtent: childScrollOffset(panelChild) + panelChildGeometry.scrollExtent, + scrollExtent: + childScrollOffset(panelChild) + panelChildGeometry.scrollExtent, paintExtent: paintExtent, - layoutExtent: math.min(headerAndOverlapPaintExtent + panelChildGeometry.layoutExtent, paintExtent), + layoutExtent: math.min( + headerAndOverlapPaintExtent + panelChildGeometry.layoutExtent, + paintExtent), cacheExtent: math.min( headerAndOverlapCacheExtent + panelChildGeometry.cacheExtent, constraints.remainingCacheExtent, ), - maxPaintExtent: childScrollOffset(panelChild) + panelChildGeometry.maxPaintExtent, + maxPaintExtent: + childScrollOffset(panelChild) + panelChildGeometry.maxPaintExtent, hitTestExtent: math.max( headerAndOverlapPaintExtent + panelChildGeometry.paintExtent, headerAndOverlapPaintExtent + panelChildGeometry.hitTestExtent, @@ -181,22 +200,36 @@ class RenderSliverStickyCollapsablePanel extends RenderSliver final panelParentData = panelChild.parentData as SliverPhysicalParentData; panelParentData.paintOffset = switch (axisDirection) { AxisDirection.up || AxisDirection.left => Offset.zero, - AxisDirection.right => Offset(calculatePaintOffset(constraints, from: 0, to: childScrollOffset(panelChild)), 0), - AxisDirection.down => Offset(0, calculatePaintOffset(constraints, from: 0, to: childScrollOffset(panelChild))), + AxisDirection.right => Offset( + calculatePaintOffset(constraints, + from: 0, to: childScrollOffset(panelChild)), + 0), + AxisDirection.down => Offset( + 0, + calculatePaintOffset(constraints, + from: 0, to: childScrollOffset(panelChild))), }; //update constraints of header if needed, update header paint Offset updateIsPinned(); final headerPosition = childMainAxisPosition(headerChild); - double headerScrollRatio = (((headerPosition - constraints.overlap).abs() / _headerExtent)).clamp(0, 1); + double headerScrollRatio = + (((headerPosition - constraints.overlap).abs() / _headerExtent)) + .clamp(0, 1); if (nearZero(headerScrollRatio, _tolerance)) { headerScrollRatio = 0; } else if (nearEqual(1, headerScrollRatio, _tolerance)) { headerScrollRatio = 1; } - if (_controller.precedingScrollExtent != constraints.precedingScrollExtent) { - _controller.precedingScrollExtent = constraints.precedingScrollExtent; - } - final status = SliverStickyCollapsablePanelStatus(headerScrollRatio, _isPinned, _isExpanded); + + SchedulerBinding.instance.addPostFrameCallback((_) { + if (_controller.precedingScrollExtent != + constraints.precedingScrollExtent) { + _controller.precedingScrollExtent = constraints.precedingScrollExtent; + } + }); + + final status = SliverStickyCollapsablePanelStatus( + headerScrollRatio, _isPinned, _isExpanded); if (_oldStatus != status || _headerSize != null) { _oldStatus = status; headerChild.layout( @@ -208,13 +241,17 @@ class RenderSliverStickyCollapsablePanel extends RenderSliver ); } if (_iOSStyleSticky) { - geometry = geometry!.copyWith(hitTestExtent: geometry!.hitTestExtent + childScrollOffset(panelChild)); + geometry = geometry!.copyWith( + hitTestExtent: + geometry!.hitTestExtent + childScrollOffset(panelChild)); } final headerParentData = headerChild.parentData as SliverPhysicalParentData; headerParentData.paintOffset = switch (axisDirection) { - AxisDirection.up => Offset(0, geometry!.paintExtent - headerPosition - _headerExtent), + AxisDirection.up => + Offset(0, geometry!.paintExtent - headerPosition - _headerExtent), AxisDirection.down => Offset(0, headerPosition), - AxisDirection.left => Offset(geometry!.paintExtent - headerPosition - _headerExtent, 0), + AxisDirection.left => + Offset(geometry!.paintExtent - headerPosition - _headerExtent, 0), AxisDirection.right => Offset(headerPosition, 0), }; } @@ -226,16 +263,25 @@ class RenderSliverStickyCollapsablePanel extends RenderSliver required double crossAxisPosition, }) { bool tryHitTestPanelChild() { - if (panelChild.geometry!.hitTestExtent > 0) { + // Ensure panelChild is laid out + if (!panelChild.debugNeedsLayout && + panelChild.geometry != null && + panelChild.geometry!.hitTestExtent > 0) { return panelChild.hitTest( result, - mainAxisPosition: mainAxisPosition - childMainAxisPosition(panelChild), + mainAxisPosition: + mainAxisPosition - childMainAxisPosition(panelChild), crossAxisPosition: crossAxisPosition, ); } return false; } + // Ensure headerChild is laid out + if (headerChild.debugNeedsLayout) { + return false; + } + double headerPosition = childMainAxisPosition(headerChild); if ((mainAxisPosition - headerPosition) <= _headerExtent) { final didHitHeader = hitTestBoxChild( @@ -263,10 +309,15 @@ class RenderSliverStickyCollapsablePanel extends RenderSliver final panelScrollExtent = panelChild.geometry!.scrollExtent; return switch (child) { RenderBox _ => _iOSStyleSticky - ? (_isPinned ? constraints.overlap : -(constraints.scrollOffset - constraints.overlap)) + ? (_isPinned + ? constraints.overlap + : -(constraints.scrollOffset - constraints.overlap)) : (_isPinned - ? math.min(constraints.overlap, - panelScrollExtent - constraints.scrollOffset - (_overlapsContent ? _headerExtent : 0)) + ? math.min( + constraints.overlap, + panelScrollExtent - + constraints.scrollOffset - + (_overlapsContent ? _headerExtent : 0)) : -(constraints.scrollOffset - constraints.overlap)), _ => calculatePaintOffset( constraints, @@ -282,7 +333,9 @@ class RenderSliverStickyCollapsablePanel extends RenderSliver assert(child == headerChild || child == panelChild); return switch (child) { RenderBox _ => constraints.overlap, - _ => _overlapsContent ? constraints.overlap : _headerExtent + constraints.overlap, + _ => _overlapsContent + ? constraints.overlap + : _headerExtent + constraints.overlap, }; } @@ -296,10 +349,12 @@ class RenderSliverStickyCollapsablePanel extends RenderSliver void paint(PaintingContext context, Offset offset) { if (geometry!.visible) { if (panelChild.geometry!.visible) { - final panelParentData = panelChild.parentData as SliverPhysicalParentData; + final panelParentData = + panelChild.parentData as SliverPhysicalParentData; context.paintChild(panelChild, offset + panelParentData.paintOffset); } - final headerParentData = headerChild.parentData as SliverPhysicalParentData; + final headerParentData = + headerChild.parentData as SliverPhysicalParentData; context.paintChild(headerChild, offset + headerParentData.paintOffset); } } diff --git a/lib/utils/sliver_sticky_collapsable_panel_controller.dart b/lib/utils/sliver_sticky_collapsable_panel_controller.dart index 7f6682b..adea2e5 100644 --- a/lib/utils/sliver_sticky_collapsable_panel_controller.dart +++ b/lib/utils/sliver_sticky_collapsable_panel_controller.dart @@ -2,13 +2,22 @@ import 'package:flutter/foundation.dart'; /// Controller to manage Sticker Header class StickyCollapsablePanelController with ChangeNotifier { - StickyCollapsablePanelController({this.key = 'default'}); - final String key; + StickyCollapsablePanelController({this.key = 'default'}); + /// The offset used as calibration when collapse/expand the panel double _precedingScrollExtent = 0; + /// Whether the panel is expanded or collapsed + bool _isExpanded = false; + + /// Whether the panel is disabled (non-interactive) + bool _isDisabled = false; + + /// Whether the panel is pinned to its position + bool _isPinned = false; + double get precedingScrollExtent => _precedingScrollExtent; set precedingScrollExtent(double value) { @@ -17,4 +26,34 @@ class StickyCollapsablePanelController with ChangeNotifier { notifyListeners(); } } + + bool get isExpanded => _isExpanded; + + set isExpanded(bool value) { + if (_isExpanded != value) { + _isExpanded = value; + notifyListeners(); + } + } + + void toggleExpanded() { + isExpanded = !isExpanded; + } + + bool get isDisabled => _isDisabled; + + set isDisabled(bool value) { + if (_isDisabled != value) { + _isDisabled = value; + notifyListeners(); + } + } + + bool get isPinned => _isPinned; + set isPinned(bool value) { + if (_isPinned != value) { + _isPinned = value; + notifyListeners(); + } + } } diff --git a/lib/utils/sliver_sticky_collapsable_panel_status.dart b/lib/utils/sliver_sticky_collapsable_panel_status.dart index bb4cc06..eba04d4 100644 --- a/lib/utils/sliver_sticky_collapsable_panel_status.dart +++ b/lib/utils/sliver_sticky_collapsable_panel_status.dart @@ -19,7 +19,9 @@ class SliverStickyCollapsablePanelStatus { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! SliverStickyCollapsablePanelStatus) return false; - return scrollPercentage == other.scrollPercentage && isPinned == other.isPinned && isExpanded == other.isExpanded; + return scrollPercentage == other.scrollPercentage && + isPinned == other.isPinned && + isExpanded == other.isExpanded; } @override diff --git a/lib/utils/value_layout_builder.dart b/lib/utils/value_layout_builder.dart index ccacdaf..13d078d 100755 --- a/lib/utils/value_layout_builder.dart +++ b/lib/utils/value_layout_builder.dart @@ -49,7 +49,8 @@ class BoxValueConstraints extends BoxConstraints { /// See also: /// /// * [LayoutBuilder]. -class ValueLayoutBuilder extends ConstrainedLayoutBuilder> { +class ValueLayoutBuilder + extends ConstrainedLayoutBuilder> { /// Creates a widget that defers its building until layout. const ValueLayoutBuilder({ super.key, @@ -57,11 +58,14 @@ class ValueLayoutBuilder extends ConstrainedLayoutBuilder createRenderObject(BuildContext context) => RenderValueLayoutBuilder(); + RenderValueLayoutBuilder createRenderObject(BuildContext context) => + RenderValueLayoutBuilder(); } class RenderValueLayoutBuilder extends RenderBox - with RenderObjectWithChildMixin, RenderConstrainedLayoutBuilder, RenderBox> { + with + RenderObjectWithChildMixin, + RenderConstrainedLayoutBuilder, RenderBox> { @override double computeMinIntrinsicWidth(double height) { assert(_debugThrowIfNotCheckingIntrinsics()); @@ -89,7 +93,8 @@ class RenderValueLayoutBuilder extends RenderBox @override Size computeDryLayout(BoxConstraints constraints) { assert(debugCannotComputeDryLayout( - reason: 'Calculating the dry layout would require running the layout callback ' + reason: + 'Calculating the dry layout would require running the layout callback ' 'speculatively, which might mutate the live render object tree.', )); return Size.zero; @@ -128,7 +133,8 @@ class RenderValueLayoutBuilder extends RenderBox bool _debugThrowIfNotCheckingIntrinsics() { assert(() { if (!RenderObject.debugCheckingIntrinsics) { - throw FlutterError('ValueLayoutBuilder does not support returning intrinsic dimensions.\n' + throw FlutterError( + 'ValueLayoutBuilder does not support returning intrinsic dimensions.\n' 'Calculating the intrinsic dimensions would require running the layout ' 'callback speculatively, which might mutate the live render object tree.'); } diff --git a/lib/widgets/sliver_sticky_collapsable_panel.dart b/lib/widgets/sliver_sticky_collapsable_panel.dart index 0509abb..29490c5 100644 --- a/lib/widgets/sliver_sticky_collapsable_panel.dart +++ b/lib/widgets/sliver_sticky_collapsable_panel.dart @@ -112,47 +112,45 @@ class SliverStickyCollapsablePanel extends StatefulWidget { State createState() => SliverStickyCollapsablePanelState(); } -class SliverStickyCollapsablePanelState extends State { - late bool isExpanded; +class SliverStickyCollapsablePanelState + extends State { @override void initState() { super.initState(); - isExpanded = widget.defaultExpanded; + widget.panelController.isExpanded = widget.defaultExpanded; + widget.panelController.isDisabled = widget.disableCollapsable; } @override Widget build(BuildContext context) { - Widget boxHeader = ValueLayoutBuilder( - builder: (context, constraints) => GestureDetector( - onTap: () { - if (!widget.disableCollapsable) { - setState(() { - isExpanded = !isExpanded; - if (constraints.value.isPinned) { - widget.scrollController.jumpTo(widget.panelController.precedingScrollExtent); - } - }); - widget.expandCallback?.call(isExpanded); - } - }, - child: widget.headerBuilder(context, constraints.value), - ), - ); - final isExpandedNow = (widget.disableCollapsable || isExpanded); - return _SliverStickyCollapsablePanel( - boxHeader: boxHeader, - sliverPanel: SliverPadding( - padding: isExpandedNow ? widget.paddingBeforeCollapse : widget.paddingAfterCollapse, - sliver: isExpandedNow ? widget.sliverPanel : null, - ), - overlapsContent: widget.overlapsContent, - sticky: widget.sticky, - controller: widget.panelController, - isExpanded: isExpandedNow, - iOSStyleSticky: widget.iOSStyleSticky, - headerSize: widget.headerSize, - ); + return ListenableBuilder( + listenable: widget.panelController, + builder: (context, _) { + Widget boxHeader = + ValueLayoutBuilder( + builder: (context, constraints) => + widget.headerBuilder(context, constraints.value), + ); + + final isExpandedNow = (widget.panelController.isDisabled || + widget.panelController.isExpanded); + return _SliverStickyCollapsablePanel( + boxHeader: boxHeader, + sliverPanel: SliverPadding( + padding: isExpandedNow + ? widget.paddingBeforeCollapse + : widget.paddingAfterCollapse, + sliver: isExpandedNow ? widget.sliverPanel : null, + ), + overlapsContent: widget.overlapsContent, + sticky: widget.sticky, + controller: widget.panelController, + isExpanded: isExpandedNow, + iOSStyleSticky: widget.iOSStyleSticky, + headerSize: widget.headerSize, + ); + }); } } @@ -160,7 +158,8 @@ class SliverStickyCollapsablePanelState extends State { +class _SliverStickyCollapsablePanel + extends SlottedMultiChildRenderObjectWidget { /// Creates a sliver that displays the [boxHeader] before its [sliverPanel], unless /// [overlapsContent] it's true. /// The [boxHeader] stays pinned when it hits the start of the viewport until @@ -236,7 +235,8 @@ class _SliverStickyCollapsablePanel extends SlottedMultiChildRenderObjectWidget< } @override - void updateRenderObject(BuildContext context, RenderSliverStickyCollapsablePanel renderObject) { + void updateRenderObject( + BuildContext context, RenderSliverStickyCollapsablePanel renderObject) { renderObject ..overlapsContent = overlapsContent ..sticky = sticky