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

import 'dart:math' as math;

import 'package:flutter/widgets.dart';
8
import 'package:flutter_test/flutter_test.dart';
9 10 11 12 13

import '../rendering/mock_canvas.dart';

final Matcher doesNotOverscroll = isNot(paints..circle());

14
Future<void> slowDrag(WidgetTester tester, Offset start, Offset offset) async {
15
  final TestGesture gesture = await tester.startGesture(start);
16 17 18 19 20 21 22 23 24 25
  for (int index = 0; index < 10; index += 1) {
    await gesture.moveBy(offset);
    await tester.pump(const Duration(milliseconds: 20));
  }
  await gesture.up();
}

void main() {
  testWidgets('Overscroll indicator color', (WidgetTester tester) async {
    await tester.pumpWidget(
26
      const Directionality(
27
        textDirection: TextDirection.ltr,
28
        child: CustomScrollView(
29
          slivers: <Widget>[
30
            SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
31 32
          ],
        ),
33 34
      ),
    );
35
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
36 37 38 39

    expect(painter, doesNotOverscroll);

    // the scroll gesture from tester.scroll happens in zero time, so nothing should appear:
40
    await tester.drag(find.byType(Scrollable), const Offset(0.0, 100.0));
41 42 43 44 45 46
    expect(painter, doesNotOverscroll);
    await tester.pump(); // allow the ticker to register itself
    expect(painter, doesNotOverscroll);
    await tester.pump(const Duration(milliseconds: 100)); // animate
    expect(painter, doesNotOverscroll);

47
    final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
48 49 50 51 52
    await tester.pump(const Duration(milliseconds: 100)); // animate
    expect(painter, doesNotOverscroll);
    await gesture.up();
    expect(painter, doesNotOverscroll);

53
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
54 55
    expect(painter, paints..circle(color: const Color(0x0DFFFFFF)));

56
    await tester.pumpAndSettle(const Duration(seconds: 1));
57 58
    expect(painter, doesNotOverscroll);
  });
59

60 61
  testWidgets('Nested scrollable', (WidgetTester tester) async {
    await tester.pumpWidget(
62
      Directionality(
63
        textDirection: TextDirection.ltr,
64
        child: GlowingOverscrollIndicator(
65 66 67
          axisDirection: AxisDirection.down,
          color: const Color(0x0DFFFFFF),
          notificationPredicate: (ScrollNotification notification) => notification.depth == 1,
68
          child: const SingleChildScrollView(
69
            scrollDirection: Axis.horizontal,
70
            child: SizedBox(
71
                width: 600.0,
72
                child: CustomScrollView(
73
                  slivers: <Widget>[
74
                    SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
75 76 77 78 79 80 81
                  ],
                ),
              ),
            ),
          ),
        ),
      );
82

83 84
    final RenderObject outerPainter = tester.renderObject(find.byType(CustomPaint).first);
    final RenderObject innerPainter = tester.renderObject(find.byType(CustomPaint).last);
85

86 87 88 89
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
    expect(outerPainter, paints..circle());
    expect(innerPainter, paints..circle());
  });
90

91 92
  testWidgets('Overscroll indicator changes side when you drag on the other side', (WidgetTester tester) async {
    await tester.pumpWidget(
93
      const Directionality(
94
        textDirection: TextDirection.ltr,
95
        child: CustomScrollView(
96
          slivers: <Widget>[
97
            SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
98 99
          ],
        ),
100 101
      ),
    );
102
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
103

104
    await slowDrag(tester, const Offset(400.0, 200.0), const Offset(0.0, 10.0));
105
    expect(painter, paints..circle(x: 400.0));
106
    await slowDrag(tester, const Offset(100.0, 200.0), const Offset(0.0, 10.0));
107
    expect(painter, paints..something((Symbol method, List<dynamic> arguments) {
108
      if (method != #drawCircle) {
109
        return false;
110
      }
111
      final Offset center = arguments[0] as Offset;
112
      if (center.dx < 400.0) {
113
        return true;
114
      }
115 116
      throw 'Dragging on left hand side did not overscroll on left hand side.';
    }));
117
    await slowDrag(tester, const Offset(700.0, 200.0), const Offset(0.0, 10.0));
118
    expect(painter, paints..something((Symbol method, List<dynamic> arguments) {
119
      if (method != #drawCircle) {
120
        return false;
121
      }
122
      final Offset center = arguments[0] as Offset;
123
      if (center.dx > 400.0) {
124
        return true;
125
      }
126 127 128
      throw 'Dragging on right hand side did not overscroll on right hand side.';
    }));

129
    await tester.pumpAndSettle(const Duration(seconds: 1));
130 131 132 133 134
    expect(painter, doesNotOverscroll);
  });

  testWidgets('Overscroll indicator changes side when you shift sides', (WidgetTester tester) async {
    await tester.pumpWidget(
135
      const Directionality(
136
        textDirection: TextDirection.ltr,
137
        child: CustomScrollView(
138
          slivers: <Widget>[
139
            SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
140 141
          ],
        ),
142 143
      ),
    );
144
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
145
    final TestGesture gesture = await tester.startGesture(const Offset(300.0, 200.0));
146 147 148 149 150 151 152
    await gesture.moveBy(const Offset(0.0, 10.0));
    await tester.pump(const Duration(milliseconds: 20));
    double oldX = 0.0;
    for (int index = 0; index < 10; index += 1) {
      await gesture.moveBy(const Offset(50.0, 50.0));
      await tester.pump(const Duration(milliseconds: 20));
      expect(painter, paints..something((Symbol method, List<dynamic> arguments) {
153
        if (method != #drawCircle) {
154
          return false;
155
        }
156
        final Offset center = arguments[0] as Offset;
157
        if (center.dx <= oldX) {
158
          throw 'Sliding to the right did not make the center of the radius slide to the right.';
159
        }
160
        oldX = center.dx;
161 162 163 164 165
        return true;
      }));
    }
    await gesture.up();

166
    await tester.pumpAndSettle(const Duration(seconds: 1));
167 168 169
    expect(painter, doesNotOverscroll);
  });

170
  group("Flipping direction of scrollable doesn't change overscroll behavior", () {
171 172
    testWidgets('down', (WidgetTester tester) async {
      await tester.pumpWidget(
173
        const Directionality(
174
          textDirection: TextDirection.ltr,
175
          child: CustomScrollView(
176 177
            physics: AlwaysScrollableScrollPhysics(),
            slivers: <Widget>[
178
              SliverToBoxAdapter(child: SizedBox(height: 20.0)),
179 180
            ],
          ),
181 182
        ),
      );
183
      final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
184
      await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
185 186
      expect(painter, paints..save()..circle()..restore()..save()..scale(y: -1.0)..restore()..restore());

187
      await tester.pumpAndSettle(const Duration(seconds: 1));
188 189 190 191 192
      expect(painter, doesNotOverscroll);
    });

    testWidgets('up', (WidgetTester tester) async {
      await tester.pumpWidget(
193
        const Directionality(
194
          textDirection: TextDirection.ltr,
195
          child: CustomScrollView(
196
            reverse: true,
197 198
            physics: AlwaysScrollableScrollPhysics(),
            slivers: <Widget>[
199
              SliverToBoxAdapter(child: SizedBox(height: 20.0)),
200 201
            ],
          ),
202 203
        ),
      );
204
      final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
205
      await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
206 207
      expect(painter, paints..save()..scale(y: -1.0)..restore()..save()..circle()..restore()..restore());

208
      await tester.pumpAndSettle(const Duration(seconds: 1));
209 210 211 212 213 214
      expect(painter, doesNotOverscroll);
    });
  });

  testWidgets('Overscroll in both directions', (WidgetTester tester) async {
    await tester.pumpWidget(
215
      const Directionality(
216
        textDirection: TextDirection.ltr,
217
        child: CustomScrollView(
218 219
          physics: AlwaysScrollableScrollPhysics(),
          slivers: <Widget>[
220
            SliverToBoxAdapter(child: SizedBox(height: 20.0)),
221 222
          ],
        ),
223 224
      ),
    );
225
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
226
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
227 228
    expect(painter, paints..circle());
    expect(painter, isNot(paints..circle()..circle()));
229
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, -5.0));
230 231
    expect(painter, paints..circle()..circle());

232
    await tester.pumpAndSettle(const Duration(seconds: 1));
233 234 235 236
    expect(painter, doesNotOverscroll);
  });

  testWidgets('Overscroll horizontally', (WidgetTester tester) async {
237
    await tester.pumpWidget(
238
      const Directionality(
239
        textDirection: TextDirection.ltr,
240
        child: CustomScrollView(
241
          scrollDirection: Axis.horizontal,
242 243
          physics: AlwaysScrollableScrollPhysics(),
          slivers: <Widget>[
244
            SliverToBoxAdapter(child: SizedBox(height: 20.0)),
245 246
          ],
        ),
247
      ),
248
    );
249
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
250
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(5.0, 0.0));
251
    expect(painter, paints..rotate(angle: math.pi / 2.0)..circle()..saveRestore());
252
    expect(painter, isNot(paints..circle()..circle()));
253
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(-5.0, 0.0));
254 255 256 257 258 259 260 261
    expect(
      painter,
      paints
        ..rotate(angle: math.pi / 2.0)
        ..circle()
        ..rotate(angle: math.pi / 2.0)
        ..circle(),
    );
262

263
    await tester.pumpAndSettle(const Duration(seconds: 1));
264 265 266
    expect(painter, doesNotOverscroll);
  });

267
  testWidgets('Nested overscrolls do not throw exceptions', (WidgetTester tester) async {
268
    await tester.pumpWidget(Directionality(
269
      textDirection: TextDirection.ltr,
270
      child: PageView(
271
        children: <Widget>[
272
          ListView(
273
            children: <Widget>[
274
              Container(
275 276
                width: 2000.0,
                height: 2000.0,
277
                color: const Color(0xFF00FF00),
278 279 280 281 282
              ),
            ],
          ),
        ],
      ),
283
    ));
284

285
    await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 2000.0));
286
    await tester.pumpAndSettle();
287 288
  });

289 290 291
  testWidgets('Changing settings', (WidgetTester tester) async {
    RenderObject painter;

292
    await tester.pumpWidget(
293
      const Directionality(
294
        textDirection: TextDirection.ltr,
295 296
        child: ScrollConfiguration(
          behavior: TestScrollBehavior1(),
297
          child: CustomScrollView(
298
            scrollDirection: Axis.horizontal,
299
            physics: AlwaysScrollableScrollPhysics(),
300
            reverse: true,
301
            slivers: <Widget>[
302
              SliverToBoxAdapter(child: SizedBox(height: 20.0)),
303 304
            ],
          ),
305
        ),
306
      ),
307
    );
308
    painter = tester.renderObject(find.byType(CustomPaint));
309
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(5.0, 0.0));
310
    expect(painter, paints..rotate(angle: math.pi / 2.0)..circle(color: const Color(0x0A00FF00)));
311 312
    expect(painter, isNot(paints..circle()..circle()));

313
    await tester.pumpAndSettle(const Duration(seconds: 1));
314
    await tester.pumpWidget(
315
      const Directionality(
316
        textDirection: TextDirection.ltr,
317 318
        child: ScrollConfiguration(
          behavior: TestScrollBehavior2(),
319
          child: CustomScrollView(
320
            scrollDirection: Axis.horizontal,
321 322
            physics: AlwaysScrollableScrollPhysics(),
            slivers: <Widget>[
323
              SliverToBoxAdapter(child: SizedBox(height: 20.0)),
324 325
            ],
          ),
326
        ),
327
      ),
328
    );
329
    painter = tester.renderObject(find.byType(CustomPaint));
330
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(5.0, 0.0));
331
    expect(painter, paints..rotate(angle: math.pi / 2.0)..circle(color: const Color(0x0A0000FF))..saveRestore());
332 333
    expect(painter, isNot(paints..circle()..circle()));
  });
334

335 336 337 338 339 340
  testWidgets('CustomScrollView overscroll indicator works if there is sliver before center', (WidgetTester tester) async {
    final Key centerKey = UniqueKey();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ScrollConfiguration(
341
          behavior: const TestScrollBehavior2(),
342 343 344 345 346 347
          child: CustomScrollView(
            center: centerKey,
            physics: const AlwaysScrollableScrollPhysics(),
            slivers: <Widget>[
              SliverList(
                delegate: SliverChildBuilderDelegate(
348
                  (BuildContext context, int index) => Text('First sliver $index'),
349 350 351 352 353 354
                  childCount: 2,
                ),
              ),
              SliverList(
                key: centerKey,
                delegate: SliverChildBuilderDelegate(
355
                  (BuildContext context, int index) => Text('Second sliver $index'),
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
                  childCount: 5,
                ),
              ),
            ],
          ),
        ),
      ),
    );

    expect(find.text('First sliver 1'), findsNothing);

    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 300.0));
    expect(find.text('First sliver 1'), findsOneWidget);
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
    // The scroll offset and paint extend should cancel out each other.
    expect(painter, paints..save()..translate(y: 0.0)..scale()..circle());
  });

374 375 376 377 378 379
  testWidgets('CustomScrollView overscroll indicator works well with [CustomScrollView.center] and [OverscrollIndicatorNotification.paintOffset]', (WidgetTester tester) async {
    final Key centerKey = UniqueKey();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ScrollConfiguration(
380
          behavior: const TestScrollBehavior2(),
381 382 383 384 385 386 387 388 389 390 391 392 393
          child: NotificationListener<OverscrollIndicatorNotification>(
            onNotification: (OverscrollIndicatorNotification notification) {
              if (notification.leading) {
                notification.paintOffset = 50.0;
              }
              return false;
            },
            child: CustomScrollView(
              center: centerKey,
              physics: const AlwaysScrollableScrollPhysics(),
              slivers: <Widget>[
                SliverList(
                  delegate: SliverChildBuilderDelegate(
394
                    (BuildContext context, int index) => Text('First sliver $index'),
395 396 397 398 399 400
                    childCount: 2,
                  ),
                ),
                SliverList(
                  key: centerKey,
                  delegate: SliverChildBuilderDelegate(
401
                    (BuildContext context, int index) => Text('Second sliver $index'),
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
                    childCount: 5,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(find.text('First sliver 1'), findsNothing);

    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0)); // offset will be magnified ten times
    expect(find.text('First sliver 1'), findsOneWidget);
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
    // The OverscrollIndicator should respect the [OverscrollIndicatorNotification.paintOffset] setting.
    expect(painter, paints..save()..translate(y: 50.0)..scale()..circle());
  });

  testWidgets('The OverscrollIndicator should not overflow the scrollable view edge', (WidgetTester tester) async {
    // Regressing test for https://github.com/flutter/flutter/issues/64149
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: NotificationListener<OverscrollIndicatorNotification>(
          onNotification: (OverscrollIndicatorNotification notification) {
            notification.paintOffset = 50.0; // both the leading and trailing indicator have a 50.0 pixels offset.
            return false;
          },
          child: const CustomScrollView(
            slivers: <Widget>[
              SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
            ],
          ),
        ),
      ),
    );
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
    expect(painter, paints..save()..translate(y: 50.0)..scale()..circle());
    // Reverse scroll (30 pixels), and the offset < notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -30.0));
    await tester.pump();
    // The OverscrollIndicator should move with the CustomScrollView.
    expect(painter, paints..save()..translate(y: 50.0 - 30.0)..scale()..circle());

    // Reverse scroll (30+20 pixels) and offset == notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -20.0));
    await tester.pump();
    expect(painter, paints..save()..translate(y: 50.0 - 50.0)..scale()..circle());

    // Reverse scroll (30+20+10 pixels) and offset > notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -10.0));
    await tester.pump();
    // The OverscrollIndicator should not overflow the CustomScrollView's edge.
    expect(painter, paints..save()..translate(y: 50.0 - 50.0)..scale()..circle());

    await tester.pumpAndSettle(); // Finish the leading indicator.

    // trigger the trailing indicator
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, -200.0));
    expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0)..scale()..circle());

    // Reverse scroll (30 pixels), and the offset < notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 30.0));
    await tester.pump();
    // The OverscrollIndicator should move with the CustomScrollView.
    expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 30.0)..scale()..circle());

    // Reverse scroll (30+20 pixels) and offset == notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 20.0));
    await tester.pump();
    expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 50.0)..scale()..circle());

    // Reverse scroll (30+20+10 pixels) and offset > notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 10.0));
    await tester.pump();
    // The OverscrollIndicator should not overflow the CustomScrollView's edge.
    expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 50.0)..scale()..circle());
  });

  group('[OverscrollIndicatorNotification.paintOffset] test', () {
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
    testWidgets('Leading', (WidgetTester tester) async {
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: NotificationListener<OverscrollIndicatorNotification>(
            onNotification: (OverscrollIndicatorNotification notification) {
              if (notification.leading) {
                notification.paintOffset = 50.0;
              }
              return false;
            },
            child: const CustomScrollView(
              slivers: <Widget>[
                SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
              ],
            ),
          ),
        ),
      );
      final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
      await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
505
      // The OverscrollIndicator should respect the [OverscrollIndicatorNotification.paintOffset] setting.
506 507 508 509
      expect(painter, paints..save()..translate(y: 50.0)..scale()..circle());
      // Reverse scroll direction.
      await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -30.0));
      await tester.pump();
510
      // The OverscrollIndicator should move with the CustomScrollView.
511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536
      expect(painter, paints..save()..translate(y: 50.0 - 30.0)..scale()..circle());
    });

    testWidgets('Trailing', (WidgetTester tester) async {
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: NotificationListener<OverscrollIndicatorNotification>(
            onNotification: (OverscrollIndicatorNotification notification) {
              if (!notification.leading) {
                notification.paintOffset = 50.0;
              }
              return false;
            },
            child: const CustomScrollView(
              slivers: <Widget>[
                SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
              ],
            ),
          ),
        ),
      );
      final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
      await tester.dragFrom(const Offset(200.0, 200.0), const Offset(200.0, -10000.0));
      await tester.pump();
      await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, -5.0));
537
      // The OverscrollIndicator should respect the [OverscrollIndicatorNotification.paintOffset] setting.
538 539 540 541
      expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0)..scale()..circle());
      // Reverse scroll direction.
      await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 30.0));
      await tester.pump();
542
      // The OverscrollIndicator should move with the CustomScrollView.
543 544 545
      expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 30.0)..scale()..circle());
    });
  });
546 547
}

Adam Barth's avatar
Adam Barth committed
548
class TestScrollBehavior1 extends ScrollBehavior {
549 550
  const TestScrollBehavior1();

551
  @override
552
  Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
553
    return GlowingOverscrollIndicator(
554
      axisDirection: details.direction,
555
      color: const Color(0xFF00FF00),
556
      child: child,
557
    );
558 559 560
  }
}

Adam Barth's avatar
Adam Barth committed
561
class TestScrollBehavior2 extends ScrollBehavior {
562 563
  const TestScrollBehavior2();

564
  @override
565
  Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
566
    return GlowingOverscrollIndicator(
567
      axisDirection: details.direction,
568
      color: const Color(0xFF0000FF),
569
      child: child,
570
    );
571 572
  }
}