material_test.dart 21.1 KB
Newer Older
1 2 3 4
// 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.

5 6
import 'dart:io' show Platform;

7
import 'package:flutter/material.dart';
8
import 'package:flutter/painting.dart';
9
import 'package:flutter/rendering.dart';
10 11
import 'package:flutter_test/flutter_test.dart';

12
import '../rendering/mock_canvas.dart';
13
import '../widgets/test_border.dart' show TestBorder;
14

15 16 17
class NotifyMaterial extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
18 19
    LayoutChangedNotification().dispatch(context);
    return Container();
20 21 22
  }
}

23 24 25 26
Widget buildMaterial({
  double elevation = 0.0,
  Color shadowColor = const Color(0xFF00FF00),
}) {
27 28
  return Center(
    child: SizedBox(
29 30
      height: 100.0,
      width: 100.0,
31
      child: Material(
32
        shadowColor: shadowColor,
33
        elevation: elevation,
34
        shape: const CircleBorder(),
35 36 37 38 39
      ),
    ),
  );
}

40 41
RenderPhysicalShape getShadow(WidgetTester tester) {
  return tester.renderObject(find.byType(PhysicalShape));
42 43
}

44 45 46
class PaintRecorder extends CustomPainter {
  PaintRecorder(this.log);

47
  final List<Size> log;
48 49 50 51

  @override
  void paint(Canvas canvas, Size size) {
    log.add(size);
52
    final Paint paint = Paint()..color = const Color(0xFF0000FF);
53
    canvas.drawRect(Offset.zero & size, paint);
54 55 56 57 58 59
  }

  @override
  bool shouldRepaint(PaintRecorder oldDelegate) => false;
}

60
void main() {
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
  testWidgets('default Material debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
    const Material().debugFillProperties(builder);

    final List<String> description = builder.properties
      .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
      .map((DiagnosticsNode node) => node.toString())
      .toList();

    expect(description, <String>['type: canvas']);
  });

  testWidgets('Material implements debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
    const Material(
      type: MaterialType.canvas,
      color: Color(0xFFFFFFFF),
      textStyle: TextStyle(color: Color(0xff00ff00)),
      borderRadius: BorderRadiusDirectional.all(Radius.circular(10)),
    ).debugFillProperties(builder);

    final List<String> description = builder.properties
      .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
      .map((DiagnosticsNode node) => node.toString())
      .toList();

    expect(description, <String>[
      'type: canvas',
      'color: Color(0xffffffff)',
      'textStyle.inherit: true',
      'textStyle.color: Color(0xff00ff00)',
92
      'borderRadius: BorderRadiusDirectional.circular(10.0)',
93 94 95
    ]);
  });

96
  testWidgets('LayoutChangedNotification test', (WidgetTester tester) async {
97
    await tester.pumpWidget(
98 99
      Material(
        child: NotifyMaterial(),
100
      ),
101 102
    );
  });
103 104

  testWidgets('ListView scroll does not repaint', (WidgetTester tester) async {
105
    final List<Size> log = <Size>[];
106 107

    await tester.pumpWidget(
108
      Directionality(
109
        textDirection: TextDirection.ltr,
110
        child: Column(
111
          children: <Widget>[
112
            SizedBox(
113 114
              width: 150.0,
              height: 150.0,
115 116
              child: CustomPaint(
                painter: PaintRecorder(log),
117
              ),
118
            ),
119 120 121
            Expanded(
              child: Material(
                child: Column(
122
                  children: <Widget>[
123 124
                    Expanded(
                      child: ListView(
125
                        children: <Widget>[
126
                          Container(
127 128 129 130 131
                            height: 2000.0,
                            color: const Color(0xFF00FF00),
                          ),
                        ],
                      ),
132
                    ),
133
                    SizedBox(
134 135
                      width: 100.0,
                      height: 100.0,
136 137
                      child: CustomPaint(
                        painter: PaintRecorder(log),
138
                      ),
139
                    ),
140 141
                  ],
                ),
142 143
              ),
            ),
144 145
          ],
        ),
146 147 148 149 150 151 152 153 154 155 156
      ),
    );

    // We paint twice because we have two CustomPaint widgets in the tree above
    // to test repainting both inside and outside the Material widget.
    expect(log, equals(<Size>[
      const Size(150.0, 150.0),
      const Size(100.0, 100.0),
    ]));
    log.clear();

157
    await tester.drag(find.byType(ListView), const Offset(0.0, -300.0));
158 159 160 161
    await tester.pump();

    expect(log, isEmpty);
  });
162 163

  testWidgets('Shadows animate smoothly', (WidgetTester tester) async {
164 165
    // This code verifies that the PhysicalModel's elevation animates over
    // a kThemeChangeDuration time interval.
166

167
    await tester.pumpWidget(buildMaterial(elevation: 0.0));
168
    final RenderPhysicalShape modelA = getShadow(tester);
169 170
    expect(modelA.elevation, equals(0.0));

171
    await tester.pumpWidget(buildMaterial(elevation: 9.0));
172
    final RenderPhysicalShape modelB = getShadow(tester);
173
    expect(modelB.elevation, equals(0.0));
174 175

    await tester.pump(const Duration(milliseconds: 1));
176
    final RenderPhysicalShape modelC = getShadow(tester);
177
    expect(modelC.elevation, closeTo(0.0, 0.001));
178 179

    await tester.pump(kThemeChangeDuration ~/ 2);
180
    final RenderPhysicalShape modelD = getShadow(tester);
181
    expect(modelD.elevation, isNot(closeTo(0.0, 0.001)));
182 183

    await tester.pump(kThemeChangeDuration);
184
    final RenderPhysicalShape modelE = getShadow(tester);
185
    expect(modelE.elevation, equals(9.0));
186
  });
187 188

  testWidgets('Shadow colors animate smoothly', (WidgetTester tester) async {
189
    // This code verifies that the PhysicalModel's shadowColor animates over
190 191 192
    // a kThemeChangeDuration time interval.

    await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFF00FF00)));
193
    final RenderPhysicalShape modelA = getShadow(tester);
194 195 196
    expect(modelA.shadowColor, equals(const Color(0xFF00FF00)));

    await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFFFF0000)));
197
    final RenderPhysicalShape modelB = getShadow(tester);
198 199 200
    expect(modelB.shadowColor, equals(const Color(0xFF00FF00)));

    await tester.pump(const Duration(milliseconds: 1));
201
    final RenderPhysicalShape modelC = getShadow(tester);
202
    expect(modelC.shadowColor, within<Color>(distance: 1, from: const Color(0xFF00FF00)));
203 204

    await tester.pump(kThemeChangeDuration ~/ 2);
205
    final RenderPhysicalShape modelD = getShadow(tester);
206
    expect(modelD.shadowColor, isNot(within<Color>(distance: 1, from: const Color(0xFF00FF00))));
207 208

    await tester.pump(kThemeChangeDuration);
209
    final RenderPhysicalShape modelE = getShadow(tester);
210 211
    expect(modelE.shadowColor, equals(const Color(0xFFFF0000)));
  });
212 213

  group('Transparency clipping', () {
214
    testWidgets('No clip by default', (WidgetTester tester) async {
215
      final GlobalKey materialKey = GlobalKey();
216
      await tester.pumpWidget(
217
          Material(
218 219 220 221 222 223 224
            key: materialKey,
            type: MaterialType.transparency,
            child: const SizedBox(width: 100.0, height: 100.0),
          )
      );

      expect(find.byKey(materialKey), hasNoImmediateClip);
225
    });
226 227

    testWidgets('clips to bounding rect by default given Clip.antiAlias', (WidgetTester tester) async {
228
      final GlobalKey materialKey = GlobalKey();
229
      await tester.pumpWidget(
230
        Material(
231 232
          key: materialKey,
          type: MaterialType.transparency,
233 234
          child: const SizedBox(width: 100.0, height: 100.0),
          clipBehavior: Clip.antiAlias,
235 236 237
        )
      );

238
      expect(find.byKey(materialKey), clipsWithBoundingRect);
239 240
    });

241
    testWidgets('clips to rounded rect when borderRadius provided given Clip.antiAlias', (WidgetTester tester) async {
242
      final GlobalKey materialKey = GlobalKey();
243
      await tester.pumpWidget(
244
        Material(
245 246
          key: materialKey,
          type: MaterialType.transparency,
247
          borderRadius: const BorderRadius.all(Radius.circular(10.0)),
248 249
          child: const SizedBox(width: 100.0, height: 100.0),
          clipBehavior: Clip.antiAlias,
250 251 252 253 254 255
        )
      );

      expect(
        find.byKey(materialKey),
        clipsWithBoundingRRect(
256
          borderRadius: const BorderRadius.all(Radius.circular(10.0))
257 258 259
        ),
      );
    });
260

261
    testWidgets('clips to shape when provided given Clip.antiAlias', (WidgetTester tester) async {
262
      final GlobalKey materialKey = GlobalKey();
263
      await tester.pumpWidget(
264
        Material(
265 266 267
          key: materialKey,
          type: MaterialType.transparency,
          shape: const StadiumBorder(),
268 269
          child: const SizedBox(width: 100.0, height: 100.0),
          clipBehavior: Clip.antiAlias,
270 271 272 273 274 275 276 277 278 279
        )
      );

      expect(
        find.byKey(materialKey),
        clipsWithShapeBorder(
          shape: const StadiumBorder(),
        ),
      );
    });
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342

    testWidgets('supports directional clips', (WidgetTester tester) async {
      final List<String> logs = <String>[];
      final ShapeBorder shape = TestBorder((String message) { logs.add(message); });
      Widget buildMaterial() {
        return Material(
          type: MaterialType.transparency,
          shape: shape,
          child: const SizedBox(width: 100.0, height: 100.0),
          clipBehavior: Clip.antiAlias,
        );
      }
      final Widget material = buildMaterial();
      // verify that a regular clip works as one would expect
      logs.add('--0');
      await tester.pumpWidget(material);
      // verify that pumping again doesn't recompute the clip
      // even though the widget itself is new (the shape doesn't change identity)
      logs.add('--1');
      await tester.pumpWidget(buildMaterial());
      // verify that Material passes the TextDirection on to its shape when it's transparent
      logs.add('--2');
      await tester.pumpWidget(Directionality(
        textDirection: TextDirection.ltr,
        child: material,
      ));
      // verify that changing the text direction from LTR to RTL has an effect
      // even though the widget itself is identical
      logs.add('--3');
      await tester.pumpWidget(Directionality(
        textDirection: TextDirection.rtl,
        child: material,
      ));
      // verify that pumping again with a text direction has no effect
      logs.add('--4');
      await tester.pumpWidget(Directionality(
        textDirection: TextDirection.rtl,
        child: buildMaterial(),
      ));
      logs.add('--5');
      // verify that changing the text direction and the widget at the same time
      // works as expected
      await tester.pumpWidget(Directionality(
        textDirection: TextDirection.ltr,
        child: material,
      ));
      expect(logs, <String>[
        '--0',
        'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) null',
        'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) null',
        '--1',
        '--2',
        'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
        'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
        '--3',
        'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.rtl',
        'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.rtl',
        '--4',
        '--5',
        'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
        'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
      ]);
    });
343 344 345 346
  });

  group('PhysicalModels', () {
    testWidgets('canvas', (WidgetTester tester) async {
347
      final GlobalKey materialKey = GlobalKey();
348
      await tester.pumpWidget(
349
        Material(
350 351
          key: materialKey,
          type: MaterialType.canvas,
352
          child: const SizedBox(width: 100.0, height: 100.0),
353 354 355 356 357 358 359 360 361 362 363
        )
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
          borderRadius: BorderRadius.zero,
          elevation: 0.0,
      ));
    });

    testWidgets('canvas with borderRadius and elevation', (WidgetTester tester) async {
364
      final GlobalKey materialKey = GlobalKey();
365
      await tester.pumpWidget(
366
        Material(
367 368
          key: materialKey,
          type: MaterialType.canvas,
369
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
370 371 372 373 374 375 376
          child: const SizedBox(width: 100.0, height: 100.0),
          elevation: 1.0,
        )
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
377
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
378 379 380 381
          elevation: 1.0,
      ));
    });

382
    testWidgets('canvas with shape and elevation', (WidgetTester tester) async {
383
      final GlobalKey materialKey = GlobalKey();
384
      await tester.pumpWidget(
385
        Material(
386 387 388 389 390 391 392 393 394 395 396 397 398 399
          key: materialKey,
          type: MaterialType.canvas,
          shape: const StadiumBorder(),
          child: const SizedBox(width: 100.0, height: 100.0),
          elevation: 1.0,
        )
      );

      expect(find.byKey(materialKey), rendersOnPhysicalShape(
          shape: const StadiumBorder(),
          elevation: 1.0,
      ));
    });

400
    testWidgets('card', (WidgetTester tester) async {
401
      final GlobalKey materialKey = GlobalKey();
402
      await tester.pumpWidget(
403
        Material(
404 405 406 407 408 409 410 411
          key: materialKey,
          type: MaterialType.card,
          child: const SizedBox(width: 100.0, height: 100.0),
        )
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
412
          borderRadius: const BorderRadius.all(Radius.circular(2.0)),
413 414 415 416 417
          elevation: 0.0,
      ));
    });

    testWidgets('card with borderRadius and elevation', (WidgetTester tester) async {
418
      final GlobalKey materialKey = GlobalKey();
419
      await tester.pumpWidget(
420
        Material(
421 422
          key: materialKey,
          type: MaterialType.card,
423
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
424 425 426 427 428 429 430
          elevation: 5.0,
          child: const SizedBox(width: 100.0, height: 100.0),
        )
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
431
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
432 433 434 435
          elevation: 5.0,
      ));
    });

436
    testWidgets('card with shape and elevation', (WidgetTester tester) async {
437
      final GlobalKey materialKey = GlobalKey();
438
      await tester.pumpWidget(
439
        Material(
440 441 442 443 444 445 446 447 448 449 450 451 452 453
          key: materialKey,
          type: MaterialType.card,
          shape: const StadiumBorder(),
          elevation: 5.0,
          child: const SizedBox(width: 100.0, height: 100.0),
        )
      );

      expect(find.byKey(materialKey), rendersOnPhysicalShape(
          shape: const StadiumBorder(),
          elevation: 5.0,
      ));
    });

454
    testWidgets('circle', (WidgetTester tester) async {
455
      final GlobalKey materialKey = GlobalKey();
456
      await tester.pumpWidget(
457
        Material(
458 459 460 461 462 463 464 465 466 467 468 469 470 471
          key: materialKey,
          type: MaterialType.circle,
          child: const SizedBox(width: 100.0, height: 100.0),
          color: const Color(0xFF0000FF),
        )
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.circle,
          elevation: 0.0,
      ));
    });

    testWidgets('button', (WidgetTester tester) async {
472
      final GlobalKey materialKey = GlobalKey();
473
      await tester.pumpWidget(
474
        Material(
475 476 477 478 479 480 481 482 483
          key: materialKey,
          type: MaterialType.button,
          child: const SizedBox(width: 100.0, height: 100.0),
          color: const Color(0xFF0000FF),
        )
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
484
          borderRadius: const BorderRadius.all(Radius.circular(2.0)),
485 486 487 488 489
          elevation: 0.0,
      ));
    });

    testWidgets('button with elevation and borderRadius', (WidgetTester tester) async {
490
      final GlobalKey materialKey = GlobalKey();
491
      await tester.pumpWidget(
492
        Material(
493 494 495 496
          key: materialKey,
          type: MaterialType.button,
          child: const SizedBox(width: 100.0, height: 100.0),
          color: const Color(0xFF0000FF),
497
          borderRadius: const BorderRadius.all(Radius.circular(6.0)),
498 499 500 501 502 503
          elevation: 4.0,
        )
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
504
          borderRadius: const BorderRadius.all(Radius.circular(6.0)),
505 506 507
          elevation: 4.0,
      ));
    });
508 509

    testWidgets('button with elevation and shape', (WidgetTester tester) async {
510
      final GlobalKey materialKey = GlobalKey();
511
      await tester.pumpWidget(
512
        Material(
513 514 515 516 517 518 519 520 521 522 523 524 525 526
          key: materialKey,
          type: MaterialType.button,
          child: const SizedBox(width: 100.0, height: 100.0),
          color: const Color(0xFF0000FF),
          shape: const StadiumBorder(),
          elevation: 4.0,
        )
      );

      expect(find.byKey(materialKey), rendersOnPhysicalShape(
          shape: const StadiumBorder(),
          elevation: 4.0,
      ));
    });
527
  });
528 529 530

  group('Border painting', () {
    testWidgets('border is painted on physical layers', (WidgetTester tester) async {
531
      final GlobalKey materialKey = GlobalKey();
532
      await tester.pumpWidget(
533
        Material(
534 535 536 537 538
          key: materialKey,
          type: MaterialType.button,
          child: const SizedBox(width: 100.0, height: 100.0),
          color: const Color(0xFF0000FF),
          shape: const CircleBorder(
539
            side: BorderSide(
540
              width: 2.0,
541
              color: Color(0xFF0000FF),
542
            ),
543 544 545 546 547 548 549 550 551
          ),
        )
      );

      final RenderBox box = tester.renderObject(find.byKey(materialKey));
      expect(box, paints..circle());
    });

    testWidgets('border is painted for transparent material', (WidgetTester tester) async {
552
      final GlobalKey materialKey = GlobalKey();
553
      await tester.pumpWidget(
554
        Material(
555 556 557 558
          key: materialKey,
          type: MaterialType.transparency,
          child: const SizedBox(width: 100.0, height: 100.0),
          shape: const CircleBorder(
559
            side: BorderSide(
560
              width: 2.0,
561
              color: Color(0xFF0000FF),
562
            ),
563 564 565 566 567 568 569 570 571
          ),
        )
      );

      final RenderBox box = tester.renderObject(find.byKey(materialKey));
      expect(box, paints..circle());
    });

    testWidgets('border is not painted for when border side is none', (WidgetTester tester) async {
572
      final GlobalKey materialKey = GlobalKey();
573
      await tester.pumpWidget(
574
        Material(
575 576 577 578 579 580 581 582 583 584
          key: materialKey,
          type: MaterialType.transparency,
          child: const SizedBox(width: 100.0, height: 100.0),
          shape: const CircleBorder(),
        )
      );

      final RenderBox box = tester.renderObject(find.byKey(materialKey));
      expect(box, isNot(paints..circle()));
    });
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608

    testWidgets('border is painted above child by default', (WidgetTester tester) async {
      final Key painterKey = UniqueKey();

      await tester.pumpWidget(MaterialApp(
        home: Scaffold(
          body: RepaintBoundary(
            key: painterKey,
            child: Card(
              child: SizedBox(
                width: 200,
                height: 300,
                child: Material(
                  clipBehavior: Clip.hardEdge,
                  elevation: 0,
                  shape: RoundedRectangleBorder(
                    side: const BorderSide(color: Colors.grey, width: 6),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Column(
                    children: <Widget>[
                      Container(
                        color: Colors.green,
                        height: 150,
609
                      ),
610 611 612 613
                    ],
                  ),
                ),
              ),
614 615
            ),
          ),
616 617 618 619 620
        ),
      ));

      await expectLater(
        find.byKey(painterKey),
621 622
        matchesGoldenFile('material.border_paint_above.png'),
        skip: !Platform.isLinux,
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
      );
    });

    testWidgets('border is painted below child when specified', (WidgetTester tester) async {
      final Key painterKey = UniqueKey();

      await tester.pumpWidget(MaterialApp(
        home: Scaffold(
          body: RepaintBoundary(
            key: painterKey,
            child: Card(
              child: SizedBox(
                width: 200,
                height: 300,
                child: Material(
                  clipBehavior: Clip.hardEdge,
                  elevation: 0,
                  shape: RoundedRectangleBorder(
                    side: const BorderSide(color: Colors.grey, width: 6),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  borderOnForeground: false,
                  child: Column(
                    children: <Widget>[
                      Container(
                        color: Colors.green,
                        height: 150,
650
                      ),
651 652 653 654
                    ],
                  ),
                ),
              ),
655 656
            ),
          ),
657 658 659 660 661
        ),
      ));

      await expectLater(
        find.byKey(painterKey),
662 663
        matchesGoldenFile('material.border_paint_below.png'),
        skip: !Platform.isLinux,
664 665
      );
    });
666
  });
667
}