Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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(
Expand Down
125 changes: 90 additions & 35 deletions lib/rendering/render_sliver_sticky_collapsable_panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<Slot, RenderObject>, RenderSliverHelpers {
with
SlottedContainerRenderObjectMixin<Slot, RenderObject>,
RenderSliverHelpers {
RenderSliverStickyCollapsablePanel({
required bool overlapsContent,
required bool sticky,
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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<SliverStickyCollapsablePanelStatus>(
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(
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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),
};
}
Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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,
};
}

Expand All @@ -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);
}
}
Expand Down
43 changes: 41 additions & 2 deletions lib/utils/sliver_sticky_collapsable_panel_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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();
}
}
}
4 changes: 3 additions & 1 deletion lib/utils/sliver_sticky_collapsable_panel_status.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading