Skip to content

Commit

Permalink
feat: foundation for sticky headers (#19)
Browse files Browse the repository at this point in the history
This PR introduces 
- extension on `SliverGeometry` that allows specifying child obstruction
extent
- extension on `RenderAbstractViewport` with `getOffsetToRevealExt`
method that provides offset to reveal that takes the obstruction extent
into account.

These should be enough to allow for implementing of sticky header sliver
that works properly with the jumpTo/animateToItem calls.
  • Loading branch information
knopp authored Feb 19, 2024
1 parent 72731b7 commit 7d26f24
Show file tree
Hide file tree
Showing 8 changed files with 381 additions and 90 deletions.
16 changes: 16 additions & 0 deletions example/lib/examples/item_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ const _kMaxSlivers = 10;
const _kItemsPerSliver = [1, 9, 27, 80, 200, 1000, 2500, 7000, 20000];

class _ReadingOrderTraversalPolicy extends ReadingOrderTraversalPolicy {
_ReadingOrderTraversalPolicy() : super(requestFocusCallback: _requestFocus);

static void _requestFocus(
FocusNode node, {
ScrollPositionAlignmentPolicy? alignmentPolicy,
double? alignment,
Duration? duration,
Curve? curve,
}) {
if (!node.hasFocus) {
node.requestFocus();
}
final renderObject = node.context!.findRenderObject();
renderObject?.safeShowOnScreen(node.context!);
}

@override
bool inDirection(FocusNode currentNode, TraversalDirection direction) {
// For list traversal the history flutter keeps have weird behavior.
Expand Down
2 changes: 1 addition & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ void main() {
});
hierarchicalLoggingEnabled = true;
WidgetsFlutterBinding.ensureInitialized();
// Logger("SuperSliver").level = Level.FINER;
// Logger("SuperSliverList").level = Level.FINER;

// Right now the debug bar doesn't work nicely with safe area so
// only enable it on desktop platform.
Expand Down
17 changes: 9 additions & 8 deletions example/lib/util/show_on_screen.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "package:flutter/rendering.dart";
import "package:flutter/widgets.dart";
import "package:super_sliver_list/super_sliver_list.dart";

extension RenderObjectShowOnScreen on RenderObject {
/// showOnScreen alternative that doesn't scroll if the widget is already
Expand All @@ -8,8 +9,6 @@ extension RenderObjectShowOnScreen on RenderObject {
BuildContext context, {
/// Extra spacing for around the widget in percent of the viewport (0.05 being 5%)
double extraSpacingPercent = 0.0,
Duration? duration,
Curve? curve,
bool allowScrollingDown = true,
bool allowScrollingUp = true,
}) {
Expand All @@ -22,16 +21,18 @@ extension RenderObjectShowOnScreen on RenderObject {
if (scrollable == null) return;

final minOffset =
viewport.getOffsetToReveal(this, extraSpacingPercent).offset;
final maxOffset =
viewport.getOffsetToReveal(this, 1.0 - extraSpacingPercent).offset;
viewport.getOffsetToRevealExt(this, extraSpacingPercent).offset;

final position = scrollable.position;

if (position.pixels > minOffset && allowScrollingDown) {
scrollable.position.moveTo(minOffset, duration: duration, curve: curve);
} else if (position.pixels < maxOffset && allowScrollingUp) {
scrollable.position.moveTo(maxOffset, duration: duration, curve: curve);
scrollable.position.moveTo(minOffset);
} else {
final maxOffset =
viewport.getOffsetToRevealExt(this, 1.0 - extraSpacingPercent).offset;
if (position.pixels < maxOffset && allowScrollingUp) {
scrollable.position.moveTo(maxOffset);
}
}
}
}
84 changes: 56 additions & 28 deletions lib/src/render_object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "element.dart";
import "extent_manager.dart";
import "layout_budget.dart";
import "layout_pass.dart";
import "sliver_extensions.dart";
import "super_sliver_list.dart";

final _log = Logger("SuperSliverList");
Expand All @@ -21,6 +22,9 @@ class _ChildScrollOffsetEstimation {
required this.offset,
required this.extent,
required this.precedingScrollExtent,
required this.revealingRect,
required this.alignment,
required this.childObstructionExtent,
});

/// Index of child for which the offset was estimated.
Expand All @@ -40,14 +44,17 @@ class _ChildScrollOffsetEstimation {
/// for change in preceding sliver extent change during layout.
final double precedingScrollExtent;

/// Scroll offset of viewport when estimation was made.
double? viewportScrollOffset;

/// Whether the entire element or only a rect should be revealed.
bool revealingRect = false;
final bool revealingRect;

/// Child alignment within viewport.
double? alignment;
final double? alignment;

/// Child obstruction extent when estimation was made.
final ChildObstructionExtent childObstructionExtent;

/// Scroll offset of viewport when estimation was made.
double? viewportScrollOffset;
}

class RenderSuperSliverList extends RenderSliverMultiBoxAdaptor
Expand Down Expand Up @@ -200,7 +207,14 @@ class RenderSuperSliverList extends RenderSliverMultiBoxAdaptor
// we can possibly correct scrollOffset in next performLayout call.
final index = indexOf(child as RenderBox);

if (child is _FakeRenderObject && child.needEstimationOnly) {
final offsetToRevealContext = OffsetToRevealContext.current();

assert(
offsetToRevealContext == null ||
offsetToRevealContext.viewport == getViewport(),
);

if (offsetToRevealContext?.estimationOnly == true) {
return _extentManager.offsetForIndex(index);
}

Expand All @@ -209,7 +223,28 @@ class RenderSuperSliverList extends RenderSliverMultiBoxAdaptor
offset: _extentManager.offsetForIndex(index),
extent: _extentManager.getExtent(index),
precedingScrollExtent: precedingScrollExtent,
revealingRect: offsetToRevealContext?.rect != null,
alignment: offsetToRevealContext?.alignment,
childObstructionExtent: child.getParentChildObstructionExtent(),
);

if (offsetToRevealContext != null) {
assert(offsetToRevealContext.estimationOnly == false);
offsetToRevealContext.registerOffsetResolvedCallback((value) {
final offset = value.offset;
// Remember the target scroll position. Scroll correction will only be enforced
// when the viewport scroll position is the same as when the estimation was made.
// This is enforced by [LayoutPass.sanitizeChildScrollOffsetEstimation].
final position = getViewport()!.offset as ScrollPosition;
// Only remember position if it is within scroll extent. Otherwise
// it will be corrected and it is not possible to check against it.
if (offset >= position.minScrollExtent &&
offset <= position.maxScrollExtent) {
_childScrollOffsetEstimation?.viewportScrollOffset = offset;
}
});
}

_log.fine(
"$_logIdentifier remembering estimated offset ${_childScrollOffsetEstimation!.offset} for child $index (preceding extent ${constraints.precedingScrollExtent})",
);
Expand All @@ -223,9 +258,7 @@ class RenderSuperSliverList extends RenderSliverMultiBoxAdaptor
break;
}
}
final offset = _childScrollOffsetEstimation!.offset;

return offset;
return _childScrollOffsetEstimation!.offset;
}

/// Moves the layout offset of this and subsequent children by the given delta.
Expand Down Expand Up @@ -758,6 +791,16 @@ class RenderSuperSliverList extends RenderSliverMultiBoxAdaptor
// the correction is equal to extent difference.
final extentDifference = paintExtentOf(c) - estimation.extent;
correction += extentDifference * childAlignmentWithinViewport;

// Obstruction extent on this sliver might have been outdated during the estimation
// so account of the difference.
final obstructionExtentDifference =
c.getParentChildObstructionExtent() -
estimation.childObstructionExtent;
correction -= obstructionExtentDifference.leading *
(1.0 - childAlignmentWithinViewport);
correction += obstructionExtentDifference.trailing *
childAlignmentWithinViewport;
}

if (correction.abs() > precisionErrorTolerance) {
Expand Down Expand Up @@ -904,29 +947,16 @@ class RenderSuperSliverList extends RenderSliverMultiBoxAdaptor
parent: this,
index: index,
extent: _extentManager.getExtent(index),
needEstimationOnly: estimationOnly,
);
final offset = getViewport()
?.getOffsetToReveal(
final viewport = getViewport()!;
return viewport
.getOffsetToRevealExt(
renderObject,
alignment,
esimationOnly: estimationOnly,
rect: rect,
)
.offset;

if (offset != null && !estimationOnly) {
final position = getViewport()!.offset as ScrollPosition;
// Only remember position if it is within scroll extent. Otherwise
// it will be corrected and it is not possible to check against it.
if (offset >= position.minScrollExtent &&
offset <= position.maxScrollExtent) {
_childScrollOffsetEstimation?.viewportScrollOffset = offset;
}
_childScrollOffsetEstimation?.revealingRect = rect != null;
_childScrollOffsetEstimation?.alignment = alignment;
}

return offset ?? 0.0;
}

@override
Expand All @@ -941,13 +971,11 @@ class _FakeRenderObject extends RenderBox {
@override
final RenderObject parent;
final double extent;
final bool needEstimationOnly;

_FakeRenderObject({
required this.parent,
required int index,
required this.extent,
required this.needEstimationOnly,
}) {
parentData = SliverMultiBoxAdaptorParentData()..index = index;
}
Expand Down
Loading

0 comments on commit 7d26f24

Please sign in to comment.