Skip to content

Commit 1667c6e

Browse files
committed
scroll: Start out scrolled to bottom of list
This is NFC as to the real message list, because so far the bottom sliver there always has height 0, so that maxScrollExtent is 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 starting out scrolled to the end.
1 parent 2541a10 commit 1667c6e

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

lib/widgets/scrolling.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,36 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext {
278278
final effectiveMax = wholeMaxScrollExtent;
279279
return applyContentDimensions(effectiveMin, effectiveMax);
280280
}
281+
282+
bool _hasEverCompletedLayout = false;
283+
284+
@override
285+
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
286+
// Inspired by _TabBarScrollPosition.applyContentDimensions upstream.
287+
bool changed = false;
288+
289+
if (!_hasEverCompletedLayout) {
290+
// The list is being laid out for the first time (its first performLayout).
291+
// Start out scrolled to the end.
292+
final target = maxScrollExtent;
293+
if (!hasPixels || pixels != target) {
294+
correctPixels(target);
295+
changed = true;
296+
}
297+
}
298+
299+
if (!super.applyContentDimensions(minScrollExtent, maxScrollExtent)) {
300+
changed = true;
301+
}
302+
303+
if (!changed) {
304+
// Because this method is about to return true,
305+
// this will be the last round of this layout.
306+
_hasEverCompletedLayout = true;
307+
}
308+
309+
return !changed;
310+
}
281311
}
282312

283313
/// A version of [ScrollController] adapted for the Zulip message list.

test/widgets/scrolling_test.dart

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'package:flutter/widgets.dart' hide SliverPaintOrder;
66
import 'package:flutter_test/flutter_test.dart';
77
import 'package:zulip/widgets/scrolling.dart';
88

9+
import '../flutter_checks.dart';
10+
911
void main() {
1012
group('CustomPaintOrderScrollView paint order', () {
1113
final paintLog = <int>[];
@@ -127,6 +129,78 @@ void main() {
127129
.deepEquals(sliverIds(result.path));
128130
});
129131
});
132+
133+
group('MessageListScrollView', () {
134+
Future<void> prepare(WidgetTester tester,
135+
{required double topHeight, required double bottomHeight}) async {
136+
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
137+
child: MessageListScrollView(
138+
controller: MessageListScrollController(),
139+
center: const ValueKey('center'),
140+
slivers: [
141+
SliverToBoxAdapter(
142+
child: SizedBox(height: topHeight, child: Text('top'))),
143+
SliverToBoxAdapter(key: const ValueKey('center'),
144+
child: SizedBox(height: bottomHeight, child: Text('bottom'))),
145+
])));
146+
await tester.pump();
147+
}
148+
149+
// The `skipOffstage: false` produces more informative output
150+
// when a test fails because one of the slivers is just offscreen.
151+
final findTop = find.text('top', skipOffstage: false);
152+
final findBottom = find.text('bottom', skipOffstage: false);
153+
154+
testWidgets('short/short -> starts scrolled to bottom', (tester) async {
155+
// Starts out with items at bottom of viewport.
156+
await prepare(tester, topHeight: 100, bottomHeight: 100);
157+
check(tester.getRect(findBottom)).bottom.equals(600);
158+
159+
// Try scrolling down (by dragging up); doesn't move.
160+
await tester.drag(findTop, Offset(0, -100));
161+
await tester.pump();
162+
check(tester.getRect(findBottom)).bottom.equals(600);
163+
});
164+
165+
testWidgets('short/long -> starts scrolled to bottom', (tester) async {
166+
// Starts out scrolled to bottom.
167+
await prepare(tester, topHeight: 100, bottomHeight: 800);
168+
check(tester.getRect(findBottom)).bottom.equals(600);
169+
170+
// Try scrolling down (by dragging up); doesn't move.
171+
await tester.drag(findBottom, Offset(0, -100));
172+
await tester.pump();
173+
check(tester.getRect(findBottom)).bottom.equals(600);
174+
});
175+
176+
testWidgets('starts at bottom, even when bottom underestimated at first', (tester) async {
177+
const numItems = 10;
178+
const itemHeight = 300.0;
179+
180+
// A list where the bottom sliver takes several rounds of layout
181+
// to see how long it really is.
182+
final controller = MessageListScrollController();
183+
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
184+
child: MessageListScrollView(
185+
controller: controller,
186+
center: const ValueKey('center'),
187+
slivers: [
188+
SliverToBoxAdapter(
189+
child: SizedBox(height: 100, child: Text('top'))),
190+
SliverList.list(key: const ValueKey('center'),
191+
children: List.generate(numItems, (i) =>
192+
SizedBox(height: (i+1) * itemHeight, child: Text('item $i')))),
193+
])));
194+
await tester.pump();
195+
196+
// Starts out scrolled all the way to the bottom,
197+
// even though it must have taken several rounds of layout to find that.
198+
check(controller.position.pixels)
199+
.equals(itemHeight * numItems * (numItems + 1)/2);
200+
check(tester.getRect(find.text('item ${numItems-1}', skipOffstage: false)))
201+
.bottom.equals(600);
202+
});
203+
});
130204
}
131205

132206
class TestCustomPainter extends CustomPainter {

0 commit comments

Comments
 (0)