Skip to content

Commit f20dfb7

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 eec002d commit f20dfb7

File tree

2 files changed

+45
-0
lines changed

2 files changed

+45
-0
lines changed

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].
@@ -320,6 +321,9 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext {
320321
return applyContentDimensions(effectiveMin, effectiveMax);
321322
}
322323

324+
bool _nearEqual(double a, double b) =>
325+
nearEqual(a, b, Tolerance.defaultTolerance.distance);
326+
323327
bool _hasEverCompletedLayout = false;
324328

325329
@override
@@ -337,6 +341,13 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext {
337341
correctPixels(target);
338342
changed = true;
339343
}
344+
} else if (_nearEqual(pixels, this.maxScrollExtent)
345+
&& !_nearEqual(pixels, maxScrollExtent)) {
346+
// The list was scrolled to the end before this layout round.
347+
// Make sure it stays at the end.
348+
// (For example, show the new message that just arrived.)
349+
correctPixels(maxScrollExtent);
350+
changed = true;
340351
}
341352

342353
// This step must come after the first-time correction above.
@@ -345,6 +356,11 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext {
345356
// then the base implementation of [applyContentDimensions] would
346357
// bring it in bounds via a scrolling animation, which isn't right when
347358
// starting from the meaningless initial 0.0 value.
359+
//
360+
// For the "stays at the end" correction, it's not clear if the order
361+
// matters in practice. But the doc on [applyNewDimensions], called by
362+
// the base [applyContentDimensions], says it should come after any
363+
// calls to [correctPixels]; so OK, do this after the [correctPixels].
348364
if (!super.applyContentDimensions(minScrollExtent, maxScrollExtent)) {
349365
changed = true;
350366
}

test/widgets/scrolling_test.dart

+29
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,35 @@ void main() {
243243
.bottom.equals(600);
244244
});
245245

246+
testWidgets('stick to end of list when it grows', (tester) async {
247+
final controller = MessageListScrollController();
248+
await prepare(tester, controller: controller,
249+
topHeight: 400, bottomHeight: 400);
250+
check(tester.getRect(findBottom))..top.equals(200)..bottom.equals(600);
251+
252+
// Bottom sliver grows; remain scrolled to (new) bottom.
253+
await prepare(tester, controller: controller,
254+
topHeight: 400, bottomHeight: 500);
255+
check(tester.getRect(findBottom))..top.equals(100)..bottom.equals(600);
256+
});
257+
258+
testWidgets('when not at end, let it grow without following', (tester) async {
259+
final controller = MessageListScrollController();
260+
await prepare(tester, controller: controller,
261+
topHeight: 400, bottomHeight: 400);
262+
check(tester.getRect(findBottom))..top.equals(200)..bottom.equals(600);
263+
264+
// Scroll up (by dragging down) to detach from end of list.
265+
await tester.drag(findBottom, Offset(0, 100));
266+
await tester.pump();
267+
check(tester.getRect(findBottom))..top.equals(300)..bottom.equals(700);
268+
269+
// Bottom sliver grows; remain at existing position, now farther from end.
270+
await prepare(tester, controller: controller,
271+
topHeight: 400, bottomHeight: 500);
272+
check(tester.getRect(findBottom))..top.equals(300)..bottom.equals(800);
273+
});
274+
246275
testWidgets('position preserved when scrollable rebuilds', (tester) async {
247276
// Tests that [MessageListScrollPosition.absorb] does its job.
248277
//

0 commit comments

Comments
 (0)