1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'dart:ui';
6library;
7
8import 'dart:async';
9
10import 'package:flutter/painting.dart';
11import 'package:flutter/scheduler.dart';
12
13import 'disposable_build_context.dart';
14import 'framework.dart';
15import 'scrollable.dart';
16
17/// An [ImageProvider] that makes use of
18/// [Scrollable.recommendDeferredLoadingForContext] to avoid loading images when
19/// rapidly scrolling.
20///
21/// This provider assumes that its wrapped [imageProvider] correctly uses the
22/// [ImageCache], and does not attempt to re-acquire or decode images in the
23/// cache.
24///
25/// Calling [resolve] on this provider will cause it to obtain the image key
26/// and then check the following:
27///
28/// 1. If the returned [ImageStream] has been completed, end. This can happen
29/// if the caller sets the completer on the stream.
30/// 2. If the [ImageCache] has a completer for the key for this image, ask the
31/// wrapped provider to resolve.
32/// This can happen if the image was precached, or another [ImageProvider]
33/// already resolved the same image.
34/// 3. If the [context] has been disposed, end. This can happen if the caller
35/// has been disposed and is no longer interested in resolving the image.
36/// 4. If the widget is scrolling with high velocity at this point in time,
37/// wait until the beginning of the next frame and go back to step 1.
38/// 5. Delegate loading the image to the wrapped provider and finish.
39///
40/// If the cycle ends at steps 1 or 3, the [ImageStream] will never be marked as
41/// complete and listeners will not be notified.
42///
43/// The [Image] widget wraps its incoming providers with this provider to avoid
44/// overutilization of resources for images that would never appear on screen or
45/// only be visible for a very brief period.
46@optionalTypeArgs
47class ScrollAwareImageProvider<T extends Object> extends ImageProvider<T> {
48 /// Creates a [ScrollAwareImageProvider].
49 ///
50 /// The [context] object is the [BuildContext] of the [State] using this
51 /// provider. It is used to determine scrolling velocity during [resolve].
52 ///
53 /// The [imageProvider] is used to create a key and load the image. It must
54 /// not be null, and is assumed to interact with the cache in the normal way
55 /// that [ImageProvider.resolveStreamForKey] does.
56 const ScrollAwareImageProvider({required this.context, required this.imageProvider});
57
58 /// The context that may or may not be enclosed by a [Scrollable].
59 ///
60 /// Once [DisposableBuildContext.dispose] is called on this context,
61 /// the provider will stop trying to resolve the image if it has not already
62 /// been resolved.
63 final DisposableBuildContext context;
64
65 /// The wrapped image provider to delegate [obtainKey] and [loadImage] to.
66 final ImageProvider<T> imageProvider;
67
68 @override
69 void resolveStreamForKey(
70 ImageConfiguration configuration,
71 ImageStream stream,
72 T key,
73 ImageErrorListener handleError,
74 ) {
75 // Something managed to complete the stream, or it's already in the image
76 // cache. Notify the wrapped provider and expect it to behave by not
77 // reloading the image since it's already resolved.
78 // Do this even if the context has gone out of the tree, since it will
79 // update LRU information about the cache. Even though we never showed the
80 // image, it was still touched more recently.
81 // Do this before checking scrolling, so that if the bytes are available we
82 // render them even though we're scrolling fast - there's no additional
83 // allocations to do for texture memory, it's already there.
84 if (stream.completer != null || PaintingBinding.instance.imageCache.containsKey(key)) {
85 imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
86 return;
87 }
88 // The context has gone out of the tree - ignore it.
89 if (context.context == null) {
90 return;
91 }
92 // Something still wants this image, but check if the context is scrolling
93 // too fast before scheduling work that might never show on screen.
94 // Try to get to end of the frame callbacks of the next frame, and then
95 // check again.
96 if (Scrollable.recommendDeferredLoadingForContext(context.context!)) {
97 SchedulerBinding.instance.scheduleFrameCallback((_) {
98 scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
99 });
100 return;
101 }
102 // We are in the tree, we're not scrolling too fast, the cache doesn't
103 // have our image, and no one has otherwise completed the stream. Go.
104 imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
105 }
106
107 @override
108 ImageStreamCompleter loadBuffer(T key, DecoderBufferCallback decode) =>
109 imageProvider.loadBuffer(key, decode);
110
111 @override
112 ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) =>
113 imageProvider.loadImage(key, decode);
114
115 @override
116 Future<T> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration);
117}
118