Skip to content

Commit 40ae9f8

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

2 files changed

Lines changed: 335 additions & 13 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
@@ -21,8 +21,8 @@ import "../data/repository/sks_favourite_dishes_repository.dart";
2121
import "../utils/toast_on_dish_tap.dart";
2222
import "sks_favourite_dishes_controller.dart";
2323
import "widgets/empty_subscribed_dishes_placeholder.dart";
24+
import "widgets/siliver_sticky_header.dart";
2425
import "widgets/sks_favourite_dishes_loading.dart";
25-
import "widgets/sks_section_header_delegate.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)
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
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+
ScrollDirection? _lastStartedScrollDirection;
117+
118+
double? _childPosition;
119+
120+
SliverStickyHeaderElement? _element;
121+
122+
RenderSliverStickyHeader({RenderBox? child}) {
123+
this.child = child;
124+
}
125+
126+
@protected
127+
double get childExtent {
128+
if (child == null) {
129+
return 0;
130+
}
131+
assert(child!.hasSize, "childExtent");
132+
switch (constraints.axis) {
133+
case Axis.vertical:
134+
return child!.size.height;
135+
case Axis.horizontal:
136+
return child!.size.width;
137+
}
138+
}
139+
140+
var _needsUpdateChild = true;
141+
142+
@override
143+
void markNeedsLayout() {
144+
_needsUpdateChild = true;
145+
super.markNeedsLayout();
146+
}
147+
148+
@protected
149+
void layoutChild(double scrollOffset, double maxExtent, {bool overlapsContent = false}) {
150+
if (_needsUpdateChild) {
151+
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
152+
assert(constraints == this.constraints, "invokeLayoutCallback");
153+
updateChild();
154+
});
155+
_needsUpdateChild = false;
156+
}
157+
child?.layout(constraints.asBoxConstraints(), parentUsesSize: true);
158+
}
159+
160+
@override
161+
bool hitTestChildren(
162+
SliverHitTestResult result, {
163+
required double mainAxisPosition,
164+
required double crossAxisPosition,
165+
}) {
166+
assert(geometry!.hitTestExtent > 0.0, "hitTestChildren");
167+
if (child != null) {
168+
return hitTestBoxChild(
169+
BoxHitTestResult.wrap(result),
170+
child!,
171+
mainAxisPosition: mainAxisPosition,
172+
crossAxisPosition: crossAxisPosition,
173+
);
174+
}
175+
return false;
176+
}
177+
178+
@override
179+
void applyPaintTransform(RenderObject child, Matrix4 transform) {
180+
assert(child == this.child, "applyPaintTransform");
181+
applyPaintTransformForBoxChild(child as RenderBox, transform);
182+
}
183+
184+
@override
185+
void paint(PaintingContext context, Offset offset) {
186+
var paintedOffset = offset;
187+
if (child != null && geometry!.visible) {
188+
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
189+
case AxisDirection.up:
190+
paintedOffset += Offset(0, geometry!.paintExtent - childMainAxisPosition(child!) - childExtent);
191+
case AxisDirection.down:
192+
paintedOffset += Offset(0, childMainAxisPosition(child!));
193+
case AxisDirection.left:
194+
paintedOffset += Offset(geometry!.paintExtent - childMainAxisPosition(child!) - childExtent, 0);
195+
case AxisDirection.right:
196+
paintedOffset += Offset(childMainAxisPosition(child!), 0);
197+
}
198+
context.paintChild(child!, paintedOffset);
199+
}
200+
}
201+
202+
@override
203+
void describeSemanticsConfiguration(SemanticsConfiguration config) {
204+
super.describeSemanticsConfiguration(config);
205+
config.addTagForChildren(RenderViewport.excludeFromScrolling);
206+
}
207+
208+
// pinned floating
209+
210+
@protected
211+
double updateGeometry() {
212+
final double minExtent = childExtent;
213+
final double minAllowedExtent = constraints.remainingPaintExtent > minExtent
214+
? minExtent
215+
: constraints.remainingPaintExtent;
216+
final double maxExtent = childExtent;
217+
final double paintExtent = maxExtent - _effectiveScrollOffset!;
218+
final double clampedPaintExtent = clampDouble(paintExtent, minAllowedExtent, constraints.remainingPaintExtent);
219+
final double layoutExtent = maxExtent - constraints.scrollOffset;
220+
geometry = SliverGeometry(
221+
scrollExtent: maxExtent,
222+
paintOrigin: math.min(constraints.overlap, 0),
223+
paintExtent: clampedPaintExtent,
224+
layoutExtent: clampDouble(layoutExtent, 0, clampedPaintExtent),
225+
maxPaintExtent: maxExtent,
226+
maxScrollObstructionExtent: minExtent,
227+
hasVisualOverflow: true,
228+
);
229+
return 0;
230+
}
231+
232+
@override
233+
void performLayout() {
234+
final SliverConstraints constraints = this.constraints;
235+
final double maxExtent = childExtent;
236+
if (_lastActualScrollOffset != null &&
237+
((constraints.scrollOffset < _lastActualScrollOffset!) || (_effectiveScrollOffset! < maxExtent))) {
238+
double delta = _lastActualScrollOffset! - constraints.scrollOffset;
239+
240+
final bool allowFloatingExpansion =
241+
constraints.userScrollDirection == ScrollDirection.forward ||
242+
(_lastStartedScrollDirection != null && _lastStartedScrollDirection == ScrollDirection.forward);
243+
if (allowFloatingExpansion) {
244+
if (_effectiveScrollOffset! > maxExtent) {
245+
_effectiveScrollOffset = maxExtent;
246+
}
247+
} else {
248+
if (delta > 0.0) {
249+
delta = 0.0;
250+
}
251+
}
252+
_effectiveScrollOffset = clampDouble(_effectiveScrollOffset! - delta, 0, constraints.scrollOffset);
253+
} else {
254+
_effectiveScrollOffset = constraints.scrollOffset;
255+
}
256+
final bool overlapsContent = _effectiveScrollOffset! < constraints.scrollOffset;
257+
258+
layoutChild(_effectiveScrollOffset!, maxExtent, overlapsContent: overlapsContent);
259+
_childPosition = updateGeometry();
260+
_lastActualScrollOffset = constraints.scrollOffset;
261+
}
262+
263+
@override
264+
void showOnScreen({
265+
RenderObject? descendant,
266+
Rect? rect,
267+
Duration duration = Duration.zero,
268+
Curve curve = Curves.ease,
269+
}) {
270+
assert(child != null || descendant == null, "showOnScreen");
271+
272+
final Rect? childBounds = descendant != null
273+
? MatrixUtils.transformRect(descendant.getTransformTo(child), rect ?? descendant.paintBounds)
274+
: rect;
275+
276+
double targetExtent;
277+
Rect? targetRect;
278+
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
279+
case AxisDirection.up:
280+
targetExtent = childExtent - (childBounds?.top ?? 0);
281+
targetRect = _trim(childBounds, bottom: childExtent);
282+
case AxisDirection.right:
283+
targetExtent = childBounds?.right ?? childExtent;
284+
targetRect = _trim(childBounds, left: 0);
285+
case AxisDirection.down:
286+
targetExtent = childBounds?.bottom ?? childExtent;
287+
targetRect = _trim(childBounds, top: 0);
288+
case AxisDirection.left:
289+
targetExtent = childExtent - (childBounds?.left ?? 0);
290+
targetRect = _trim(childBounds, right: childExtent);
291+
}
292+
293+
final double effectiveMaxExtent = math.max(childExtent, childExtent);
294+
295+
targetExtent = clampDouble(
296+
clampDouble(targetExtent, double.negativeInfinity, double.infinity),
297+
childExtent,
298+
effectiveMaxExtent,
299+
);
300+
301+
super.showOnScreen(
302+
descendant: descendant == null ? this : child,
303+
rect: targetRect,
304+
duration: duration,
305+
curve: curve,
306+
);
307+
}
308+
309+
@override
310+
double childMainAxisPosition(RenderBox child) {
311+
assert(child == this.child, "childMainAxisPosition");
312+
return _childPosition ?? 0.0;
313+
}
314+
315+
void updateChild() {
316+
assert(_element != null, "updateChild");
317+
_element!._build();
318+
}
319+
320+
@protected
321+
void triggerRebuild() {
322+
markNeedsLayout();
323+
}
324+
}

0 commit comments

Comments
 (0)