| // 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. |
| |
| import 'dart:ui' as ui show Image; |
| |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../painting/image_test_utils.dart'; |
| |
| void main() { |
| |
| late ui.Image testImage; |
| |
| setUpAll(() async { |
| testImage = await createTestImage(width: 10, height: 10); |
| }); |
| |
| tearDown(() { |
| imageCache?.clear(); |
| }); |
| |
| T _findPhysics<T extends ScrollPhysics>(WidgetTester tester) { |
| return Scrollable.of(find.byType(TestWidget).evaluate().first)!.position.physics as T; |
| } |
| |
| ScrollMetrics _findMetrics(WidgetTester tester) { |
| return Scrollable.of(find.byType(TestWidget).evaluate().first)!.position; |
| } |
| |
| 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)); |
| |
| final DisposableBuildContext context = DisposableBuildContext(key.currentState!); |
| final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); |
| final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>( |
| context: context, |
| imageProvider: testImageProvider, |
| ); |
| |
| expect(testImageProvider.configuration, null); |
| expect(imageCache!.containsKey(testImageProvider), false); |
| |
| final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); |
| |
| expect(testImageProvider.configuration, ImageConfiguration.empty); |
| expect(stream.completer, isNotNull); |
| expect(stream.completer!.hasListeners, true); |
| expect(imageCache!.containsKey(testImageProvider), true); |
| expect(imageCache!.currentSize, 0); |
| |
| testImageProvider.complete(); |
| |
| expect(imageCache!.currentSize, 1); |
| }); |
| |
| 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), |
| ], |
| ), |
| )); |
| |
| final DisposableBuildContext context = DisposableBuildContext(key.currentState!); |
| final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); |
| final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>( |
| context: context, |
| imageProvider: testImageProvider, |
| ); |
| |
| expect(testImageProvider.configuration, null); |
| expect(imageCache!.containsKey(testImageProvider), false); |
| |
| final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); |
| |
| expect(testImageProvider.configuration, ImageConfiguration.empty); |
| expect(stream.completer, isNotNull); |
| expect(stream.completer!.hasListeners, true); |
| expect(imageCache!.containsKey(testImageProvider), true); |
| expect(imageCache!.currentSize, 0); |
| |
| testImageProvider.complete(); |
| |
| expect(imageCache!.currentSize, 1); |
| expect(_findPhysics<RecordingPhysics>(tester).velocities, <double>[0]); |
| }); |
| |
| 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, |
| ), |
| )); |
| |
| final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!); |
| final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); |
| final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>( |
| context: context, |
| imageProvider: testImageProvider, |
| ); |
| |
| expect(testImageProvider.configuration, null); |
| expect(imageCache!.containsKey(testImageProvider), false); |
| |
| scrollController.animateTo( |
| 100, |
| duration: const Duration(seconds: 2), |
| curve: Curves.fastLinearToSlowEaseIn, |
| ); |
| await tester.pump(); |
| final RecordingPhysics physics = _findPhysics<RecordingPhysics>(tester); |
| |
| 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); |
| expect(stream.completer!.hasListeners, true); |
| expect(imageCache!.containsKey(testImageProvider), true); |
| expect(imageCache!.currentSize, 0); |
| |
| testImageProvider.complete(); |
| |
| expect(imageCache!.currentSize, 1); |
| }); |
| |
| 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, |
| ), |
| )); |
| |
| final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!); |
| final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); |
| final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>( |
| context: context, |
| imageProvider: testImageProvider, |
| ); |
| |
| expect(testImageProvider.configuration, null); |
| expect(imageCache!.containsKey(testImageProvider), false); |
| |
| scrollController.animateTo( |
| 3000, |
| duration: const Duration(seconds: 2), |
| curve: Curves.fastLinearToSlowEaseIn, |
| ); |
| await tester.pump(); |
| final RecordingPhysics physics = _findPhysics<RecordingPhysics>(tester); |
| |
| 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); |
| |
| expect(imageCache!.containsKey(testImageProvider), false); |
| expect(imageCache!.currentSize, 0); |
| |
| await tester.pump(const Duration(seconds: 1)); |
| expect(physics.velocities.last, 0); |
| |
| expect(testImageProvider.configuration, ImageConfiguration.empty); |
| expect(stream.completer, isNotNull); |
| expect(stream.completer!.hasListeners, true); |
| |
| expect(imageCache!.containsKey(testImageProvider), true); |
| expect(imageCache!.currentSize, 0); |
| |
| testImageProvider.complete(); |
| |
| expect(imageCache!.currentSize, 1); |
| }); |
| |
| 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, |
| ), |
| )); |
| |
| final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!); |
| final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); |
| final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>( |
| context: context, |
| imageProvider: testImageProvider, |
| ); |
| |
| expect(testImageProvider.configuration, null); |
| expect(imageCache!.containsKey(testImageProvider), false); |
| |
| scrollController.animateTo( |
| 3000, |
| duration: const Duration(seconds: 2), |
| curve: Curves.fastLinearToSlowEaseIn, |
| ); |
| await tester.pump(); |
| final RecordingPhysics physics = _findPhysics<RecordingPhysics>(tester); |
| |
| 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); |
| |
| expect(imageCache!.containsKey(testImageProvider), false); |
| expect(imageCache!.currentSize, 0); |
| |
| // 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); |
| |
| expect(imageCache!.containsKey(testImageProvider), false); |
| expect(imageCache!.currentSize, 0); |
| |
| testImageProvider.complete(); |
| |
| expect(imageCache!.currentSize, 0); |
| }); |
| |
| 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), |
| ), |
| )); |
| |
| final DisposableBuildContext context = DisposableBuildContext(key.currentState!); |
| final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); |
| final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>( |
| context: context, |
| imageProvider: testImageProvider, |
| ); |
| |
| expect(testImageProvider.configuration, null); |
| expect(imageCache!.containsKey(testImageProvider), false); |
| |
| final ControllablePhysics physics = _findPhysics<ControllablePhysics>(tester); |
| physics.recommendDeferredLoadingValue = true; |
| |
| final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); |
| |
| expect(testImageProvider.configuration, null); |
| expect(stream.completer, null); |
| expect(imageCache!.containsKey(testImageProvider), false); |
| expect(imageCache!.currentSize, 0); |
| |
| // Simulate a case where someone else has managed to complete this stream - |
| // so it can land in the cache right before we stop scrolling fast. |
| // If we miss the early return, we will fail. |
| testImageProvider.complete(); |
| |
| imageCache!.putIfAbsent(testImageProvider, () => testImageProvider.load(testImageProvider, PaintingBinding.instance!.instantiateImageCodec)); |
| // We've stopped scrolling fast. |
| physics.recommendDeferredLoadingValue = false; |
| await tester.idle(); |
| |
| expect(imageCache!.containsKey(testImageProvider), true); |
| expect(imageCache!.currentSize, 1); |
| expect(testImageProvider.loadCallCount, 1); |
| expect(stream.completer, null); |
| }); |
| |
| testWidgets('ScrollAwareImageProvider does not block LRU updates to image cache', (WidgetTester tester) async { |
| final int oldSize = imageCache!.maximumSize; |
| imageCache!.maximumSize = 1; |
| |
| 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), |
| ), |
| )); |
| |
| final DisposableBuildContext context = DisposableBuildContext(key.currentState!); |
| final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); |
| final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>( |
| context: context, |
| imageProvider: testImageProvider, |
| ); |
| |
| expect(testImageProvider.configuration, null); |
| expect(imageCache!.containsKey(testImageProvider), false); |
| |
| final ControllablePhysics physics = _findPhysics<ControllablePhysics>(tester); |
| physics.recommendDeferredLoadingValue = true; |
| |
| final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); |
| |
| expect(testImageProvider.configuration, null); |
| expect(stream.completer, null); |
| expect(imageCache!.currentSize, 0); |
| |
| // Occupy the only slot in the cache with another image. |
| final TestImageProvider testImageProvider2 = TestImageProvider(testImage.clone()); |
| testImageProvider2.complete(); |
| await precacheImage(testImageProvider2, context.context!); |
| expect(imageCache!.containsKey(testImageProvider), false); |
| expect(imageCache!.containsKey(testImageProvider2), true); |
| expect(imageCache!.currentSize, 1); |
| |
| // Complete the original image while we're still scrolling fast. |
| testImageProvider.complete(); |
| stream.setCompleter(testImageProvider.load(testImageProvider, PaintingBinding.instance!.instantiateImageCodec)); |
| |
| // Verify that this hasn't changed the cache state yet |
| expect(imageCache!.containsKey(testImageProvider), false); |
| expect(imageCache!.containsKey(testImageProvider2), true); |
| expect(imageCache!.currentSize, 1); |
| 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. |
| expect(imageCache!.containsKey(testImageProvider), true); |
| expect(imageCache!.containsKey(testImageProvider2), false); |
| expect(imageCache!.currentSize, 1); |
| expect(testImageProvider.loadCallCount, 1); |
| |
| imageCache!.maximumSize = oldSize; |
| }); |
| } |
| |
| class TestWidget extends StatefulWidget { |
| const TestWidget(Key? key) : super(key: key); |
| |
| @override |
| State<TestWidget> createState() => TestWidgetState(); |
| } |
| |
| class TestWidgetState extends State<TestWidget> { |
| @override |
| Widget build(BuildContext context) => const SizedBox(height: 50); |
| } |
| |
| class RecordingPhysics extends ScrollPhysics { |
| RecordingPhysics({ ScrollPhysics? parent }) : super(parent: parent); |
| |
| final List<double> velocities = <double>[]; |
| |
| @override |
| RecordingPhysics applyTo(ScrollPhysics? ancestor) { |
| return RecordingPhysics(parent: buildParent(ancestor)); |
| } |
| |
| @override |
| bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) { |
| velocities.add(velocity); |
| return super.recommendDeferredLoading(velocity, metrics, context); |
| } |
| } |
| |
| // 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 { |
| ControllablePhysics({ ScrollPhysics? parent }) : super(parent: parent); |
| |
| bool recommendDeferredLoadingValue = false; |
| |
| @override |
| ControllablePhysics applyTo(ScrollPhysics? ancestor) { |
| return ControllablePhysics(parent: buildParent(ancestor)); |
| } |
| |
| @override |
| bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) { |
| return recommendDeferredLoadingValue; |
| } |
| } |