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

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

void verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) {
10
  final RenderSliver target = key.currentContext.findRenderObject();
Adam Barth's avatar
Adam Barth committed
11
  expect(target.parent, const isInstanceOf<RenderViewport>());
12 13
  final SliverPhysicalParentData parentData = target.parentData;
  final Offset actual = parentData.paintOffset;
Ian Hickson's avatar
Ian Hickson committed
14
  expect(actual, ideal);
15
  final SliverGeometry geometry = target.geometry;
Ian Hickson's avatar
Ian Hickson committed
16 17 18 19
  expect(geometry.visible, visible);
}

void verifyActualBoxPosition(WidgetTester tester, Finder finder, int index, Rect ideal) {
20
  final RenderBox box = tester.renderObjectList<RenderBox>(finder).elementAt(index);
21
  final Rect rect = new Rect.fromPoints(box.localToGlobal(Offset.zero), box.localToGlobal(box.size.bottomRight(Offset.zero)));
Ian Hickson's avatar
Ian Hickson committed
22 23 24 25 26 27 28 29
  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(
30 31 32 33 34 35 36 37 38 39 40
      new Directionality(
        textDirection: TextDirection.ltr,
        child: new CustomScrollView(
          slivers: <Widget>[
            new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
            new SliverPersistentHeader(key: key2 = new GlobalKey(), delegate: new TestDelegate(), pinned: true),
            new SliverPersistentHeader(key: key3 = new GlobalKey(), delegate: new TestDelegate(), pinned: true),
            new BigSliver(key: key4 = new GlobalKey(), height: bigHeight),
            new BigSliver(key: key5 = new GlobalKey(), height: bigHeight),
          ],
        ),
Ian Hickson's avatar
Ian Hickson committed
41 42
      ),
    );
43
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
Ian Hickson's avatar
Ian Hickson committed
44 45 46 47 48 49
    final double max = bigHeight * 3.0 + new TestDelegate().maxExtent * 2.0 - 600.0; // 600 is the height of the test viewport
    assert(max < 10000.0);
    expect(max, 1450.0);
    expect(position.pixels, 0.0);
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, max);
50
    position.animateTo(10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
51
    await tester.pumpAndSettle(const Duration(milliseconds: 10));
Ian Hickson's avatar
Ian Hickson committed
52 53 54
    expect(position.pixels, max);
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, max);
55 56
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
57
    verifyPaintPosition(key3, const Offset(0.0, 100.0), true);
58 59
    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
60 61
  });

62 63 64 65
  testWidgets('Sliver appbars - toStringDeep of maxExtent that throws', (WidgetTester tester) async {
    final TestDelegateThatCanThrow delegateThatCanThrow = new TestDelegateThatCanThrow();
    GlobalKey key;
    await tester.pumpWidget(
66 67 68 69 70 71 72
      new Directionality(
        textDirection: TextDirection.ltr,
        child: new CustomScrollView(
          slivers: <Widget>[
            new SliverPersistentHeader(key: key = new GlobalKey(), delegate: delegateThatCanThrow, pinned: true),
          ],
        ),
73 74 75 76 77 78 79 80 81 82 83 84
      ),
    );
    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(
85
      renderObject.toStringDeep(minLevel: DiagnosticLevel.info),
86 87 88 89 90 91
      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'
92
        ' │   crossAxisDirection: AxisDirection.right,\n'
93 94
        ' │   viewportMainAxisExtent: 600.0)\n'
        ' │ geometry: SliverGeometry(scrollExtent: 200.0, paintExtent: 200.0,\n'
95
        ' │   maxPaintExtent: 200.0, hasVisualOverflow: true)\n'
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
        ' │ 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'
117
        '         additionalConstraints: BoxConstraints(biggest)\n'
118 119 120 121
      ),
    );
  });

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

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

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

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

272 273
    expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 200.0));
274

275
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
276 277 278
    position.jumpTo(-50.0);
    await tester.pump();

279 280
    expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 250.0));
281 282 283 284

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

285 286
    expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 150.0));
287 288 289 290

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

291 292
    expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 50.0));
293
  });
Ian Hickson's avatar
Ian Hickson committed
294 295
}

296
class TestDelegate extends SliverPersistentHeaderDelegate {
Ian Hickson's avatar
Ian Hickson committed
297 298 299 300
  @override
  double get maxExtent => 200.0;

  @override
301 302
  double get minExtent => 100.0;

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

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

class TestDelegateThatCanThrow extends SliverPersistentHeaderDelegate {
  bool shouldThrow = false;

  @override
  double get maxExtent {
    return shouldThrow ? throw new FlutterError('Unavailable maxExtent') : 200.0;
  }

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

325 326 327
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return new Container(constraints: new BoxConstraints(minHeight: minExtent, maxHeight: maxExtent));
Ian Hickson's avatar
Ian Hickson committed
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
  }

  @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();
  }

  double get paintExtent => (height - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent);

  @override
  void performLayout() {
    geometry = new SliverGeometry(
      scrollExtent: height,
      paintExtent: paintExtent,
      maxPaintExtent: height,
    );
  }
}

class BigSliver extends LeafRenderObjectWidget {
360
  const BigSliver({ Key key, this.height }) : super(key: key);
Ian Hickson's avatar
Ian Hickson committed
361 362 363 364 365 366 367 368 369 370 371 372 373

  final double height;

  @override
  RenderBigSliver createRenderObject(BuildContext context) {
    return new RenderBigSliver(height);
  }

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