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