Skip to content

Commit bfbadaf

Browse files
fix: sks favs headers text overflow
1 parent afbb5e8 commit bfbadaf

4 files changed

Lines changed: 325 additions & 50 deletions

File tree

lib/features/sks/sks_favourite_dishes/presentation/sks_favourite_dishes_view.dart

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import "../utils/toast_on_dish_tap.dart";
2222
import "sks_favourite_dishes_controller.dart";
2323
import "widgets/empty_subscribed_dishes_placeholder.dart";
2424
import "widgets/sks_favourite_dishes_loading.dart";
25-
import "widgets/sks_section_header_delegate.dart";
25+
import "widgets/sliver_sticky_header.dart";
2626

2727
@RoutePage()
2828
class SksFavouriteDishesView extends ConsumerWidget {
@@ -76,12 +76,11 @@ class _SksFavouriteDishesView extends ConsumerWidget {
7676
MultiSliver(
7777
pushPinnedChildren: true,
7878
children: [
79-
SliverPersistentHeader(
80-
pinned: true,
81-
delegate: SksSectionHeaderDelegate(
82-
title: context.localize.sks_favourite_dishes_subscribed,
83-
textStyle: context.textTheme.headlineMedium!,
84-
backgroundColor: context.colorScheme.surface,
79+
SliverStickyHeader(
80+
child: Container(
81+
color: context.colorScheme.surface,
82+
padding: const EdgeInsets.all(SksMenuConfig.paddingLarge),
83+
child: Text(context.localize.sks_favourite_dishes_subscribed, style: context.textTheme.headlineMedium),
8584
),
8685
),
8786
if (subscribedDishes.isEmpty)
@@ -118,12 +117,11 @@ class _SksFavouriteDishesView extends ConsumerWidget {
118117
MultiSliver(
119118
pushPinnedChildren: true,
120119
children: [
121-
SliverPersistentHeader(
122-
pinned: true,
123-
delegate: SksSectionHeaderDelegate(
124-
title: context.localize.sks_favourite_dishes_remaining,
125-
textStyle: context.textTheme.headlineMedium!,
126-
backgroundColor: context.colorScheme.surface,
120+
SliverStickyHeader(
121+
child: Container(
122+
color: context.colorScheme.surface,
123+
padding: const EdgeInsets.all(SksMenuConfig.paddingLarge),
124+
child: Text(context.localize.sks_favourite_dishes_remaining, style: context.textTheme.headlineMedium),
127125
),
128126
),
129127
if (unsubscribedDishes.isEmpty)

lib/features/sks/sks_favourite_dishes/presentation/widgets/sks_section_header_delegate.dart

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
import "dart:math" as math;
2+
3+
import "package:flutter/foundation.dart";
4+
import "package:flutter/material.dart";
5+
import "package:flutter/rendering.dart";
6+
7+
// Based on the Flutter issue workaround discussed here:
8+
// https://github.com/flutter/flutter/issues/44557#issuecomment-664416718
9+
//
10+
// Original implementation:
11+
// https://gist.github.com/tomaszpolanski/cf0edb7961d2304c2f293da9971cd4c9
12+
13+
class SliverStickyHeader extends RenderObjectWidget {
14+
final Widget child;
15+
16+
const SliverStickyHeader({required this.child});
17+
18+
@override
19+
RenderSliverStickyHeader createRenderObject(BuildContext context) {
20+
return RenderSliverStickyHeader();
21+
}
22+
23+
@override
24+
SliverStickyHeaderElement createElement() => SliverStickyHeaderElement(this);
25+
}
26+
27+
class SliverStickyHeaderElement extends RenderObjectElement {
28+
SliverStickyHeaderElement(super.widget);
29+
30+
@override
31+
RenderSliverStickyHeader get renderObject => super.renderObject as RenderSliverStickyHeader;
32+
33+
@override
34+
void mount(Element? parent, Object? newSlot) {
35+
super.mount(parent, newSlot);
36+
renderObject._element = this;
37+
}
38+
39+
@override
40+
void unmount() {
41+
renderObject._element = null;
42+
super.unmount();
43+
}
44+
45+
@override
46+
void update(SliverStickyHeader newWidget) {
47+
final oldWidget = widget as SliverStickyHeader;
48+
super.update(newWidget);
49+
final newChild = newWidget.child;
50+
final oldChild = oldWidget.child;
51+
if (newChild != oldChild && (newChild.runtimeType != oldChild.runtimeType)) {
52+
renderObject.triggerRebuild();
53+
}
54+
}
55+
56+
@override
57+
void performRebuild() {
58+
super.performRebuild();
59+
renderObject.triggerRebuild();
60+
}
61+
62+
Element? child;
63+
64+
void _build() {
65+
owner!.buildScope(this, () {
66+
final headerWidget = widget as SliverStickyHeader;
67+
child = updateChild(child, headerWidget.child, null);
68+
});
69+
}
70+
71+
@override
72+
void forgetChild(Element child) {
73+
assert(child == this.child, "forgetChild");
74+
this.child = null;
75+
super.forgetChild(child);
76+
}
77+
78+
@override
79+
void insertRenderObjectChild(covariant RenderBox child, Object? slot) {
80+
assert(renderObject.debugValidateChild(child), "insertRenderObjectChild");
81+
renderObject.child = child;
82+
}
83+
84+
@override
85+
void moveRenderObjectChild(covariant RenderObject child, Object? oldSlot, Object? newSlot) {
86+
assert(false, "moveRenderObjectChild");
87+
}
88+
89+
@override
90+
void removeRenderObjectChild(covariant RenderObject child, Object? slot) {
91+
renderObject.child = null;
92+
}
93+
94+
@override
95+
void visitChildren(ElementVisitor visitor) {
96+
if (child != null) {
97+
visitor(child!);
98+
}
99+
}
100+
}
101+
102+
// --------------------- renderer --------------------------- //
103+
104+
Rect? _trim(
105+
Rect? original, {
106+
double top = -double.infinity,
107+
double right = double.infinity,
108+
double bottom = double.infinity,
109+
double left = -double.infinity,
110+
}) => original?.intersect(Rect.fromLTRB(left, top, right, bottom));
111+
112+
class RenderSliverStickyHeader extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
113+
double? _lastActualScrollOffset;
114+
late double? _effectiveScrollOffset;
115+
116+
double? _childPosition;
117+
118+
SliverStickyHeaderElement? _element;
119+
120+
RenderSliverStickyHeader({RenderBox? child}) {
121+
this.child = child;
122+
}
123+
124+
@protected
125+
double get childExtent {
126+
if (child == null) {
127+
return 0;
128+
}
129+
assert(child!.hasSize, "childExtent");
130+
switch (constraints.axis) {
131+
case Axis.vertical:
132+
return child!.size.height;
133+
case Axis.horizontal:
134+
return child!.size.width;
135+
}
136+
}
137+
138+
var _needsUpdateChild = true;
139+
140+
@override
141+
void markNeedsLayout() {
142+
_needsUpdateChild = true;
143+
super.markNeedsLayout();
144+
}
145+
146+
@protected
147+
void layoutChild(double scrollOffset, double maxExtent, {bool overlapsContent = false}) {
148+
if (_needsUpdateChild) {
149+
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
150+
assert(constraints == this.constraints, "invokeLayoutCallback");
151+
updateChild();
152+
});
153+
_needsUpdateChild = false;
154+
}
155+
child?.layout(constraints.asBoxConstraints(), parentUsesSize: true);
156+
}
157+
158+
@override
159+
bool hitTestChildren(
160+
SliverHitTestResult result, {
161+
required double mainAxisPosition,
162+
required double crossAxisPosition,
163+
}) {
164+
assert(geometry!.hitTestExtent > 0.0, "hitTestChildren");
165+
if (child != null) {
166+
return hitTestBoxChild(
167+
BoxHitTestResult.wrap(result),
168+
child!,
169+
mainAxisPosition: mainAxisPosition,
170+
crossAxisPosition: crossAxisPosition,
171+
);
172+
}
173+
return false;
174+
}
175+
176+
@override
177+
void applyPaintTransform(RenderObject child, Matrix4 transform) {
178+
assert(child == this.child, "applyPaintTransform");
179+
applyPaintTransformForBoxChild(child as RenderBox, transform);
180+
}
181+
182+
@override
183+
void paint(PaintingContext context, Offset offset) {
184+
var paintedOffset = offset;
185+
if (child != null && geometry!.visible) {
186+
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
187+
case AxisDirection.up:
188+
paintedOffset += Offset(0, geometry!.paintExtent - childMainAxisPosition(child!) - childExtent);
189+
case AxisDirection.down:
190+
paintedOffset += Offset(0, childMainAxisPosition(child!));
191+
case AxisDirection.left:
192+
paintedOffset += Offset(geometry!.paintExtent - childMainAxisPosition(child!) - childExtent, 0);
193+
case AxisDirection.right:
194+
paintedOffset += Offset(childMainAxisPosition(child!), 0);
195+
}
196+
context.paintChild(child!, paintedOffset);
197+
}
198+
}
199+
200+
@override
201+
void describeSemanticsConfiguration(SemanticsConfiguration config) {
202+
super.describeSemanticsConfiguration(config);
203+
config.addTagForChildren(RenderViewport.excludeFromScrolling);
204+
}
205+
206+
// pinned floating
207+
208+
@protected
209+
double updateGeometry() {
210+
final double minExtent = childExtent;
211+
final double minAllowedExtent = constraints.remainingPaintExtent > minExtent
212+
? minExtent
213+
: constraints.remainingPaintExtent;
214+
final double maxExtent = childExtent;
215+
final double paintExtent = maxExtent - _effectiveScrollOffset!;
216+
final double clampedPaintExtent = clampDouble(paintExtent, minAllowedExtent, constraints.remainingPaintExtent);
217+
final double layoutExtent = maxExtent - constraints.scrollOffset;
218+
geometry = SliverGeometry(
219+
scrollExtent: maxExtent,
220+
paintOrigin: math.min(constraints.overlap, 0),
221+
paintExtent: clampedPaintExtent,
222+
layoutExtent: clampDouble(layoutExtent, 0, clampedPaintExtent),
223+
maxPaintExtent: maxExtent,
224+
maxScrollObstructionExtent: minExtent,
225+
hasVisualOverflow: true,
226+
);
227+
return 0;
228+
}
229+
230+
@override
231+
void performLayout() {
232+
final SliverConstraints constraints = this.constraints;
233+
final double maxExtent = childExtent;
234+
if (_lastActualScrollOffset != null &&
235+
((constraints.scrollOffset < _lastActualScrollOffset!) || (_effectiveScrollOffset! < maxExtent))) {
236+
double delta = _lastActualScrollOffset! - constraints.scrollOffset;
237+
238+
if (constraints.userScrollDirection == ScrollDirection.forward) {
239+
if (_effectiveScrollOffset! > maxExtent) {
240+
_effectiveScrollOffset = maxExtent;
241+
}
242+
} else {
243+
if (delta > 0.0) {
244+
delta = 0.0;
245+
}
246+
}
247+
_effectiveScrollOffset = clampDouble(_effectiveScrollOffset! - delta, 0, constraints.scrollOffset);
248+
} else {
249+
_effectiveScrollOffset = constraints.scrollOffset;
250+
}
251+
final bool overlapsContent = _effectiveScrollOffset! < constraints.scrollOffset;
252+
253+
layoutChild(_effectiveScrollOffset!, maxExtent, overlapsContent: overlapsContent);
254+
_childPosition = updateGeometry();
255+
_lastActualScrollOffset = constraints.scrollOffset;
256+
}
257+
258+
@override
259+
void showOnScreen({
260+
RenderObject? descendant,
261+
Rect? rect,
262+
Duration duration = Duration.zero,
263+
Curve curve = Curves.ease,
264+
}) {
265+
assert(child != null || descendant == null, "showOnScreen");
266+
267+
final Rect? childBounds = descendant != null
268+
? MatrixUtils.transformRect(descendant.getTransformTo(child), rect ?? descendant.paintBounds)
269+
: rect;
270+
Rect? targetRect;
271+
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
272+
case AxisDirection.up:
273+
targetRect = _trim(childBounds, bottom: childExtent);
274+
case AxisDirection.right:
275+
targetRect = _trim(childBounds, left: 0);
276+
case AxisDirection.down:
277+
targetRect = _trim(childBounds, top: 0);
278+
case AxisDirection.left:
279+
targetRect = _trim(childBounds, right: childExtent);
280+
}
281+
282+
super.showOnScreen(
283+
descendant: descendant == null ? this : child,
284+
rect: targetRect,
285+
duration: duration,
286+
curve: curve,
287+
);
288+
}
289+
290+
@override
291+
double childMainAxisPosition(RenderBox child) {
292+
assert(child == this.child, "childMainAxisPosition");
293+
return _childPosition ?? 0.0;
294+
}
295+
296+
void updateChild() {
297+
assert(_element != null, "updateChild");
298+
_element!._build();
299+
}
300+
301+
@protected
302+
void triggerRebuild() {
303+
markNeedsLayout();
304+
}
305+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import "package:flutter/widgets.dart";
2+
import "package:topwr/features/sks/sks_favourite_dishes/presentation/widgets/sliver_sticky_header.dart";
3+
import "package:widgetbook/widgetbook.dart";
4+
5+
part "sliver_sticky_header.stories.stories.g.dart";
6+
7+
const meta = Meta<SliverStickyHeader>();
8+
9+
final $default = SliverStickyHeaderStory();

0 commit comments

Comments
 (0)