Skip to content

Commit 0f7bc7a

Browse files
committed
scroll: Stay at end once there
This is NFC as to the real message list, because so far the bottom sliver there always has height 0, so that both maxScrollExtent and this.maxScrollExtent are always 0. This is a step toward letting us move part of the message list into the bottom sliver, because it means that doing so would preserve the list's current behavior of remaining scrolled to the end once there as e.g. new messages arrive.
1 parent 07a3905 commit 0f7bc7a

File tree

2 files changed

+51
-3
lines changed

2 files changed

+51
-3
lines changed

Diff for: lib/widgets/scrolling.dart

+16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:math' as math;
22

33
import 'package:flutter/foundation.dart';
44
import 'package:flutter/material.dart';
5+
import 'package:flutter/physics.dart';
56
import 'package:flutter/rendering.dart';
67

78
/// A [SingleChildScrollView] that always shows a Material [Scrollbar].
@@ -312,6 +313,9 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext {
312313
return applyContentDimensions(effectiveMin, effectiveMax);
313314
}
314315

316+
bool _nearEqual(double a, double b) =>
317+
nearEqual(a, b, Tolerance.defaultTolerance.distance);
318+
315319
bool _hasEverCompletedLayout = false;
316320

317321
@override
@@ -329,6 +333,13 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext {
329333
correctPixels(target);
330334
changed = true;
331335
}
336+
} else if (_nearEqual(pixels, this.maxScrollExtent)
337+
&& !_nearEqual(pixels, maxScrollExtent)) {
338+
// The list was scrolled to the end before this layout round.
339+
// Make sure it stays at the end.
340+
// (For example, show the new message that just arrived.)
341+
correctPixels(maxScrollExtent);
342+
changed = true;
332343
}
333344

334345
// This step must come after the first-time correction above.
@@ -337,6 +348,11 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext {
337348
// then the base implementation of [applyContentDimensions] would
338349
// bring it in bounds via a scrolling animation, which isn't right when
339350
// starting from the meaningless initial 0.0 value.
351+
//
352+
// For the "stays at the end" correction, it's not clear if the order
353+
// matters in practice. But the doc on [applyNewDimensions], called by
354+
// the base [applyContentDimensions], says it should come after any
355+
// calls to [correctPixels]; so OK, do this after the [correctPixels].
340356
if (!super.applyContentDimensions(minScrollExtent, maxScrollExtent)) {
341357
changed = true;
342358
}

Diff for: test/widgets/scrolling_test.dart

+35-3
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,14 @@ void main() {
131131
});
132132

133133
group('MessageListScrollView', () {
134-
Future<void> prepare(WidgetTester tester,
135-
{required double topHeight, required double bottomHeight}) async {
134+
Future<void> prepare(WidgetTester tester, {
135+
MessageListScrollController? controller,
136+
required double topHeight,
137+
required double bottomHeight,
138+
}) async {
136139
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
137140
child: MessageListScrollView(
138-
controller: MessageListScrollController(),
141+
controller: controller ?? MessageListScrollController(),
139142
center: const ValueKey('center'),
140143
slivers: [
141144
SliverToBoxAdapter(
@@ -233,6 +236,35 @@ void main() {
233236
check(tester.getRect(find.text('item ${numItems-1}', skipOffstage: false)))
234237
.bottom.equals(600);
235238
});
239+
240+
testWidgets('stick to end of list when it grows', (tester) async {
241+
final controller = MessageListScrollController();
242+
await prepare(tester, controller: controller,
243+
topHeight: 400, bottomHeight: 400);
244+
check(tester.getRect(findBottom))..top.equals(200)..bottom.equals(600);
245+
246+
// Bottom sliver grows; remain scrolled to (new) bottom.
247+
await prepare(tester, controller: controller,
248+
topHeight: 400, bottomHeight: 500);
249+
check(tester.getRect(findBottom))..top.equals(100)..bottom.equals(600);
250+
});
251+
252+
testWidgets('when not at end, let it grow without following', (tester) async {
253+
final controller = MessageListScrollController();
254+
await prepare(tester, controller: controller,
255+
topHeight: 400, bottomHeight: 400);
256+
check(tester.getRect(findBottom))..top.equals(200)..bottom.equals(600);
257+
258+
// Scroll up (by dragging down) to detach from end of list.
259+
await tester.drag(findBottom, Offset(0, 100));
260+
await tester.pump();
261+
check(tester.getRect(findBottom))..top.equals(300)..bottom.equals(700);
262+
263+
// Bottom sliver grows; remain at existing position, now farther from end.
264+
await prepare(tester, controller: controller,
265+
topHeight: 400, bottomHeight: 500);
266+
check(tester.getRect(findBottom))..top.equals(300)..bottom.equals(800);
267+
});
236268
});
237269
}
238270

0 commit comments

Comments
 (0)