448 lines
16 KiB
Dart
448 lines
16 KiB
Dart
// 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_test/flutter_test.dart';
|
|
import 'package:flutter/widgets.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);
|
|
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;
|
|
}
|
|
}
|