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 | import 'dart:async'; |
6 | |
7 | import 'package:flutter/painting.dart'; |
8 | import 'package:flutter/scheduler.dart'; |
9 | |
10 | import 'disposable_build_context.dart'; |
11 | import 'framework.dart'; |
12 | import '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 |
44 | class 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 | |