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
5import 'dart:async';
6
7import 'package:flutter/painting.dart';
8import 'package:flutter/scheduler.dart';
9
10import 'disposable_build_context.dart';
11import 'framework.dart';
12import 'scrollable.dart';
13
14/// An [ImageProvider] that makes use of
15/// [Scrollable.recommendDeferredLoadingForContext] to avoid loading images when
16/// rapidly scrolling.
17///
18/// This provider assumes that its wrapped [imageProvider] correctly uses the
19/// [ImageCache], and does not attempt to re-acquire or decode images in the
20/// cache.
21///
22/// Calling [resolve] on this provider will cause it to obtain the image key
23/// and then check the following:
24///
25/// 1. If the returned [ImageStream] has been completed, end. This can happen
26/// if the caller sets the completer on the stream.
27/// 2. If the [ImageCache] has a completer for the key for this image, ask the
28/// wrapped provider to resolve.
29/// This can happen if the image was precached, or another [ImageProvider]
30/// already resolved the same image.
31/// 3. If the [context] has been disposed, end. This can happen if the caller
32/// has been disposed and is no longer interested in resolving the image.
33/// 4. If the widget is scrolling with high velocity at this point in time,
34/// wait until the beginning of the next frame and go back to step 1.
35/// 5. Delegate loading the image to the wrapped provider and finish.
36///
37/// If the cycle ends at steps 1 or 3, the [ImageStream] will never be marked as
38/// complete and listeners will not be notified.
39///
40/// The [Image] widget wraps its incoming providers with this provider to avoid
41/// overutilization of resources for images that would never appear on screen or
42/// only be visible for a very brief period.
43@optionalTypeArgs
44class ScrollAwareImageProvider<T extends Object> extends ImageProvider<T> {
45 /// Creates a [ScrollAwareImageProvider].
46 ///
47 /// The [context] object is the [BuildContext] of the [State] using this
48 /// provider. It is used to determine scrolling velocity during [resolve].
49 ///
50 /// The [imageProvider] is used to create a key and load the image. It must
51 /// not be null, and is assumed to interact with the cache in the normal way
52 /// that [ImageProvider.resolveStreamForKey] does.
53 const ScrollAwareImageProvider({
54 required this.context,
55 required this.imageProvider,
56 });
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) => imageProvider.loadBuffer(key, decode);
109
110 @override
111 ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) => imageProvider.loadImage(key, decode);
112
113 @override
114 Future<T> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration);
115}
116