decoration_test.dart 19 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
@TestOn('!chrome')
6
import 'dart:async';
7
import 'dart:typed_data';
8
import 'dart:ui' as ui show Image, ImageByteFormat, ColorFilter;
9 10

import 'package:flutter/foundation.dart';
11
import 'package:flutter/painting.dart';
12
import 'package:quiver/testing/async.dart';
13

14
import '../flutter_test_alternative.dart';
15 16
import '../painting/mocks_for_image_cache.dart';
import '../rendering/rendering_tester.dart';
17 18

class TestCanvas implements Canvas {
19 20 21 22
  TestCanvas([this.invocations]);

  final List<Invocation> invocations;

23
  @override
24 25 26
  void noSuchMethod(Invocation invocation) {
    invocations?.add(invocation);
  }
27 28 29 30 31
}

class SynchronousTestImageProvider extends ImageProvider<int> {
  @override
  Future<int> obtainKey(ImageConfiguration configuration) {
32
    return SynchronousFuture<int>(1);
33 34 35
  }

  @override
36
  ImageStreamCompleter load(int key, DecoderCallback decode) {
37 38
    return OneFrameImageStreamCompleter(
      SynchronousFuture<ImageInfo>(TestImageInfo(key, image: TestImage(), scale: 1.0))
39 40 41 42 43 44 45
    );
  }
}

class AsyncTestImageProvider extends ImageProvider<int> {
  @override
  Future<int> obtainKey(ImageConfiguration configuration) {
46
    return Future<int>.value(2);
47 48 49
  }

  @override
50
  ImageStreamCompleter load(int key, DecoderCallback decode) {
51 52
    return OneFrameImageStreamCompleter(
      Future<ImageInfo>.value(TestImageInfo(key))
53 54 55
    );
  }
}
56

57
class DelayedImageProvider extends ImageProvider<DelayedImageProvider> {
58
  final Completer<ImageInfo> _completer = Completer<ImageInfo>();
59 60

  @override
61
  Future<DelayedImageProvider> obtainKey(ImageConfiguration configuration) {
62
    return SynchronousFuture<DelayedImageProvider>(this);
63 64 65
  }

  @override
66
  ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) {
67
    return OneFrameImageStreamCompleter(_completer.future);
68 69 70
  }

  void complete() {
71
    _completer.complete(ImageInfo(image: TestImage()));
72 73 74
  }

  @override
75
  String toString() => '${describeIdentity(this)}()';
76 77
}

78
class TestImage implements ui.Image {
79 80 81 82 83 84 85 86
  @override
  int get width => 100;

  @override
  int get height => 100;

  @override
  void dispose() { }
87 88

  @override
89
  Future<ByteData> toByteData({ ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba }) async {
90
    throw UnsupportedError('Cannot encode test image');
91
  }
92 93
}

94
void main() {
95
  TestRenderingFlutterBinding(); // initializes the imageCache
96

97
  test('Decoration.lerp()', () {
98 99
    const BoxDecoration a = BoxDecoration(color: Color(0xFFFFFFFF));
    const BoxDecoration b = BoxDecoration(color: Color(0x00000000));
100 101

    BoxDecoration c = Decoration.lerp(a, b, 0.0);
102
    expect(c.color, equals(a.color));
103 104

    c = Decoration.lerp(a, b, 0.25);
105
    expect(c.color, equals(Color.lerp(const Color(0xFFFFFFFF), const Color(0x00000000), 0.25)));
106 107

    c = Decoration.lerp(a, b, 1.0);
108
    expect(c.color, equals(b.color));
109
  });
110

111
  test('BoxDecorationImageListenerSync', () {
112 113
    final ImageProvider imageProvider = SynchronousTestImageProvider();
    final DecorationImage backgroundImage = DecorationImage(image: imageProvider);
114

115
    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
116
    bool onChangedCalled = false;
117
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
118 119 120
      onChangedCalled = true;
    });

121
    final TestCanvas canvas = TestCanvas();
122
    const ImageConfiguration imageConfiguration = ImageConfiguration(size: Size.zero);
123 124 125 126 127 128
    boxPainter.paint(canvas, Offset.zero, imageConfiguration);

    // The onChanged callback should not be invoked during the call to boxPainter.paint
    expect(onChangedCalled, equals(false));
  });

129
  test('BoxDecorationImageListenerAsync', () {
130 131 132
    FakeAsync().run((FakeAsync async) {
      final ImageProvider imageProvider = AsyncTestImageProvider();
      final DecorationImage backgroundImage = DecorationImage(image: imageProvider);
133

134
      final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
135
      bool onChangedCalled = false;
136
      final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
137 138 139
        onChangedCalled = true;
      });

140
      final TestCanvas canvas = TestCanvas();
141
      const ImageConfiguration imageConfiguration = ImageConfiguration(size: Size.zero);
142 143 144 145 146 147 148 149
      boxPainter.paint(canvas, Offset.zero, imageConfiguration);

      // The onChanged callback should be invoked asynchronously.
      expect(onChangedCalled, equals(false));
      async.flushMicrotasks();
      expect(onChangedCalled, equals(true));
    });
  });
150 151 152

  // Regression test for https://github.com/flutter/flutter/issues/7289.
  // A reference test would be better.
153
  test('BoxDecoration backgroundImage clip', () {
154
    void testDecoration({ BoxShape shape = BoxShape.rectangle, BorderRadius borderRadius, bool expectClip }) {
155
      assert(shape != null);
156 157 158
      FakeAsync().run((FakeAsync async) {
        final DelayedImageProvider imageProvider = DelayedImageProvider();
        final DecorationImage backgroundImage = DecorationImage(image: imageProvider);
159

160
        final BoxDecoration boxDecoration = BoxDecoration(
161 162
          shape: shape,
          borderRadius: borderRadius,
163
          image: backgroundImage,
164 165
        );

166
        final List<Invocation> invocations = <Invocation>[];
167
        final TestCanvas canvas = TestCanvas(invocations);
168 169
        const ImageConfiguration imageConfiguration = ImageConfiguration(
            size: Size(100.0, 100.0)
170 171
        );
        bool onChangedCalled = false;
172
        final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
173 174 175
          onChangedCalled = true;
        });

176
        // _BoxDecorationPainter._paintDecorationImage() resolves the background
177 178 179 180 181 182 183 184 185 186
        // image and adds a listener to the resolved image stream.
        boxPainter.paint(canvas, Offset.zero, imageConfiguration);
        imageProvider.complete();

        // Run the listener which calls onChanged() which saves an internal
        // reference to the TestImage.
        async.flushMicrotasks();
        expect(onChangedCalled, isTrue);
        boxPainter.paint(canvas, Offset.zero, imageConfiguration);

Josh Soref's avatar
Josh Soref committed
187
        // We expect a clip to precede the drawImageRect call.
188
        final List<Invocation> commands = canvas.invocations.where((Invocation invocation) {
189 190
          return invocation.memberName == #clipPath || invocation.memberName == #drawImageRect;
        }).toList();
Josh Soref's avatar
Josh Soref committed
191
        if (expectClip) { // We expect a clip to precede the drawImageRect call.
192 193 194 195 196 197 198 199 200 201 202
          expect(commands.length, 2);
          expect(commands[0].memberName, equals(#clipPath));
          expect(commands[1].memberName, equals(#drawImageRect));
        } else {
          expect(commands.length, 1);
          expect(commands[0].memberName, equals(#drawImageRect));
        }
      });
    }

    testDecoration(shape: BoxShape.circle, expectClip: true);
203
    testDecoration(borderRadius: const BorderRadius.all(Radius.circular(16.0)), expectClip: true);
204 205
    testDecoration(expectClip: false);
  });
206 207

  test('DecorationImage test', () {
208
    const ColorFilter colorFilter = ui.ColorFilter.mode(Color(0xFF00FF00), BlendMode.src);
209 210
    final DecorationImage backgroundImage = DecorationImage(
      image: SynchronousTestImageProvider(),
211 212
      colorFilter: colorFilter,
      fit: BoxFit.contain,
213
      alignment: Alignment.bottomLeft,
Dan Field's avatar
Dan Field committed
214
      centerSlice: const Rect.fromLTWH(10.0, 20.0, 30.0, 40.0),
215 216 217
      repeat: ImageRepeat.repeatY,
    );

218
    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
219
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() { assert(false); });
220
    final TestCanvas canvas = TestCanvas(<Invocation>[]);
221
    boxPainter.paint(canvas, Offset.zero, const ImageConfiguration(size: Size(100.0, 100.0)));
222 223 224 225

    final Invocation call = canvas.invocations.singleWhere((Invocation call) => call.memberName == #drawImageNine);
    expect(call.isMethod, isTrue);
    expect(call.positionalArguments, hasLength(4));
226
    expect(call.positionalArguments[0], isInstanceOf<TestImage>());
Dan Field's avatar
Dan Field committed
227 228
    expect(call.positionalArguments[1], const Rect.fromLTRB(10.0, 20.0, 40.0, 60.0));
    expect(call.positionalArguments[2], const Rect.fromLTRB(0.0, 0.0, 100.0, 100.0));
229
    expect(call.positionalArguments[3], isInstanceOf<Paint>());
230 231 232 233
    expect(call.positionalArguments[3].isAntiAlias, false);
    expect(call.positionalArguments[3].colorFilter, colorFilter);
    expect(call.positionalArguments[3].filterQuality, FilterQuality.low);
  });
234

235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
  test(
      'DecorationImage with null textDirection configuration should throw Error', () {
    final DecorationImage backgroundImage = DecorationImage(
      image: SynchronousTestImageProvider(),
      matchTextDirection: true,
    );
    final BoxDecoration boxDecoration = BoxDecoration(
        image: backgroundImage);
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
      assert(false);
    });
    final TestCanvas canvas = TestCanvas(<Invocation>[]);
    FlutterError error;
    try {
      boxPainter.paint(canvas, Offset.zero, const ImageConfiguration(
          size: Size(100.0, 100.0), textDirection: null));
    } on FlutterError catch (e) {
      error = e;
    }
    expect(error, isNotNull);
    expect(error.diagnostics.length, 4);
    expect(error.diagnostics[2], isInstanceOf<DiagnosticsProperty<DecorationImage>>());
    expect(error.diagnostics[3], isInstanceOf<DiagnosticsProperty<ImageConfiguration>>());
    expect(error.toStringDeep(),
      'FlutterError\n'
      '   ImageDecoration.matchTextDirection can only be used when a\n'
      '   TextDirection is available.\n'
      '   When DecorationImagePainter.paint() was called, there was no text\n'
      '   direction provided in the ImageConfiguration object to match.\n'
      '   The DecorationImage was:\n'
      '     DecorationImage(SynchronousTestImageProvider(), center, match\n'
      '     text direction)\n'
      '   The ImageConfiguration was:\n'
      '     ImageConfiguration(size: Size(100.0, 100.0))\n'
    );
  });

272 273 274 275 276 277 278 279 280
  test('BoxDecoration.lerp - shapes', () {
    // We don't lerp the shape, we just switch from one to the other at t=0.5.
    // (Use a ShapeDecoration and ShapeBorder if you want to lerp the shapes...)
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(shape: BoxShape.rectangle),
        const BoxDecoration(shape: BoxShape.circle),
        -1.0,
      ),
281
      const BoxDecoration(shape: BoxShape.rectangle),
282 283 284 285 286 287 288
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(shape: BoxShape.rectangle),
        const BoxDecoration(shape: BoxShape.circle),
        0.0,
      ),
289
      const BoxDecoration(shape: BoxShape.rectangle),
290 291 292 293 294 295 296
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(shape: BoxShape.rectangle),
        const BoxDecoration(shape: BoxShape.circle),
        0.25,
      ),
297
      const BoxDecoration(shape: BoxShape.rectangle),
298 299 300 301 302 303 304
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(shape: BoxShape.rectangle),
        const BoxDecoration(shape: BoxShape.circle),
        0.75,
      ),
305
      const BoxDecoration(shape: BoxShape.circle),
306 307 308 309 310 311 312
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(shape: BoxShape.rectangle),
        const BoxDecoration(shape: BoxShape.circle),
        1.0,
      ),
313
      const BoxDecoration(shape: BoxShape.circle),
314 315 316 317 318 319 320
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(shape: BoxShape.rectangle),
        const BoxDecoration(shape: BoxShape.circle),
        2.0,
      ),
321
      const BoxDecoration(shape: BoxShape.circle),
322 323 324 325
    );
  });

  test('BoxDecoration.lerp - gradients', () {
326
    const Gradient gradient = LinearGradient(colors: <Color>[ Color(0x00000000), Color(0xFFFFFFFF) ]);
327 328 329
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
330
        const BoxDecoration(gradient: gradient),
331 332
        -1.0,
      ),
333
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0x00FFFFFF) ])),
334 335 336 337
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
338
        const BoxDecoration(gradient: gradient),
339 340
        0.0,
      ),
341
      const BoxDecoration(),
342 343 344 345
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
346
        const BoxDecoration(gradient: gradient),
347 348
        0.25,
      ),
349
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0x40FFFFFF) ])),
350 351 352 353
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
354
        const BoxDecoration(gradient: gradient),
355 356
        0.75,
      ),
357
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0xBFFFFFFF) ])),
358 359 360 361
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
362
        const BoxDecoration(gradient: gradient),
363 364
        1.0,
      ),
365
      const BoxDecoration(gradient: gradient),
366 367 368 369
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
370
        const BoxDecoration(gradient: gradient),
371 372
        2.0,
      ),
373
      const BoxDecoration(gradient: gradient),
374 375
    );
  });
376 377

  test('Decoration.lerp with unrelated decorations', () {
378 379 380 381
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 0.0), isInstanceOf<FlutterLogoDecoration>()); // ignore: CONST_EVAL_THROWS_EXCEPTION
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 0.25), isInstanceOf<FlutterLogoDecoration>()); // ignore: CONST_EVAL_THROWS_EXCEPTION
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 0.75), isInstanceOf<BoxDecoration>()); // ignore: CONST_EVAL_THROWS_EXCEPTION
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 1.0), isInstanceOf<BoxDecoration>()); // ignore: CONST_EVAL_THROWS_EXCEPTION
382
  });
383 384 385

  test('paintImage BoxFit.none scale test', () {
    for (double scale = 1.0; scale <= 4.0; scale += 1.0) {
386
      final TestCanvas canvas = TestCanvas(<Invocation>[]);
387

Dan Field's avatar
Dan Field committed
388
      const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
389
      final ui.Image image = TestImage();
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416

      paintImage(
        canvas: canvas,
        rect: outputRect,
        image: image,
        scale: scale,
        alignment: Alignment.bottomRight,
        fit: BoxFit.none,
        repeat: ImageRepeat.noRepeat,
        flipHorizontally: false,
      );

      const Size imageSize = Size(100.0, 100.0);

      final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

      expect(call.isMethod, isTrue);
      expect(call.positionalArguments, hasLength(4));

      expect(call.positionalArguments[0], isInstanceOf<TestImage>());

      // sourceRect should contain all pixels of the source image
      expect(call.positionalArguments[1], Offset.zero & imageSize);

      // Image should be scaled down (divided by scale)
      // and be positioned in the bottom right of the outputRect
      final Size expectedTileSize = imageSize / scale;
417
      final Rect expectedTileRect = Rect.fromPoints(
418 419 420 421 422 423 424 425 426 427 428
        outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
        outputRect.bottomRight,
      );
      expect(call.positionalArguments[2], expectedTileRect);

      expect(call.positionalArguments[3], isInstanceOf<Paint>());
    }
  });

  test('paintImage BoxFit.scaleDown scale test', () {
    for (double scale = 1.0; scale <= 4.0; scale += 1.0) {
429
      final TestCanvas canvas = TestCanvas(<Invocation>[]);
430 431

      // container size > scaled image size
Dan Field's avatar
Dan Field committed
432
      const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
433
      final ui.Image image = TestImage();
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

      paintImage(
        canvas: canvas,
        rect: outputRect,
        image: image,
        scale: scale,
        alignment: Alignment.bottomRight,
        fit: BoxFit.scaleDown,
        repeat: ImageRepeat.noRepeat,
        flipHorizontally: false,
      );

      const Size imageSize = Size(100.0, 100.0);

      final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

      expect(call.isMethod, isTrue);
      expect(call.positionalArguments, hasLength(4));

      expect(call.positionalArguments[0], isInstanceOf<TestImage>());

      // sourceRect should contain all pixels of the source image
      expect(call.positionalArguments[1], Offset.zero & imageSize);

      // Image should be scaled down (divided by scale)
      // and be positioned in the bottom right of the outputRect
      final Size expectedTileSize = imageSize / scale;
461
      final Rect expectedTileRect = Rect.fromPoints(
462 463 464 465 466 467 468 469 470 471
        outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
        outputRect.bottomRight,
      );
      expect(call.positionalArguments[2], expectedTileRect);

      expect(call.positionalArguments[3], isInstanceOf<Paint>());
    }
  });

  test('paintImage BoxFit.scaleDown test', () {
472
    final TestCanvas canvas = TestCanvas(<Invocation>[]);
473 474

    // container height (20 px) < scaled image height (50 px)
Dan Field's avatar
Dan Field committed
475
    const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 20.0);
476
    final ui.Image image = TestImage();
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

    paintImage(
      canvas: canvas,
      rect: outputRect,
      image: image,
      scale: 2.0,
      alignment: Alignment.bottomRight,
      fit: BoxFit.scaleDown,
      repeat: ImageRepeat.noRepeat,
      flipHorizontally: false,
    );

    const Size imageSize = Size(100.0, 100.0);

    final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

    expect(call.isMethod, isTrue);
    expect(call.positionalArguments, hasLength(4));

    expect(call.positionalArguments[0], isInstanceOf<TestImage>());

    // sourceRect should contain all pixels of the source image
    expect(call.positionalArguments[1], Offset.zero & imageSize);

    // Image should be scaled down to fit in hejght
    // and be positioned in the bottom right of the outputRect
    const Size expectedTileSize = Size(20.0, 20.0);
504
    final Rect expectedTileRect = Rect.fromPoints(
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
      outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
      outputRect.bottomRight,
    );
    expect(call.positionalArguments[2], expectedTileRect);

    expect(call.positionalArguments[3], isInstanceOf<Paint>());
  });

  test('paintImage boxFit, scale and alignment test', () {
    const List<BoxFit> boxFits = <BoxFit>[
      BoxFit.contain,
      BoxFit.cover,
      BoxFit.fitWidth,
      BoxFit.fitWidth,
      BoxFit.fitHeight,
      BoxFit.none,
      BoxFit.scaleDown,
    ];

524
    for (BoxFit boxFit in boxFits) {
525
      final TestCanvas canvas = TestCanvas(<Invocation>[]);
526

Dan Field's avatar
Dan Field committed
527
      const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
528
      final ui.Image image = TestImage();
529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549

      paintImage(
        canvas: canvas,
        rect: outputRect,
        image: image,
        scale: 3.0,
        alignment: Alignment.center,
        fit: boxFit,
        repeat: ImageRepeat.noRepeat,
        flipHorizontally: false,
      );

      final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

      expect(call.isMethod, isTrue);
      expect(call.positionalArguments, hasLength(4));

      // Image should be positioned in the center of the container
      expect(call.positionalArguments[2].center, outputRect.center);
    }
  });
550
}