scroll_aware_image_provider_test.dart 16.1 KB
Newer Older
1 2 3 4
// Copyright 2014 The Flutter 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:ui' as ui show Image;

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

import '../painting/image_test_utils.dart';

void main() {
13

14
  late ui.Image testImage;
15 16 17 18 19

  setUpAll(() async {
    testImage = await createTestImage(width: 10, height: 10);
  });

20
  tearDown(() {
21
    imageCache?.clear();
22 23
  });

24
  T _findPhysics<T extends ScrollPhysics>(WidgetTester tester) {
25
    return Scrollable.of(find.byType(TestWidget).evaluate().first)!.position.physics as T;
26 27 28
  }

  ScrollMetrics _findMetrics(WidgetTester tester) {
29
    return Scrollable.of(find.byType(TestWidget).evaluate().first)!.position;
30 31 32 33 34 35
  }

  testWidgets('ScrollAwareImageProvider does not delay if widget is not in scrollable', (WidgetTester tester) async {
    final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>();
    await tester.pumpWidget(TestWidget(key));

36
    final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
37 38 39 40 41 42 43
    final TestImageProvider testImageProvider = TestImageProvider(testImage);
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
44
    expect(imageCache!.containsKey(testImageProvider), false);
45 46 47 48 49

    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);

    expect(testImageProvider.configuration, ImageConfiguration.empty);
    expect(stream.completer, isNotNull);
50 51 52
    expect(stream.completer!.hasListeners, true);
    expect(imageCache!.containsKey(testImageProvider), true);
    expect(imageCache!.currentSize, 0);
53 54 55

    testImageProvider.complete();

56
    expect(imageCache!.currentSize, 1);
57 58 59 60 61 62 63 64 65 66 67 68 69 70
  });

  testWidgets('ScrollAwareImageProvider does not delay if in scrollable that is not scrolling', (WidgetTester tester) async {
    final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: ListView(
        physics: RecordingPhysics(),
        children: <Widget>[
          TestWidget(key),
        ],
      ),
    ));

71
    final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
72 73 74 75 76 77 78
    final TestImageProvider testImageProvider = TestImageProvider(testImage);
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
79
    expect(imageCache!.containsKey(testImageProvider), false);
80 81 82 83 84

    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);

    expect(testImageProvider.configuration, ImageConfiguration.empty);
    expect(stream.completer, isNotNull);
85 86 87
    expect(stream.completer!.hasListeners, true);
    expect(imageCache!.containsKey(testImageProvider), true);
    expect(imageCache!.currentSize, 0);
88 89 90

    testImageProvider.complete();

91
    expect(imageCache!.currentSize, 1);
92
    expect(_findPhysics<RecordingPhysics>(tester).velocities, <double>[0]);
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
  });

  testWidgets('ScrollAwareImageProvider does not delay if in scrollable that is scrolling slowly', (WidgetTester tester) async {
    final List<GlobalKey<TestWidgetState>> keys = <GlobalKey<TestWidgetState>>[];
    final ScrollController scrollController = ScrollController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: ListView.builder(
        physics: RecordingPhysics(),
        controller: scrollController,
        itemBuilder: (BuildContext context, int index) {
          keys.add(GlobalKey<TestWidgetState>());
          return TestWidget(keys.last);
        },
        itemCount: 50,
      ),
    ));

111
    final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!);
112 113 114 115 116 117 118
    final TestImageProvider testImageProvider = TestImageProvider(testImage);
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
119
    expect(imageCache!.containsKey(testImageProvider), false);
120 121 122 123 124 125 126

    scrollController.animateTo(
      100,
      duration: const Duration(seconds: 2),
      curve: Curves.fastLinearToSlowEaseIn,
    );
    await tester.pump();
127
    final RecordingPhysics physics = _findPhysics<RecordingPhysics>(tester);
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142

    expect(physics.velocities.length, 0);
    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
    expect(physics.velocities.length, 1);
    expect(
      const ScrollPhysics().recommendDeferredLoading(
        physics.velocities.first,
        _findMetrics(tester),
        find.byType(TestWidget).evaluate().first,
      ),
      false,
    );

    expect(testImageProvider.configuration, ImageConfiguration.empty);
    expect(stream.completer, isNotNull);
143 144 145
    expect(stream.completer!.hasListeners, true);
    expect(imageCache!.containsKey(testImageProvider), true);
    expect(imageCache!.currentSize, 0);
146 147 148

    testImageProvider.complete();

149
    expect(imageCache!.currentSize, 1);
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
  });

  testWidgets('ScrollAwareImageProvider delays if in scrollable that is scrolling fast', (WidgetTester tester) async {
    final List<GlobalKey<TestWidgetState>> keys = <GlobalKey<TestWidgetState>>[];
    final ScrollController scrollController = ScrollController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: ListView.builder(
        physics: RecordingPhysics(),
        controller: scrollController,
        itemBuilder: (BuildContext context, int index) {
          keys.add(GlobalKey<TestWidgetState>());
          return TestWidget(keys.last);
        },
        itemCount: 50,
      ),
    ));

168
    final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!);
169 170 171 172 173 174 175
    final TestImageProvider testImageProvider = TestImageProvider(testImage);
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
176
    expect(imageCache!.containsKey(testImageProvider), false);
177 178 179 180 181 182 183

    scrollController.animateTo(
      3000,
      duration: const Duration(seconds: 2),
      curve: Curves.fastLinearToSlowEaseIn,
    );
    await tester.pump();
184
    final RecordingPhysics physics = _findPhysics<RecordingPhysics>(tester);
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200

    expect(physics.velocities.length, 0);
    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
    expect(physics.velocities.length, 1);
    expect(
      const ScrollPhysics().recommendDeferredLoading(
        physics.velocities.first,
        _findMetrics(tester),
        find.byType(TestWidget).evaluate().first,
      ),
      true,
    );

    expect(testImageProvider.configuration, null);
    expect(stream.completer, null);

201 202
    expect(imageCache!.containsKey(testImageProvider), false);
    expect(imageCache!.currentSize, 0);
203 204 205 206 207 208

    await tester.pump(const Duration(seconds: 1));
    expect(physics.velocities.last, 0);

    expect(testImageProvider.configuration, ImageConfiguration.empty);
    expect(stream.completer, isNotNull);
209
    expect(stream.completer!.hasListeners, true);
210

211 212
    expect(imageCache!.containsKey(testImageProvider), true);
    expect(imageCache!.currentSize, 0);
213 214 215

    testImageProvider.complete();

216
    expect(imageCache!.currentSize, 1);
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
  });

  testWidgets('ScrollAwareImageProvider delays if in scrollable that is scrolling fast and fizzles if disposed', (WidgetTester tester) async {
    final List<GlobalKey<TestWidgetState>> keys = <GlobalKey<TestWidgetState>>[];
    final ScrollController scrollController = ScrollController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: ListView.builder(
        physics: RecordingPhysics(),
        controller: scrollController,
        itemBuilder: (BuildContext context, int index) {
          keys.add(GlobalKey<TestWidgetState>());
          return TestWidget(keys.last);
        },
        itemCount: 50,
      ),
    ));

235
    final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!);
236 237 238 239 240 241 242
    final TestImageProvider testImageProvider = TestImageProvider(testImage);
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
243
    expect(imageCache!.containsKey(testImageProvider), false);
244 245 246 247 248 249 250

    scrollController.animateTo(
      3000,
      duration: const Duration(seconds: 2),
      curve: Curves.fastLinearToSlowEaseIn,
    );
    await tester.pump();
251
    final RecordingPhysics physics = _findPhysics<RecordingPhysics>(tester);
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267

    expect(physics.velocities.length, 0);
    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
    expect(physics.velocities.length, 1);
    expect(
      const ScrollPhysics().recommendDeferredLoading(
        physics.velocities.first,
        _findMetrics(tester),
        find.byType(TestWidget).evaluate().first,
      ),
      true,
    );

    expect(testImageProvider.configuration, null);
    expect(stream.completer, null);

268 269
    expect(imageCache!.containsKey(testImageProvider), false);
    expect(imageCache!.currentSize, 0);
270 271 272 273 274 275 276 277 278 279

    // as if we had picked a context that scrolled out of the tree.
    context.dispose();

    await tester.pump(const Duration(seconds: 1));
    expect(physics.velocities.length, 1);

    expect(testImageProvider.configuration, null);
    expect(stream.completer, null);

280 281
    expect(imageCache!.containsKey(testImageProvider), false);
    expect(imageCache!.currentSize, 0);
282 283 284

    testImageProvider.complete();

285
    expect(imageCache!.currentSize, 0);
286
  });
287 288 289 290 291 292 293 294 295 296 297 298 299

  testWidgets('ScrollAwareImageProvider resolves from ImageCache and does not set completer twice', (WidgetTester tester) async {
    final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>();
    final ScrollController scrollController = ScrollController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: SingleChildScrollView(
        physics: ControllablePhysics(),
        controller: scrollController,
        child: TestWidget(key),
      ),
    ));

300
    final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
301 302 303 304 305 306 307
    final TestImageProvider testImageProvider = TestImageProvider(testImage);
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
308
    expect(imageCache!.containsKey(testImageProvider), false);
309 310 311 312 313 314 315 316

    final ControllablePhysics physics = _findPhysics<ControllablePhysics>(tester);
    physics.recommendDeferredLoadingValue = true;

    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);

    expect(testImageProvider.configuration, null);
    expect(stream.completer, null);
317 318
    expect(imageCache!.containsKey(testImageProvider), false);
    expect(imageCache!.currentSize, 0);
319

320
    // Simulate a case where someone else has managed to complete this stream -
321 322 323 324
    // so it can land in the cache right before we stop scrolling fast.
    // If we miss the early return, we will fail.
    testImageProvider.complete();

325
    imageCache!.putIfAbsent(testImageProvider, () => testImageProvider.load(testImageProvider, PaintingBinding.instance!.instantiateImageCodec));
326 327 328 329
    // We've stopped scrolling fast.
    physics.recommendDeferredLoadingValue = false;
    await tester.idle();

330 331
    expect(imageCache!.containsKey(testImageProvider), true);
    expect(imageCache!.currentSize, 1);
332
    expect(testImageProvider.loadCallCount, 1);
333 334
    expect(stream.completer, null);
  });
335 336

  testWidgets('ScrollAwareImageProvider does not block LRU updates to image cache', (WidgetTester tester) async {
337 338
    final int oldSize = imageCache!.maximumSize;
    imageCache!.maximumSize = 1;
339 340 341 342 343 344 345 346 347 348 349 350

    final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>();
    final ScrollController scrollController = ScrollController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: SingleChildScrollView(
        physics: ControllablePhysics(),
        controller: scrollController,
        child: TestWidget(key),
      ),
    ));

351
    final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
352 353 354 355 356 357 358
    final TestImageProvider testImageProvider = TestImageProvider(testImage);
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
359
    expect(imageCache!.containsKey(testImageProvider), false);
360 361 362 363 364 365 366 367

    final ControllablePhysics physics = _findPhysics<ControllablePhysics>(tester);
    physics.recommendDeferredLoadingValue = true;

    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);

    expect(testImageProvider.configuration, null);
    expect(stream.completer, null);
368
    expect(imageCache!.currentSize, 0);
369 370

    // Occupy the only slot in the cache with another image.
371
    final TestImageProvider testImageProvider2 = TestImageProvider(testImage);
372
    testImageProvider2.complete();
373 374 375 376
    await precacheImage(testImageProvider2, context.context!);
    expect(imageCache!.containsKey(testImageProvider), false);
    expect(imageCache!.containsKey(testImageProvider2), true);
    expect(imageCache!.currentSize, 1);
377 378 379

    // Complete the original image while we're still scrolling fast.
    testImageProvider.complete();
380
    stream.setCompleter(testImageProvider.load(testImageProvider, PaintingBinding.instance!.instantiateImageCodec));
381

382
    // Verify that this hasn't changed the cache state yet
383 384 385
    expect(imageCache!.containsKey(testImageProvider), false);
    expect(imageCache!.containsKey(testImageProvider2), true);
    expect(imageCache!.currentSize, 1);
386 387 388 389 390 391
    expect(testImageProvider.loadCallCount, 1);

    await tester.pump();

    // After pumping a frame, the original image should be in the cache because
    // it took the LRU slot.
392 393 394
    expect(imageCache!.containsKey(testImageProvider), true);
    expect(imageCache!.containsKey(testImageProvider2), false);
    expect(imageCache!.currentSize, 1);
395 396
    expect(testImageProvider.loadCallCount, 1);

397
    imageCache!.maximumSize = oldSize;
398
  });
399 400 401
}

class TestWidget extends StatefulWidget {
402
  const TestWidget(Key? key) : super(key: key);
403 404 405 406 407 408 409 410 411 412 413

  @override
  State<TestWidget> createState() => TestWidgetState();
}

class TestWidgetState extends State<TestWidget> {
  @override
  Widget build(BuildContext context) => const SizedBox(height: 50);
}

class RecordingPhysics extends ScrollPhysics {
414
  RecordingPhysics({ ScrollPhysics? parent }) : super(parent: parent);
415 416 417 418

  final List<double> velocities = <double>[];

  @override
419 420
  RecordingPhysics applyTo(ScrollPhysics? ancestor) {
    return RecordingPhysics(parent: buildParent(ancestor)!);
421 422 423 424 425 426 427 428
  }

  @override
  bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
    velocities.add(velocity);
    return super.recommendDeferredLoading(velocity, metrics, context);
  }
}
429 430 431 432 433

// Ignore this so that we can mutate whether we defer loading or not at specific
// times without worrying about actual scrolling mechanics.
// ignore: must_be_immutable
class ControllablePhysics extends ScrollPhysics {
434
  ControllablePhysics({ ScrollPhysics? parent }) : super(parent: parent);
435 436 437 438

  bool recommendDeferredLoadingValue = false;

  @override
439 440
  ControllablePhysics applyTo(ScrollPhysics? ancestor) {
    return ControllablePhysics(parent: buildParent(ancestor)!);
441 442 443 444 445 446 447
  }

  @override
  bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
    return recommendDeferredLoadingValue;
  }
}