slivers_appbar_pinned_test.dart 16.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1 2 3 4 5 6
// 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';
7
import 'package:flutter/widgets.dart';
Ian Hickson's avatar
Ian Hickson committed
8 9

void verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) {
10
  final RenderSliver target = key.currentContext.findRenderObject();
11
  expect(target.parent, 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, remainingCacheExtent: 850.0\n'
        ' │   cacheOrigin: 0.0 )\n'
95
        ' │ geometry: SliverGeometry(scrollExtent: 200.0, paintExtent: 200.0,\n'
96 97
        ' │   maxPaintExtent: 200.0, hasVisualOverflow: true, cacheExtent:\n'
        ' │   200.0)\n'
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
        ' │ 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'
119
        '         additionalConstraints: BoxConstraints(biggest)\n'
120 121 122 123
      ),
    );
  });

Ian Hickson's avatar
Ian Hickson committed
124 125 126 127
  testWidgets('Sliver appbars - pinned with slow scroll', (WidgetTester tester) async {
    const double bigHeight = 550.0;
    GlobalKey key1, key2, key3, key4, key5;
    await tester.pumpWidget(
128 129 130 131 132 133 134 135 136 137 138
      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
139 140
      ),
    );
141

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

  testWidgets('Sliver appbars - pinned with less overlap', (WidgetTester tester) async {
    const double bigHeight = 650.0;
    GlobalKey key1, key2, key3, key4, key5;
    await tester.pumpWidget(
221 222 223 224 225 226 227 228 229 230 231
      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
232 233
      ),
    );
234
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
Ian Hickson's avatar
Ian Hickson committed
235 236 237 238 239 240
    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);
241
    position.animateTo(10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
242
    await tester.pumpAndSettle(const Duration(milliseconds: 10));
Ian Hickson's avatar
Ian Hickson committed
243 244 245
    expect(position.pixels, max);
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, max);
246 247
    verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
    verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
248
    verifyPaintPosition(key3, const Offset(0.0, 100.0), true);
249 250
    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
251
  });
252 253 254

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

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

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

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

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

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

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

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

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

  @override
303 304
  double get minExtent => 100.0;

305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
  @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;
  }

327 328 329
  @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
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 360 361
  }

  @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 {
362
  const BigSliver({ Key key, this.height }) : super(key: key);
Ian Hickson's avatar
Ian Hickson committed
363 364 365 366 367 368 369 370 371 372 373 374 375

  final double height;

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

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