Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7ca1ff7

Browse files
gnpricechrisbobbe
authored andcommittedApr 30, 2025·
scroll [nfc]: Cut CustomPaintOrderScrollView now that upstream PR merged
This functionality is now covered by [ScrollView.paintOrder], after my PR adding that feature upstream.
1 parent e6cd503 commit 7ca1ff7

File tree

3 files changed

+2
-390
lines changed

3 files changed

+2
-390
lines changed
 

‎lib/widgets/message_list.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import 'emoji_reaction.dart';
2121
import 'icons.dart';
2222
import 'page.dart';
2323
import 'profile.dart';
24-
import 'scrolling.dart' hide SliverPaintOrder;
24+
import 'scrolling.dart';
2525
import 'sticky_header.dart';
2626
import 'store.dart';
2727
import 'text.dart';

‎lib/widgets/scrolling.dart

Lines changed: 0 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -43,212 +43,6 @@ class _SingleChildScrollViewWithScrollbarState
4343
}
4444
}
4545

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

‎test/widgets/scrolling_test.dart

Lines changed: 1 addition & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,11 @@
11
import 'package:checks/checks.dart';
2-
// ignore: undefined_hidden_name // anticipates https://github.com/flutter/flutter/pull/164818
3-
import 'package:flutter/rendering.dart' hide SliverPaintOrder;
4-
// ignore: undefined_hidden_name // anticipates https://github.com/flutter/flutter/pull/164818
5-
import 'package:flutter/widgets.dart' hide SliverPaintOrder;
2+
import 'package:flutter/widgets.dart';
63
import 'package:flutter_test/flutter_test.dart';
74
import 'package:zulip/widgets/scrolling.dart';
85

96
import '../flutter_checks.dart';
107

118
void main() {
12-
group('CustomPaintOrderScrollView paint order', () {
13-
final paintLog = <int>[];
14-
15-
Widget makeSliver(int i) {
16-
return SliverToBoxAdapter(
17-
key: ValueKey(i),
18-
child: CustomPaint(
19-
painter: TestCustomPainter()
20-
..onPaint = (_, _) => paintLog.add(i),
21-
child: Text('Item $i')));
22-
}
23-
24-
testWidgets('firstIsTop', (tester) async {
25-
addTearDown(paintLog.clear);
26-
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
27-
child: CustomPaintOrderScrollView(
28-
paintOrder: SliverPaintOrder.firstIsTop,
29-
center: ValueKey(2), anchor: 0.5,
30-
slivers: List.generate(5, makeSliver))));
31-
32-
// First sliver paints last, over other slivers; last sliver paints first.
33-
check(paintLog).deepEquals([4, 3, 2, 1, 0]);
34-
});
35-
36-
testWidgets('lastIsTop', (tester) async {
37-
addTearDown(paintLog.clear);
38-
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
39-
child: CustomPaintOrderScrollView(
40-
paintOrder: SliverPaintOrder.lastIsTop,
41-
center: ValueKey(2), anchor: 0.5,
42-
slivers: List.generate(5, makeSliver))));
43-
44-
// Last sliver paints last, over other slivers; first sliver paints first.
45-
check(paintLog).deepEquals([0, 1, 2, 3, 4]);
46-
});
47-
48-
testWidgets('centerTopFirstBottom', (tester) async {
49-
addTearDown(paintLog.clear);
50-
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
51-
child: CustomPaintOrderScrollView(
52-
paintOrder: SliverPaintOrder.centerTopFirstBottom,
53-
center: ValueKey(2), anchor: 0.5,
54-
slivers: List.generate(5, makeSliver))));
55-
56-
// The particular order CustomScrollView paints in.
57-
check(paintLog).deepEquals([0, 1, 4, 3, 2]);
58-
59-
// Check that CustomScrollView indeed paints in the same order.
60-
final result = paintLog.toList();
61-
paintLog.clear();
62-
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
63-
child: CustomScrollView(
64-
center: ValueKey(2), anchor: 0.5,
65-
slivers: List.generate(5, makeSliver))));
66-
check(paintLog).deepEquals(result);
67-
}, skip: true, // TODO(upstream): once PR 164818 lands, cut our CustomPaintOrderScrollView
68-
// in favor of CustomScrollView; this test was checking that
69-
// the former matched the latter's old default behavior.
70-
);
71-
});
72-
73-
group('CustomPaintOrderScrollView hit-test order', () {
74-
Widget makeSliver(int i) {
75-
return _AllOverlapSliver(key: ValueKey<int>(i), id: i);
76-
}
77-
78-
List<int> sliverIds(Iterable<HitTestEntry> path) => [
79-
for (final e in path)
80-
if (e.target case _RenderAllOverlapSliver(:final id))
81-
id,
82-
];
83-
84-
testWidgets('firstIsTop', (WidgetTester tester) async {
85-
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
86-
child: CustomPaintOrderScrollView(
87-
paintOrder: SliverPaintOrder.firstIsTop,
88-
center: const ValueKey(2), anchor: 0.5,
89-
slivers: List.generate(5, makeSliver))));
90-
91-
final result = tester.hitTestOnBinding(const Offset(400, 300));
92-
check(sliverIds(result.path)).deepEquals([0, 1, 2, 3, 4]);
93-
});
94-
95-
testWidgets('lastIsTop', (WidgetTester tester) async {
96-
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
97-
child: CustomPaintOrderScrollView(
98-
paintOrder: SliverPaintOrder.lastIsTop,
99-
center: const ValueKey(2), anchor: 0.5,
100-
slivers: List.generate(5, makeSliver))));
101-
102-
final result = tester.hitTestOnBinding(const Offset(400, 300));
103-
check(sliverIds(result.path)).deepEquals([4, 3, 2, 1, 0]);
104-
});
105-
106-
testWidgets('centerTopFirstBottom', (tester) async {
107-
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
108-
child: CustomPaintOrderScrollView(
109-
paintOrder: SliverPaintOrder.centerTopFirstBottom,
110-
center: const ValueKey(2), anchor: 0.5,
111-
slivers: List.generate(5, makeSliver))));
112-
113-
final result = tester.hitTestOnBinding(const Offset(400, 300));
114-
// The particular order CustomScrollView hit-tests in.
115-
check(sliverIds(result.path)).deepEquals([2, 3, 4, 1, 0]);
116-
117-
// Check that CustomScrollView indeed hit-tests in the same order.
118-
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
119-
child: CustomScrollView(
120-
center: const ValueKey(2), anchor: 0.5,
121-
slivers: List.generate(5, makeSliver))));
122-
check(sliverIds(tester.hitTestOnBinding(const Offset(400, 300)).path))
123-
.deepEquals(sliverIds(result.path));
124-
}, skip: true, // TODO(upstream): once PR 164818 lands, cut our CustomPaintOrderScrollView
125-
// in favor of CustomScrollView; this test was checking that
126-
// the former matched the latter's old default behavior.
127-
);
128-
});
129-
1309
group('MessageListScrollView', () {
13110
Widget buildList({
13211
MessageListScrollController? controller,
@@ -309,64 +188,3 @@ void main() {
309188
});
310189
});
311190
}
312-
313-
class TestCustomPainter extends CustomPainter {
314-
void Function(Canvas canvas, Size size)? onPaint;
315-
316-
@override
317-
void paint(Canvas canvas, Size size) {
318-
if (onPaint != null) onPaint!(canvas, size);
319-
}
320-
321-
@override
322-
bool shouldRepaint(covariant CustomPainter oldDelegate) {
323-
return true;
324-
}
325-
}
326-
327-
/// A sliver that overlaps with other slivers as far as possible,
328-
/// and does nothing else.
329-
class _AllOverlapSliver extends LeafRenderObjectWidget {
330-
const _AllOverlapSliver({super.key, required this.id});
331-
332-
final int id;
333-
334-
@override
335-
RenderObject createRenderObject(BuildContext context) => _RenderAllOverlapSliver(id);
336-
}
337-
338-
class _RenderAllOverlapSliver extends RenderSliver {
339-
_RenderAllOverlapSliver(this.id);
340-
341-
final int id;
342-
343-
@override
344-
void performLayout() {
345-
geometry = SliverGeometry(
346-
paintExtent: constraints.remainingPaintExtent,
347-
maxPaintExtent: constraints.remainingPaintExtent,
348-
layoutExtent: 0.0,
349-
);
350-
}
351-
352-
@override
353-
bool hitTest(
354-
SliverHitTestResult result, {
355-
required double mainAxisPosition,
356-
required double crossAxisPosition,
357-
}) {
358-
if (mainAxisPosition >= 0.0 &&
359-
mainAxisPosition < geometry!.hitTestExtent &&
360-
crossAxisPosition >= 0.0 &&
361-
crossAxisPosition < constraints.crossAxisExtent) {
362-
result.add(
363-
SliverHitTestEntry(
364-
this,
365-
mainAxisPosition: mainAxisPosition,
366-
crossAxisPosition: crossAxisPosition,
367-
),
368-
);
369-
}
370-
return false;
371-
}
372-
}

0 commit comments

Comments
 (0)
Please sign in to comment.