transitions_test.dart 19.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
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
import 'dart:math' as math;

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

void main() {
  testWidgets('toString control test', (WidgetTester tester) async {
13
    const Widget widget = FadeTransition(
14
      opacity: kAlwaysCompleteAnimation,
15
      child: Text('Ready', textDirection: TextDirection.ltr),
16 17 18
    );
    expect(widget.toString, isNot(throwsException));
  });
19

20
  group('DecoratedBoxTransition test', () {
21 22
    final DecorationTween decorationTween = DecorationTween(
      begin: BoxDecoration(
23
        color: const Color(0xFFFFFFFF),
24
        border: Border.all(
25 26 27
          width: 4.0,
        ),
        borderRadius: BorderRadius.zero,
28 29 30 31 32 33 34
        boxShadow: const <BoxShadow>[
          BoxShadow(
            color: Color(0x66000000),
            blurRadius: 10.0,
            spreadRadius: 4.0,
          ),
        ],
35
      ),
36
      end: BoxDecoration(
37
        color: const Color(0xFF000000),
38
        border: Border.all(
39 40
          color: const Color(0xFF202020),
        ),
41
        borderRadius: const BorderRadius.all(Radius.circular(10.0)),
42 43 44 45
        // No shadow.
      ),
    );

46
    late AnimationController controller;
47 48

    setUp(() {
49
      controller = AnimationController(vsync: const TestVSync());
50 51
    });

52 53 54 55 56
    testWidgets('decoration test', (WidgetTester tester) async {
      final DecoratedBoxTransition transitionUnderTest =
      DecoratedBoxTransition(
        decoration: decorationTween.animate(controller),
        child: const Text(
57
          "Doesn't matter",
58 59 60 61 62 63 64 65 66
          textDirection: TextDirection.ltr,
        ),
      );

      await tester.pumpWidget(transitionUnderTest);
      RenderDecoratedBox actualBox = tester.renderObject(find.byType(DecoratedBox));
      BoxDecoration actualDecoration = actualBox.decoration as BoxDecoration;

      expect(actualDecoration.color, const Color(0xFFFFFFFF));
67 68 69
      expect(actualDecoration.boxShadow![0].blurRadius, 10.0);
      expect(actualDecoration.boxShadow![0].spreadRadius, 4.0);
      expect(actualDecoration.boxShadow![0].color, const Color(0x66000000));
70 71 72 73 74 75 76 77

      controller.value = 0.5;

      await tester.pump();
      actualBox = tester.renderObject(find.byType(DecoratedBox));
      actualDecoration = actualBox.decoration as BoxDecoration;

      expect(actualDecoration.color, const Color(0xFF7F7F7F));
Dan Field's avatar
Dan Field committed
78
      expect(actualDecoration.border, isA<Border>());
79
      final Border border = actualDecoration.border! as Border;
80 81 82
      expect(border.left.width, 2.5);
      expect(border.left.style, BorderStyle.solid);
      expect(border.left.color, const Color(0xFF101010));
83
      expect(actualDecoration.borderRadius, const BorderRadius.all(Radius.circular(5.0)));
84
      expect(actualDecoration.shape, BoxShape.rectangle);
85 86
      expect(actualDecoration.boxShadow![0].blurRadius, 5.0);
      expect(actualDecoration.boxShadow![0].spreadRadius, 2.0);
87
      // Scaling a shadow doesn't change the color.
88
      expect(actualDecoration.boxShadow![0].color, const Color(0x66000000));
89 90 91 92 93 94 95 96 97 98

      controller.value = 1.0;

      await tester.pump();
      actualBox = tester.renderObject(find.byType(DecoratedBox));
      actualDecoration = actualBox.decoration as BoxDecoration;

      expect(actualDecoration.color, const Color(0xFF000000));
      expect(actualDecoration.boxShadow, null);
    });
99 100

    testWidgets('animations work with curves test', (WidgetTester tester) async {
101
      final Animation<Decoration> curvedDecorationAnimation =
102 103 104 105 106 107 108 109 110
        decorationTween.animate(CurvedAnimation(
        parent: controller,
        curve: Curves.easeOut,
      ));

      final DecoratedBoxTransition transitionUnderTest = DecoratedBoxTransition(
        decoration: curvedDecorationAnimation,
        position: DecorationPosition.foreground,
        child: const Text(
111
          "Doesn't matter",
112 113 114
          textDirection: TextDirection.ltr,
        ),
      );
115 116

      await tester.pumpWidget(transitionUnderTest);
117 118

      RenderDecoratedBox actualBox = tester.renderObject(find.byType(DecoratedBox));
119
      BoxDecoration actualDecoration = actualBox.decoration as BoxDecoration;
120

121
      expect(actualDecoration.color, const Color(0xFFFFFFFF));
122 123 124
      expect(actualDecoration.boxShadow![0].blurRadius, 10.0);
      expect(actualDecoration.boxShadow![0].spreadRadius, 4.0);
      expect(actualDecoration.boxShadow![0].color, const Color(0x66000000));
125 126 127 128 129

      controller.value = 0.5;

      await tester.pump();
      actualBox = tester.renderObject(find.byType(DecoratedBox));
130
      actualDecoration = actualBox.decoration as BoxDecoration;
131

132
      // Same as the test above but the values should be much closer to the
133
      // tween's end values given the easeOut curve.
134
      expect(actualDecoration.color, const Color(0xFF505050));
Dan Field's avatar
Dan Field committed
135
      expect(actualDecoration.border, isA<Border>());
136
      final Border border = actualDecoration.border! as Border;
137
      expect(border.left.width, moreOrLessEquals(1.9, epsilon: 0.1));
Ian Hickson's avatar
Ian Hickson committed
138 139
      expect(border.left.style, BorderStyle.solid);
      expect(border.left.color, const Color(0xFF151515));
140
      expect(actualDecoration.borderRadius!.resolve(TextDirection.ltr).topLeft.x, moreOrLessEquals(6.8, epsilon: 0.1));
141
      expect(actualDecoration.shape, BoxShape.rectangle);
142 143
      expect(actualDecoration.boxShadow![0].blurRadius, moreOrLessEquals(3.1, epsilon: 0.1));
      expect(actualDecoration.boxShadow![0].spreadRadius, moreOrLessEquals(1.2, epsilon: 0.1));
144
      // Scaling a shadow doesn't change the color.
145
      expect(actualDecoration.boxShadow![0].color, const Color(0x66000000));
146 147
    });
  });
148 149

  testWidgets('AlignTransition animates', (WidgetTester tester) async {
150 151
    final AnimationController controller = AnimationController(vsync: const TestVSync());
    final Animation<Alignment> alignmentTween = AlignmentTween(
152 153
      begin: Alignment.centerLeft,
      end: Alignment.bottomRight,
154
    ).animate(controller);
155
    final Widget widget = AlignTransition(
156 157 158 159 160 161 162 163
      alignment: alignmentTween,
      child: const Text('Ready', textDirection: TextDirection.ltr),
    );

    await tester.pumpWidget(widget);

    final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align));

164
    Alignment actualAlignment = actualPositionedBox.alignment as Alignment;
165
    expect(actualAlignment, Alignment.centerLeft);
166 167 168

    controller.value = 0.5;
    await tester.pump();
169
    actualAlignment = actualPositionedBox.alignment as Alignment;
170 171 172
    expect(actualAlignment, const Alignment(0.0, 0.5));
  });

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
  testWidgets('RelativePositionedTransition animates', (WidgetTester tester) async {
    final AnimationController controller = AnimationController(vsync: const TestVSync());
    final Animation<Rect?> rectTween = RectTween(
      begin: const Rect.fromLTWH(0, 0, 30, 40),
      end: const Rect.fromLTWH(100, 200, 100, 200),
    ).animate(controller);
    final Widget widget = Directionality(
      textDirection: TextDirection.rtl,
      child: Stack(
        alignment: Alignment.centerLeft,
        children: <Widget>[
          RelativePositionedTransition(
            size: const Size(200, 300),
            rect: rectTween,
            child: const Placeholder(),
          ),
        ],
      ),
    );

    await tester.pumpWidget(widget);

    final Positioned actualPositioned = tester.widget(find.byType(Positioned));
    final RenderBox renderBox = tester.renderObject(find.byType(Placeholder));

    Rect actualRect = Rect.fromLTRB(
      actualPositioned.left!,
      actualPositioned.top!,
      actualPositioned.right ?? 0.0,
      actualPositioned.bottom ?? 0.0,
    );
    expect(actualRect, equals(const Rect.fromLTRB(0, 0, 170, 260)));
    expect(renderBox.size, equals(const Size(630, 340)));

    controller.value = 0.5;
    await tester.pump();
    actualRect = Rect.fromLTRB(
      actualPositioned.left!,
      actualPositioned.top!,
      actualPositioned.right ?? 0.0,
      actualPositioned.bottom ?? 0.0,
    );
    expect(actualRect, equals(const Rect.fromLTWH(0, 0, 170, 260)));
    expect(renderBox.size, equals(const Size(665, 420)));
  });

219
  testWidgets('AlignTransition keeps width and height factors', (WidgetTester tester) async {
220 221
    final AnimationController controller = AnimationController(vsync: const TestVSync());
    final Animation<Alignment> alignmentTween = AlignmentTween(
222 223
      begin: Alignment.centerLeft,
      end: Alignment.bottomRight,
224
    ).animate(controller);
225
    final Widget widget = AlignTransition(
226 227 228
      alignment: alignmentTween,
      widthFactor: 0.3,
      heightFactor: 0.4,
229
      child: const Text('Ready', textDirection: TextDirection.ltr),
230 231 232 233 234 235 236 237 238
    );

    await tester.pumpWidget(widget);

    final Align actualAlign = tester.widget(find.byType(Align));

    expect(actualAlign.widthFactor, 0.3);
    expect(actualAlign.heightFactor, 0.4);
  });
239 240

  testWidgets('SizeTransition clamps negative size factors - vertical axis', (WidgetTester tester) async {
241 242
    final AnimationController controller = AnimationController(vsync: const TestVSync());
    final Animation<double> animation = Tween<double>(begin: -1.0, end: 1.0).animate(controller);
243

244
    final Widget widget =  Directionality(
245 246 247 248 249 250
      textDirection: TextDirection.ltr,
      child: SizeTransition(
        sizeFactor: animation,
        child: const Text('Ready'),
      ),
    );
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270

    await tester.pumpWidget(widget);

    final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align));
    expect(actualPositionedBox.heightFactor, 0.0);

    controller.value = 0.0;
    await tester.pump();
    expect(actualPositionedBox.heightFactor, 0.0);

    controller.value = 0.75;
    await tester.pump();
    expect(actualPositionedBox.heightFactor, 0.5);

    controller.value = 1.0;
    await tester.pump();
    expect(actualPositionedBox.heightFactor, 1.0);
  });

  testWidgets('SizeTransition clamps negative size factors - horizontal axis', (WidgetTester tester) async {
271 272
    final AnimationController controller = AnimationController(vsync: const TestVSync());
    final Animation<double> animation = Tween<double>(begin: -1.0, end: 1.0).animate(controller);
273

274
    final Widget widget =  Directionality(
275 276 277 278 279 280 281
      textDirection: TextDirection.ltr,
      child: SizeTransition(
        axis: Axis.horizontal,
        sizeFactor: animation,
        child: const Text('Ready'),
      ),
    );
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299

    await tester.pumpWidget(widget);

    final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align));
    expect(actualPositionedBox.widthFactor, 0.0);

    controller.value = 0.0;
    await tester.pump();
    expect(actualPositionedBox.widthFactor, 0.0);

    controller.value = 0.75;
    await tester.pump();
    expect(actualPositionedBox.widthFactor, 0.5);

    controller.value = 1.0;
    await tester.pump();
    expect(actualPositionedBox.widthFactor, 1.0);
  });
300 301

  testWidgets('RotationTransition animates', (WidgetTester tester) async {
302 303
    final AnimationController controller = AnimationController(vsync: const TestVSync());
    final Widget widget = RotationTransition(
304 305
      alignment: Alignment.topRight,
      turns: controller,
306 307 308 309
      child: const Text(
        'Rotation',
        textDirection: TextDirection.ltr,
      ),
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
    );

    await tester.pumpWidget(widget);
    Transform actualRotatedBox = tester.widget(find.byType(Transform));
    Matrix4 actualTurns = actualRotatedBox.transform;
    expect(actualTurns, equals(Matrix4.rotationZ(0.0)));

    controller.value = 0.5;
    await tester.pump();
    actualRotatedBox = tester.widget(find.byType(Transform));
    actualTurns = actualRotatedBox.transform;
    expect(actualTurns, Matrix4.rotationZ(math.pi));

    controller.value = 0.75;
    await tester.pump();
    actualRotatedBox = tester.widget(find.byType(Transform));
    actualTurns = actualRotatedBox.transform;
    expect(actualTurns, Matrix4.rotationZ(math.pi * 1.5));
  });

330
  testWidgets('RotationTransition maintains chosen alignment during animation', (WidgetTester tester) async {
331 332
    final AnimationController controller = AnimationController(vsync: const TestVSync());
    final Widget widget = RotationTransition(
333 334 335 336 337 338
      alignment: Alignment.topRight,
      turns: controller,
      child: const Text('Rotation', textDirection: TextDirection.ltr),
    );

    await tester.pumpWidget(widget);
339
    RotationTransition actualRotatedBox = tester.widget(find.byType(RotationTransition));
340
    Alignment actualAlignment = actualRotatedBox.alignment;
341
    expect(actualAlignment, Alignment.topRight);
342 343 344 345 346

    controller.value = 0.5;
    await tester.pump();
    actualRotatedBox = tester.widget(find.byType(RotationTransition));
    actualAlignment = actualRotatedBox.alignment;
347
    expect(actualAlignment, Alignment.topRight);
348
  });
349 350

  group('FadeTransition', () {
351
    double getOpacity(WidgetTester tester, String textValue) {
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
      final FadeTransition opacityWidget = tester.widget<FadeTransition>(
        find.ancestor(
          of: find.text(textValue),
          matching: find.byType(FadeTransition),
        ).first,
      );
      return opacityWidget.opacity.value;
    }
    testWidgets('animates', (WidgetTester tester) async {
      final AnimationController controller = AnimationController(vsync: const TestVSync());
      final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
      final Widget widget =  Directionality(
        textDirection: TextDirection.ltr,
        child: FadeTransition(
          opacity: animation,
          child: const Text('Fade In'),
        ),
      );

      await tester.pumpWidget(widget);

373
      expect(getOpacity(tester, 'Fade In'), 0.0);
374 375 376

      controller.value = 0.25;
      await tester.pump();
377
      expect(getOpacity(tester, 'Fade In'), 0.25);
378 379 380

      controller.value = 0.5;
      await tester.pump();
381
      expect(getOpacity(tester, 'Fade In'), 0.5);
382 383 384

      controller.value = 0.75;
      await tester.pump();
385
      expect(getOpacity(tester, 'Fade In'), 0.75);
386 387 388

      controller.value = 1.0;
      await tester.pump();
389
      expect(getOpacity(tester, 'Fade In'), 1.0);
390 391 392 393
    });
  });

  group('SliverFadeTransition', () {
394
    double getOpacity(WidgetTester tester, String textValue) {
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
      final SliverFadeTransition opacityWidget = tester.widget<SliverFadeTransition>(
        find.ancestor(
          of: find.text(textValue),
          matching: find.byType(SliverFadeTransition),
        ).first,
      );
      return opacityWidget.opacity.value;
    }
    testWidgets('animates', (WidgetTester tester) async {
      final AnimationController controller = AnimationController(vsync: const TestVSync());
      final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
      final Widget widget = Localizations(
        locale: const Locale('en', 'us'),
        delegates: const <LocalizationsDelegate<dynamic>>[
          DefaultWidgetsLocalizations.delegate,
          DefaultMaterialLocalizations.delegate,
        ],
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: MediaQuery(
            data: const MediaQueryData(),
            child: CustomScrollView(
              slivers: <Widget>[
                SliverFadeTransition(
                  opacity: animation,
                  sliver: const SliverToBoxAdapter(
                    child: Text('Fade In'),
                  ),
                ),
424 425 426 427
              ],
            ),
          ),
        ),
428 429 430 431
      );

      await tester.pumpWidget(widget);

432
      expect(getOpacity(tester, 'Fade In'), 0.0);
433 434 435

      controller.value = 0.25;
      await tester.pump();
436
      expect(getOpacity(tester, 'Fade In'), 0.25);
437 438 439

      controller.value = 0.5;
      await tester.pump();
440
      expect(getOpacity(tester, 'Fade In'), 0.5);
441 442 443

      controller.value = 0.75;
      await tester.pump();
444
      expect(getOpacity(tester, 'Fade In'), 0.75);
445 446 447

      controller.value = 1.0;
      await tester.pump();
448
      expect(getOpacity(tester, 'Fade In'), 1.0);
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 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 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 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556

  group('ScaleTransition', () {
    testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async {
      final AnimationController controller = AnimationController(vsync: const TestVSync());
      final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
      final Widget widget =  Directionality(
        textDirection: TextDirection.ltr,
        child: ScaleTransition(
          scale: animation,
          filterQuality: FilterQuality.none,
          child: const Text('Scale Transition'),
        ),
      );

      await tester.pumpWidget(widget);

      // Validate that expensive layer is not left in tree before animation has started.
      expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));

      controller.value = 0.25;
      await tester.pump();

      expect(tester.layers, contains(isA<ImageFilterLayer>().having(
        (ImageFilterLayer layer) => layer.imageFilter.toString(),
        'image filter',
        startsWith('ImageFilter.matrix('),
      )));

      controller.value = 0.5;
      await tester.pump();

      expect(tester.layers, contains(isA<ImageFilterLayer>().having(
        (ImageFilterLayer layer) => layer.imageFilter.toString(),
        'image filter',
        startsWith('ImageFilter.matrix('),
      )));

      controller.value = 0.75;
      await tester.pump();

      expect(tester.layers, contains(isA<ImageFilterLayer>().having(
        (ImageFilterLayer layer) => layer.imageFilter.toString(),
        'image filter',
        startsWith('ImageFilter.matrix('),
      )));

      controller.value = 1;
      await tester.pump();

      // Validate that expensive layer is not left in tree after animation has finished.
      expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
    });
  });

  group('RotationTransition', () {
    testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async {
      final AnimationController controller = AnimationController(vsync: const TestVSync());
      final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
      final Widget widget =  Directionality(
        textDirection: TextDirection.ltr,
        child: RotationTransition(
          turns: animation,
          filterQuality: FilterQuality.none,
          child: const Text('Scale Transition'),
        ),
      );

      await tester.pumpWidget(widget);

      // Validate that expensive layer is not left in tree before animation has started.
      expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));

      controller.value = 0.25;
      await tester.pump();

      expect(tester.layers, contains(isA<ImageFilterLayer>().having(
        (ImageFilterLayer layer) => layer.imageFilter.toString(),
        'image filter',
        startsWith('ImageFilter.matrix('),
      )));

      controller.value = 0.5;
      await tester.pump();

      expect(tester.layers, contains(isA<ImageFilterLayer>().having(
        (ImageFilterLayer layer) => layer.imageFilter.toString(),
        'image filter',
        startsWith('ImageFilter.matrix('),
      )));

      controller.value = 0.75;
      await tester.pump();

      expect(tester.layers, contains(isA<ImageFilterLayer>().having(
        (ImageFilterLayer layer) => layer.imageFilter.toString(),
        'image filter',
        startsWith('ImageFilter.matrix('),
      )));

      controller.value = 1;
      await tester.pump();

      // Validate that expensive layer is not left in tree after animation has finished.
      expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
    });
  });
557
}