blob: 6f1bf8ad0995e64635714c7760b4e781516a9d9c [file] [log] [blame]
// 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;
}
}