slivers_appbar_pinned_test.dart 16.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Ian Hickson's avatar
Ian Hickson committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

Ian Hickson's avatar
Ian Hickson committed
7 8
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
9
import 'package:flutter/widgets.dart';
Ian Hickson's avatar
Ian Hickson committed
10 11

void verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) {
12
  final RenderSliver target = key.currentContext.findRenderObject() as RenderSliver;
Dan Field's avatar
Dan Field committed
13
  expect(target.parent, isA<RenderViewport>());
14
  final SliverPhysicalParentData parentData = target.parentData as SliverPhysicalParentData;
15
  final Offset actual = parentData.paintOffset;
Ian Hickson's avatar
Ian Hickson committed
16
  expect(actual, ideal);
17
  final SliverGeometry geometry = target.geometry;
Ian Hickson's avatar
Ian Hickson committed
18 19 20 21
  expect(geometry.visible, visible);
}

void verifyActualBoxPosition(WidgetTester tester, Finder finder, int index, Rect ideal) {
22
  final RenderBox box = tester.renderObjectList<RenderBox>(finder).elementAt(index);
23
  final Rect rect = Rect.fromPoints(box.localToGlobal(Offset.zero), box.localToGlobal(box.size.bottomRight(Offset.zero)));
Ian Hickson's avatar
Ian Hickson committed
24 25 26 27 28 29 30 31
  expect(rect, equals(ideal));
}

void main() {
  testWidgets('Sliver appbars - pinned', (WidgetTester tester) async {
    const double bigHeight = 550.0;
    GlobalKey key1, key2, key3, key4, key5;
    await tester.pumpWidget(
32
      Directionality(
33
        textDirection: TextDirection.ltr,
34
        child: CustomScrollView(
35
          slivers: <Widget>[
36 37 38 39 40
            BigSliver(key: key1 = GlobalKey(), height: bigHeight),
            SliverPersistentHeader(key: key2 = GlobalKey(), delegate: TestDelegate(), pinned: true),
            SliverPersistentHeader(key: key3 = GlobalKey(), delegate: TestDelegate(), pinned: true),
            BigSliver(key: key4 = GlobalKey(), height: bigHeight),
            BigSliver(key: key5 = GlobalKey(), height: bigHeight),
41 42
          ],
        ),
Ian Hickson's avatar
Ian Hickson committed
43 44
      ),
    );
45
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
46
    final double max = bigHeight * 3.0 + TestDelegate().maxExtent * 2.0 - 600.0; // 600 is the height of the test viewport
Ian Hickson's avatar
Ian Hickson committed
47 48 49 50 51
    assert(max < 10000.0);
    expect(max, 1450.0);
    expect(position.pixels, 0.0);
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, max);
52
    position.animateTo(10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
53
    await tester.pumpAndSettle(const Duration(milliseconds: 10));
Ian Hickson's avatar
Ian Hickson committed
54 55 56
    expect(position.pixels, max);
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, max);
57 58
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
59
    verifyPaintPosition(key3, const Offset(0.0, 100.0), true);
60 61
    verifyPaintPosition(key4, const Offset(0.0, 0.0), true);
    verifyPaintPosition(key5, const Offset(0.0, 50.0), true);
Ian Hickson's avatar
Ian Hickson committed
62 63
  });

64
  testWidgets('Sliver appbars - toStringDeep of maxExtent that throws', (WidgetTester tester) async {
65
    final TestDelegateThatCanThrow delegateThatCanThrow = TestDelegateThatCanThrow();
66 67
    GlobalKey key;
    await tester.pumpWidget(
68
      Directionality(
69
        textDirection: TextDirection.ltr,
70
        child: CustomScrollView(
71
          slivers: <Widget>[
72
            SliverPersistentHeader(key: key = GlobalKey(), delegate: delegateThatCanThrow, pinned: true),
73 74
          ],
        ),
75 76 77 78 79 80 81 82 83 84 85 86
      ),
    );
    await tester.pumpAndSettle(const Duration(milliseconds: 10));

    final RenderObject renderObject = key.currentContext.findRenderObject();
    // The delegate must only start throwing immediately before calling
    // toStringDeep to avoid triggering spurious exceptions.
    // If the _RenderSliverPinnedPersistentHeaderForWidgets class was not
    // private it would make more sense to create an instance of it directly.
    delegateThatCanThrow.shouldThrow = true;
    expect(renderObject, hasAGoodToStringDeep);
    expect(
87
      renderObject.toStringDeep(minLevel: DiagnosticLevel.info),
88 89 90 91 92 93
      equalsIgnoringHashCodes(
        '_RenderSliverPinnedPersistentHeaderForWidgets#00000 relayoutBoundary=up1\n'
        ' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
        ' │ constraints: SliverConstraints(AxisDirection.down,\n'
        ' │   GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
        ' │   0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n'
94
        ' │   crossAxisDirection: AxisDirection.right,\n'
95 96
        ' │   viewportMainAxisExtent: 600.0, remainingCacheExtent: 850.0,\n'
        ' │   cacheOrigin: 0.0)\n'
97
        ' │ geometry: SliverGeometry(scrollExtent: 200.0, paintExtent: 200.0,\n'
98 99
        ' │   maxPaintExtent: 200.0, hasVisualOverflow: true, cacheExtent:\n'
        ' │   200.0)\n'
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
        ' │ maxExtent: EXCEPTION (FlutterError)\n'
        ' │ child position: 0.0\n'
        ' │\n'
        ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
        '   │ parentData: <none> (can use size)\n'
        '   │ constraints: BoxConstraints(w=800.0, 0.0<=h<=200.0)\n'
        '   │ size: Size(800.0, 200.0)\n'
        '   │ additionalConstraints: BoxConstraints(0.0<=w<=Infinity,\n'
        '   │   100.0<=h<=200.0)\n'
        '   │\n'
        '   └─child: RenderLimitedBox#00000 relayoutBoundary=up3\n'
        '     │ parentData: <none> (can use size)\n'
        '     │ constraints: BoxConstraints(w=800.0, 100.0<=h<=200.0)\n'
        '     │ size: Size(800.0, 200.0)\n'
        '     │ maxWidth: 0.0\n'
        '     │ maxHeight: 0.0\n'
        '     │\n'
        '     └─child: RenderConstrainedBox#00000 relayoutBoundary=up4\n'
        '         parentData: <none> (can use size)\n'
        '         constraints: BoxConstraints(w=800.0, 100.0<=h<=200.0)\n'
        '         size: Size(800.0, 200.0)\n'
121
        '         additionalConstraints: BoxConstraints(biggest)\n'
122 123 124 125
      ),
    );
  });

Ian Hickson's avatar
Ian Hickson committed
126 127 128 129
  testWidgets('Sliver appbars - pinned with slow scroll', (WidgetTester tester) async {
    const double bigHeight = 550.0;
    GlobalKey key1, key2, key3, key4, key5;
    await tester.pumpWidget(
130
      Directionality(
131
        textDirection: TextDirection.ltr,
132
        child: CustomScrollView(
133
          slivers: <Widget>[
134 135 136 137 138
            BigSliver(key: key1 = GlobalKey(), height: bigHeight),
            SliverPersistentHeader(key: key2 = GlobalKey(), delegate: TestDelegate(), pinned: true),
            SliverPersistentHeader(key: key3 = GlobalKey(), delegate: TestDelegate(), pinned: true),
            BigSliver(key: key4 = GlobalKey(), height: bigHeight),
            BigSliver(key: key5 = GlobalKey(), height: bigHeight),
139 140
          ],
        ),
Ian Hickson's avatar
Ian Hickson committed
141 142
      ),
    );
143

144
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
145 146
    verifyPaintPosition(key1, const Offset(0.0, 0.0), true);
    verifyPaintPosition(key2, const Offset(0.0, 550.0), true);
147 148 149
    verifyPaintPosition(key3, const Offset(0.0, 750.0), false);
    verifyPaintPosition(key4, const Offset(0.0, 950.0), false);
    verifyPaintPosition(key5, const Offset(0.0, 1500.0), false);
150
    position.animateTo(550.0, curve: Curves.linear, duration: const Duration(minutes: 1));
151
    await tester.pumpAndSettle(const Duration(milliseconds: 100));
152 153 154 155
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
    verifyPaintPosition(key3, const Offset(0.0, 200.0), true);
    verifyPaintPosition(key4, const Offset(0.0, 400.0), true);
156
    verifyPaintPosition(key5, const Offset(0.0, 950.0), false);
157
    position.animateTo(600.0, curve: Curves.linear, duration: const Duration(minutes: 1));
158
    await tester.pumpAndSettle(const Duration(milliseconds: 200));
159 160 161 162
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
    verifyPaintPosition(key3, const Offset(0.0, 150.0), true);
    verifyPaintPosition(key4, const Offset(0.0, 350.0), true);
163
    verifyPaintPosition(key5, const Offset(0.0, 900.0), false);
164
    position.animateTo(650.0, curve: Curves.linear, duration: const Duration(minutes: 1));
165
    await tester.pumpAndSettle(const Duration(milliseconds: 300));
166 167 168
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
    verifyPaintPosition(key3, const Offset(0.0, 100.0), true);
Dan Field's avatar
Dan Field committed
169
    verifyActualBoxPosition(tester, find.byType(Container), 1, const Rect.fromLTWH(0.0, 100.0, 800.0, 200.0));
170
    verifyPaintPosition(key4, const Offset(0.0, 300.0), true);
171
    verifyPaintPosition(key5, const Offset(0.0, 850.0), false);
172
    position.animateTo(700.0, curve: Curves.linear, duration: const Duration(minutes: 1));
173
    await tester.pumpAndSettle(const Duration(milliseconds: 400));
174 175
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
176
    verifyPaintPosition(key3, const Offset(0.0, 100.0), true);
Dan Field's avatar
Dan Field committed
177
    verifyActualBoxPosition(tester, find.byType(Container), 1, const Rect.fromLTWH(0.0, 100.0, 800.0, 200.0));
178
    verifyPaintPosition(key4, const Offset(0.0, 250.0), true);
179
    verifyPaintPosition(key5, const Offset(0.0, 800.0), false);
180
    position.animateTo(750.0, curve: Curves.linear, duration: const Duration(minutes: 1));
181
    await tester.pumpAndSettle(const Duration(milliseconds: 500));
182 183
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
184
    verifyPaintPosition(key3, const Offset(0.0, 100.0), true);
Dan Field's avatar
Dan Field committed
185
    verifyActualBoxPosition(tester, find.byType(Container), 1, const Rect.fromLTWH(0.0, 100.0, 800.0, 200.0));
186
    verifyPaintPosition(key4, const Offset(0.0, 200.0), true);
187
    verifyPaintPosition(key5, const Offset(0.0, 750.0), false);
188
    position.animateTo(800.0, curve: Curves.linear, duration: const Duration(minutes: 1));
189
    await tester.pumpAndSettle(const Duration(milliseconds: 60));
190 191
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
192
    verifyPaintPosition(key3, const Offset(0.0, 100.0), true);
193
    verifyPaintPosition(key4, const Offset(0.0, 150.0), true);
194
    verifyPaintPosition(key5, const Offset(0.0, 700.0), false);
195
    position.animateTo(850.0, curve: Curves.linear, duration: const Duration(minutes: 1));
196
    await tester.pumpAndSettle(const Duration(milliseconds: 70));
197 198
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
199
    verifyPaintPosition(key3, const Offset(0.0, 100.0), true);
200
    verifyPaintPosition(key4, const Offset(0.0, 100.0), true);
201
    verifyPaintPosition(key5, const Offset(0.0, 650.0), false);
202
    position.animateTo(900.0, curve: Curves.linear, duration: const Duration(minutes: 1));
203
    await tester.pumpAndSettle(const Duration(milliseconds: 80));
204 205
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
206
    verifyPaintPosition(key3, const Offset(0.0, 100.0), true);
207 208
    verifyPaintPosition(key4, const Offset(0.0, 50.0), true);
    verifyPaintPosition(key5, const Offset(0.0, 600.0), false);
209
    position.animateTo(950.0, curve: Curves.linear, duration: const Duration(minutes: 1));
210
    await tester.pumpAndSettle(const Duration(milliseconds: 90));
211 212
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
213
    verifyPaintPosition(key3, const Offset(0.0, 100.0), true);
Dan Field's avatar
Dan Field committed
214
    verifyActualBoxPosition(tester, find.byType(Container), 1, const Rect.fromLTWH(0.0, 100.0, 800.0, 100.0));
215 216
    verifyPaintPosition(key4, const Offset(0.0, 0.0), true);
    verifyPaintPosition(key5, const Offset(0.0, 550.0), true);
Ian Hickson's avatar
Ian Hickson committed
217 218 219 220 221 222
  });

  testWidgets('Sliver appbars - pinned with less overlap', (WidgetTester tester) async {
    const double bigHeight = 650.0;
    GlobalKey key1, key2, key3, key4, key5;
    await tester.pumpWidget(
223
      Directionality(
224
        textDirection: TextDirection.ltr,
225
        child: CustomScrollView(
226
          slivers: <Widget>[
227 228 229 230 231
            BigSliver(key: key1 = GlobalKey(), height: bigHeight),
            SliverPersistentHeader(key: key2 = GlobalKey(), delegate: TestDelegate(), pinned: true),
            SliverPersistentHeader(key: key3 = GlobalKey(), delegate: TestDelegate(), pinned: true),
            BigSliver(key: key4 = GlobalKey(), height: bigHeight),
            BigSliver(key: key5 = GlobalKey(), height: bigHeight),
232 233
          ],
        ),
Ian Hickson's avatar
Ian Hickson committed
234 235
      ),
    );
236
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
237
    final double max = bigHeight * 3.0 + TestDelegate().maxExtent * 2.0 - 600.0; // 600 is the height of the test viewport
Ian Hickson's avatar
Ian Hickson committed
238 239 240 241 242
    assert(max < 10000.0);
    expect(max, 1750.0);
    expect(position.pixels, 0.0);
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, max);
243
    position.animateTo(10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
244
    await tester.pumpAndSettle(const Duration(milliseconds: 10));
Ian Hickson's avatar
Ian Hickson committed
245 246 247
    expect(position.pixels, max);
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, max);
248 249
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
250
    verifyPaintPosition(key3, const Offset(0.0, 100.0), true);
251 252
    verifyPaintPosition(key4, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key5, const Offset(0.0, 0.0), true);
Ian Hickson's avatar
Ian Hickson committed
253
  });
254 255 256

  testWidgets('Sliver appbars - overscroll gap is below header', (WidgetTester tester) async {
    await tester.pumpWidget(
257
      Directionality(
258
        textDirection: TextDirection.ltr,
259
        child: CustomScrollView(
260 261
          physics: const BouncingScrollPhysics(),
          slivers: <Widget>[
262
            SliverPersistentHeader(delegate: TestDelegate(), pinned: true),
263
            SliverList(
264
              delegate: SliverChildListDelegate(<Widget>[
265
                const SizedBox(
266
                  height: 300.0,
267
                  child: Text('X'),
268 269 270 271 272
                ),
              ]),
            ),
          ],
        ),
273 274 275
      ),
    );

276 277
    expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 200.0));
278

279
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
280 281 282
    position.jumpTo(-50.0);
    await tester.pump();

283 284
    expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 250.0));
285 286 287 288

    position.jumpTo(50.0);
    await tester.pump();

289 290
    expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 150.0));
291 292 293 294

    position.jumpTo(150.0);
    await tester.pump();

295 296
    expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 50.0));
297
  });
Ian Hickson's avatar
Ian Hickson committed
298 299
}

300
class TestDelegate extends SliverPersistentHeaderDelegate {
Ian Hickson's avatar
Ian Hickson committed
301 302 303 304
  @override
  double get maxExtent => 200.0;

  @override
305 306
  double get minExtent => 100.0;

307 308
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
309
    return Container(constraints: BoxConstraints(minHeight: minExtent, maxHeight: maxExtent));
310 311 312 313 314 315 316 317 318 319 320
  }

  @override
  bool shouldRebuild(TestDelegate oldDelegate) => false;
}

class TestDelegateThatCanThrow extends SliverPersistentHeaderDelegate {
  bool shouldThrow = false;

  @override
  double get maxExtent {
321
    return shouldThrow ? throw FlutterError('Unavailable maxExtent') : 200.0;
322 323 324 325
  }

  @override
  double get minExtent {
326
    return shouldThrow ? throw FlutterError('Unavailable minExtent') : 100.0;
327 328
  }

329 330
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
331
    return Container(constraints: BoxConstraints(minHeight: minExtent, maxHeight: maxExtent));
Ian Hickson's avatar
Ian Hickson committed
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
  }

  @override
  bool shouldRebuild(TestDelegate oldDelegate) => false;
}


class RenderBigSliver extends RenderSliver {
  RenderBigSliver(double height) : _height = height;

  double get height => _height;
  double _height;
  set height(double value) {
    if (value == _height)
      return;
    _height = value;
    markNeedsLayout();
  }

351
  double get paintExtent => (height - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent) as double;
Ian Hickson's avatar
Ian Hickson committed
352 353 354

  @override
  void performLayout() {
355
    geometry = SliverGeometry(
Ian Hickson's avatar
Ian Hickson committed
356 357 358 359 360 361 362 363
      scrollExtent: height,
      paintExtent: paintExtent,
      maxPaintExtent: height,
    );
  }
}

class BigSliver extends LeafRenderObjectWidget {
364
  const BigSliver({ Key key, this.height }) : super(key: key);
Ian Hickson's avatar
Ian Hickson committed
365 366 367 368 369

  final double height;

  @override
  RenderBigSliver createRenderObject(BuildContext context) {
370
    return RenderBigSliver(height);
Ian Hickson's avatar
Ian Hickson committed
371 372 373 374 375 376 377
  }

  @override
  void updateRenderObject(BuildContext context, RenderBigSliver renderObject) {
    renderObject.height = height;
  }
}