Skip to content

Commit 83cdc11

Browse files
committed
scrolling: Add CustomPaintOrderScrollView
This lets us control the paint order between the slivers, so that the top sliver is able to overflow over the bottom sliver when a sticky header calls for that. I've sent a PR upstream to add the same feature to CustomScrollView itself: flutter/flutter#164818 That PR has been delayed by infra issues in Google testing, though. So doing this in our tree lets us move ahead without waiting. (I actually wrote this version first, then adapted it to produce the upstream PR. So landing this version isn't significant extra work.)
1 parent b9de887 commit 83cdc11

File tree

2 files changed

+398
-0
lines changed

2 files changed

+398
-0
lines changed

lib/widgets/scrolling.dart

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter/rendering.dart';
23

34
/// A [SingleChildScrollView] that always shows a Material [Scrollbar].
45
///
@@ -37,3 +38,209 @@ class _SingleChildScrollViewWithScrollbarState
3738
child: widget.child));
3839
}
3940
}
41+
42+
/// Specifies an order in which to paint the slivers of a [CustomScrollView].
43+
///
44+
/// Whichever order the slivers are painted in,
45+
/// they will be hit-tested in the opposite order.
46+
///
47+
/// This can also be thought of as an ordering in the z-direction:
48+
/// whichever sliver is painted last (and hit-tested first) is on top,
49+
/// because it will paint over other slivers if there is overlap.
50+
/// Similarly, whichever sliver is painted first (and hit-tested last)
51+
/// is on the bottom.
52+
enum SliverPaintOrder {
53+
/// The first sliver paints on top, and the last sliver on bottom.
54+
///
55+
/// The slivers are painted in the reverse order of [CustomScrollView.slivers],
56+
/// and hit-tested in the same order as [CustomScrollView.slivers].
57+
firstIsTop,
58+
59+
/// The last sliver paints on top, and the first sliver on bottom.
60+
///
61+
/// The slivers are painted in the same order as [CustomScrollView.slivers],
62+
/// and hit-tested in the reverse order.
63+
lastIsTop,
64+
65+
/// The default order for [CustomScrollView]: the center sliver paints on top,
66+
/// and the first sliver paints on bottom.
67+
///
68+
/// If [CustomScrollView.center] is null or corresponds to the first sliver
69+
/// in [CustomScrollView.slivers], this order is equivalent to [firstIsTop].
70+
/// Otherwise, the [CustomScrollView.center] sliver paints on top;
71+
/// it's followed in the z-order by the slivers after it to the end
72+
/// of the list, then the slivers before the center in reverse order,
73+
/// with the first sliver in the list at the bottom in the z-direction.
74+
centerTopFirstBottom,
75+
}
76+
77+
/// A [CustomScrollView] with control over the paint order, or z-order,
78+
/// between slivers.
79+
///
80+
/// This is just like [CustomScrollView] except it adds the [paintOrder_] field.
81+
///
82+
/// (Actually there's one [CustomScrollView] feature this doesn't implement:
83+
/// [shrinkWrap] always has its default value of false. That feature would be
84+
/// easy to add if desired.)
85+
// TODO(upstream): Pending PR: https://github.com/flutter/flutter/pull/164818
86+
// Notes from before sending that PR:
87+
// Add an option [ScrollView.zOrder]? (An enum, or possibly
88+
// a delegate.) Or at minimum document on [ScrollView.center] the
89+
// existing behavior, which is counterintuitive.
90+
// Nearest related upstream feature requests I find are for a "z-index",
91+
// for CustomScrollView, Column, Row, and Stack respectively:
92+
// https://github.com/flutter/flutter/issues/121173#issuecomment-1712825747
93+
// https://github.com/flutter/flutter/issues/121173
94+
// https://github.com/flutter/flutter/issues/121173#issuecomment-1914959184
95+
// https://github.com/flutter/flutter/issues/70836
96+
// A delegate would give enough flexibility for that and much else,
97+
// but I'm not sure how many use cases wouldn't be covered by a small enum.
98+
//
99+
// Ah, and here's a more on-point issue (more recently):
100+
// https://github.com/flutter/flutter/issues/145592
101+
//
102+
// TODO: perh sticky_header should configure a CustomPaintOrderScrollView automatically?
103+
class CustomPaintOrderScrollView extends CustomScrollView {
104+
const CustomPaintOrderScrollView({
105+
super.key,
106+
super.scrollDirection,
107+
super.reverse,
108+
super.controller,
109+
super.primary,
110+
super.physics,
111+
super.scrollBehavior,
112+
// super.shrinkWrap, // omitted, always false
113+
super.center,
114+
super.anchor,
115+
super.cacheExtent,
116+
super.slivers,
117+
super.semanticChildCount,
118+
super.dragStartBehavior,
119+
super.keyboardDismissBehavior,
120+
super.restorationId,
121+
super.clipBehavior,
122+
super.hitTestBehavior,
123+
SliverPaintOrder paintOrder = SliverPaintOrder.centerTopFirstBottom,
124+
}) : paintOrder_ = paintOrder;
125+
126+
/// The order in which to paint the slivers;
127+
/// equivalently, the order in which to arrange them in the z-direction.
128+
///
129+
/// Whichever order the slivers are painted in,
130+
/// they will be hit-tested in the opposite order.
131+
///
132+
/// To think of this as an ordering in the z-direction:
133+
/// whichever sliver is painted last (and hit-tested first) is on top,
134+
/// because it will paint over other slivers if there is overlap.
135+
/// Similarly, whichever sliver is painted first (and hit-tested last)
136+
/// is on the bottom.
137+
///
138+
/// This defaults to [SliverPaintOrder.centerTopFirstBottom],
139+
/// the behavior of the [CustomScrollView] base class.
140+
final SliverPaintOrder paintOrder_;
141+
142+
@override
143+
Widget buildViewport(BuildContext context, ViewportOffset offset,
144+
AxisDirection axisDirection, List<Widget> slivers) {
145+
return CustomPaintOrderViewport(
146+
axisDirection: axisDirection,
147+
offset: offset,
148+
slivers: slivers,
149+
cacheExtent: cacheExtent,
150+
center: center,
151+
anchor: anchor,
152+
clipBehavior: clipBehavior,
153+
paintOrder_: paintOrder_,
154+
);
155+
}
156+
}
157+
158+
/// The viewport configured by a [CustomPaintOrderScrollView].
159+
class CustomPaintOrderViewport extends Viewport {
160+
CustomPaintOrderViewport({
161+
super.key,
162+
super.axisDirection,
163+
super.crossAxisDirection,
164+
super.anchor,
165+
required super.offset,
166+
super.center,
167+
super.cacheExtent,
168+
super.cacheExtentStyle,
169+
super.slivers,
170+
super.clipBehavior,
171+
required this.paintOrder_,
172+
});
173+
174+
final SliverPaintOrder paintOrder_;
175+
176+
@override
177+
RenderViewport createRenderObject(BuildContext context) {
178+
return RenderCustomPaintOrderViewport(
179+
axisDirection: axisDirection,
180+
crossAxisDirection: crossAxisDirection
181+
?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
182+
anchor: anchor,
183+
offset: offset,
184+
cacheExtent: cacheExtent,
185+
cacheExtentStyle: cacheExtentStyle,
186+
clipBehavior: clipBehavior,
187+
paintOrder_: paintOrder_,
188+
);
189+
}
190+
}
191+
192+
/// The render object configured by a [CustomPaintOrderViewport].
193+
class RenderCustomPaintOrderViewport extends RenderViewport {
194+
RenderCustomPaintOrderViewport({
195+
super.axisDirection,
196+
required super.crossAxisDirection,
197+
required super.offset,
198+
super.anchor,
199+
super.children,
200+
super.center,
201+
super.cacheExtent,
202+
super.cacheExtentStyle,
203+
super.clipBehavior,
204+
required this.paintOrder_,
205+
});
206+
207+
final SliverPaintOrder paintOrder_;
208+
209+
Iterable<RenderSliver> get _lastToFirst {
210+
final List<RenderSliver> children = <RenderSliver>[];
211+
RenderSliver? child = lastChild;
212+
while (child != null) {
213+
children.add(child);
214+
child = childBefore(child);
215+
}
216+
return children;
217+
}
218+
219+
Iterable<RenderSliver> get _firstToLast {
220+
final List<RenderSliver> children = <RenderSliver>[];
221+
RenderSliver? child = firstChild;
222+
while (child != null) {
223+
children.add(child);
224+
child = childAfter(child);
225+
}
226+
return children;
227+
}
228+
229+
@override
230+
Iterable<RenderSliver> get childrenInPaintOrder {
231+
return switch (paintOrder_) {
232+
SliverPaintOrder.firstIsTop => _lastToFirst,
233+
SliverPaintOrder.lastIsTop => _firstToLast,
234+
SliverPaintOrder.centerTopFirstBottom => super.childrenInPaintOrder,
235+
};
236+
}
237+
238+
@override
239+
Iterable<RenderSliver> get childrenInHitTestOrder {
240+
return switch (paintOrder_) {
241+
SliverPaintOrder.firstIsTop => _firstToLast,
242+
SliverPaintOrder.lastIsTop => _lastToFirst,
243+
SliverPaintOrder.centerTopFirstBottom => super.childrenInHitTestOrder,
244+
};
245+
}
246+
}

0 commit comments

Comments
 (0)