decoration_test.dart 17.5 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
import '../flutter_test_alternative.dart';
14 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 36
  }

  @override
  ImageStreamCompleter load(int key) {
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 50
  }

  @override
  ImageStreamCompleter load(int key) {
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) {
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

  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,
      ),
244
      const BoxDecoration(shape: BoxShape.rectangle),
245 246 247 248 249 250 251
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(shape: BoxShape.rectangle),
        const BoxDecoration(shape: BoxShape.circle),
        0.0,
      ),
252
      const BoxDecoration(shape: BoxShape.rectangle),
253 254 255 256 257 258 259
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(shape: BoxShape.rectangle),
        const BoxDecoration(shape: BoxShape.circle),
        0.25,
      ),
260
      const BoxDecoration(shape: BoxShape.rectangle),
261 262 263 264 265 266 267
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(shape: BoxShape.rectangle),
        const BoxDecoration(shape: BoxShape.circle),
        0.75,
      ),
268
      const BoxDecoration(shape: BoxShape.circle),
269 270 271 272 273 274 275
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(shape: BoxShape.rectangle),
        const BoxDecoration(shape: BoxShape.circle),
        1.0,
      ),
276
      const BoxDecoration(shape: BoxShape.circle),
277 278 279 280 281 282 283
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(shape: BoxShape.rectangle),
        const BoxDecoration(shape: BoxShape.circle),
        2.0,
      ),
284
      const BoxDecoration(shape: BoxShape.circle),
285 286 287 288
    );
  });

  test('BoxDecoration.lerp - gradients', () {
289
    const Gradient gradient = LinearGradient(colors: <Color>[ Color(0x00000000), Color(0xFFFFFFFF) ]);
290 291 292
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
293
        const BoxDecoration(gradient: gradient),
294 295
        -1.0,
      ),
296
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0x00FFFFFF) ])),
297 298 299 300
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
301
        const BoxDecoration(gradient: gradient),
302 303
        0.0,
      ),
304
      const BoxDecoration(),
305 306 307 308
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
309
        const BoxDecoration(gradient: gradient),
310 311
        0.25,
      ),
312
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0x40FFFFFF) ])),
313 314 315 316
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
317
        const BoxDecoration(gradient: gradient),
318 319
        0.75,
      ),
320
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0xBFFFFFFF) ])),
321 322 323 324
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
325
        const BoxDecoration(gradient: gradient),
326 327
        1.0,
      ),
328
      const BoxDecoration(gradient: gradient),
329 330 331 332
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
333
        const BoxDecoration(gradient: gradient),
334 335
        2.0,
      ),
336
      const BoxDecoration(gradient: gradient),
337 338
    );
  });
339 340

  test('Decoration.lerp with unrelated decorations', () {
341 342 343 344
    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
345
  });
346 347 348

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

Dan Field's avatar
Dan Field committed
351
      const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
352
      final ui.Image image = TestImage();
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379

      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;
380
      final Rect expectedTileRect = Rect.fromPoints(
381 382 383 384 385 386 387 388 389 390 391
        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) {
392
      final TestCanvas canvas = TestCanvas(<Invocation>[]);
393 394

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

      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;
424
      final Rect expectedTileRect = Rect.fromPoints(
425 426 427 428 429 430 431 432 433 434
        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', () {
435
    final TestCanvas canvas = TestCanvas(<Invocation>[]);
436 437

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

    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);
467
    final Rect expectedTileRect = Rect.fromPoints(
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486
      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,
    ];

487
    for (BoxFit boxFit in boxFits) {
488
      final TestCanvas canvas = TestCanvas(<Invocation>[]);
489

Dan Field's avatar
Dan Field committed
490
      const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
491
      final ui.Image image = TestImage();
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512

      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);
    }
  });
513
}