| // 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:async'; |
| |
| import 'package:flutter/painting.dart'; |
| import 'package:flutter/scheduler.dart'; |
| |
| import 'disposable_build_context.dart'; |
| import 'framework.dart'; |
| import 'scrollable.dart'; |
| |
| /// An [ImageProvider] that makes use of |
| /// [Scrollable.recommendDeferredLoadingForContext] to avoid loading images when |
| /// rapidly scrolling. |
| /// |
| /// This provider assumes that its wrapped [imageProvider] correctly uses the |
| /// [ImageCache], and does not attempt to re-acquire or decode images in the |
| /// cache. |
| /// |
| /// Calling [resolve] on this provider will cause it to obtain the image key |
| /// and then check the following: |
| /// |
| /// 1. If the returned [ImageStream] has been completed, end. This can happen |
| /// if the caller sets the completer on the stream. |
| /// 2. If the [ImageCache] has a completer for the key for this image, ask the |
| /// wrapped provider to resolve. |
| /// This can happen if the image was precached, or another [ImageProvider] |
| /// already resolved the same image. |
| /// 3. If the [context] has been disposed, end. This can happen if the caller |
| /// has been disposed and is no longer interested in resolving the image. |
| /// 4. If the widget is scrolling with high velocity at this point in time, |
| /// wait until the beginning of the next frame and go back to step 1. |
| /// 5. Delegate loading the image to the wrapped provider and finish. |
| /// |
| /// If the cycle ends at steps 1 or 3, the [ImageStream] will never be marked as |
| /// complete and listeners will not be notified. |
| /// |
| /// The [Image] widget wraps its incoming providers with this provider to avoid |
| /// overutilization of resources for images that would never appear on screen or |
| /// only be visible for a very brief period. |
| @optionalTypeArgs |
| class ScrollAwareImageProvider<T extends Object> extends ImageProvider<T> { |
| /// Creates a [ScrollAwareImageProvider]. |
| /// |
| /// The [context] object is the [BuildContext] of the [State] using this |
| /// provider. It is used to determine scrolling velocity during [resolve]. It |
| /// must not be null. |
| /// |
| /// The [imageProvider] is used to create a key and load the image. It must |
| /// not be null, and is assumed to interact with the cache in the normal way |
| /// that [ImageProvider.resolveStreamForKey] does. |
| const ScrollAwareImageProvider({ |
| required this.context, |
| required this.imageProvider, |
| }) : assert(context != null), |
| assert(imageProvider != null); |
| |
| /// The context that may or may not be enclosed by a [Scrollable]. |
| /// |
| /// Once [DisposableBuildContext.dispose] is called on this context, |
| /// the provider will stop trying to resolve the image if it has not already |
| /// been resolved. |
| final DisposableBuildContext context; |
| |
| /// The wrapped image provider to delegate [obtainKey] and [load] to. |
| final ImageProvider<T> imageProvider; |
| |
| @override |
| void resolveStreamForKey( |
| ImageConfiguration configuration, |
| ImageStream stream, |
| T key, |
| ImageErrorListener handleError, |
| ) { |
| // Something managed to complete the stream, or it's already in the image |
| // cache. Notify the wrapped provider and expect it to behave by not |
| // reloading the image since it's already resolved. |
| // Do this even if the context has gone out of the tree, since it will |
| // update LRU information about the cache. Even though we never showed the |
| // image, it was still touched more recently. |
| // Do this before checking scrolling, so that if the bytes are available we |
| // render them even though we're scrolling fast - there's no additional |
| // allocations to do for texture memory, it's already there. |
| if (stream.completer != null || PaintingBinding.instance.imageCache.containsKey(key)) { |
| imageProvider.resolveStreamForKey(configuration, stream, key, handleError); |
| return; |
| } |
| // The context has gone out of the tree - ignore it. |
| if (context.context == null) { |
| return; |
| } |
| // Something still wants this image, but check if the context is scrolling |
| // too fast before scheduling work that might never show on screen. |
| // Try to get to end of the frame callbacks of the next frame, and then |
| // check again. |
| if (Scrollable.recommendDeferredLoadingForContext(context.context!)) { |
| SchedulerBinding.instance.scheduleFrameCallback((_) { |
| scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError)); |
| }); |
| return; |
| } |
| // We are in the tree, we're not scrolling too fast, the cache doesn't |
| // have our image, and no one has otherwise completed the stream. Go. |
| imageProvider.resolveStreamForKey(configuration, stream, key, handleError); |
| } |
| |
| @override |
| ImageStreamCompleter load(T key, DecoderCallback decode) => imageProvider.load(key, decode); |
| |
| @override |
| ImageStreamCompleter loadBuffer(T key, DecoderBufferCallback decode) => imageProvider.loadBuffer(key, decode); |
| |
| @override |
| Future<T> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration); |
| } |