From 4894cfb0c517e0eb6c91d4fa3222c75807147fe6 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Fri, 10 Feb 2017 09:22:23 -0500 Subject: [PATCH] fix(ImageLoader): Adds prefetch cancellation and cache query (#996) Recent update to the UWP Community Toolkit allows us to cancel prefetch operations. This changeset also adds the ability to query for images on disk, but not yet in-memory. --- Libraries/Image/Image.windows.js | 86 ++++++++++++++++--- .../Modules/Image/ImageLoaderModule.cs | 15 +++- .../Modules/Image/ImageLoaderModule.cs | 41 ++++++++- 3 files changed, 126 insertions(+), 16 deletions(-) diff --git a/Libraries/Image/Image.windows.js b/Libraries/Image/Image.windows.js index 4247a8f2397..2c31964288d 100644 --- a/Libraries/Image/Image.windows.js +++ b/Libraries/Image/Image.windows.js @@ -11,11 +11,11 @@ */ 'use strict'; -const NativeMethodsMixin = require('NativeMethodsMixin'); +var NativeMethodsMixin = require('NativeMethodsMixin'); var NativeModules = require('NativeModules'); var ImageResizeMode = require('ImageResizeMode'); var ImageStylePropTypes = require('ImageStylePropTypes'); -var PropTypes = require('react/lib/ReactPropTypes'); +var ViewStylePropTypes = require('ViewStylePropTypes'); var React = require('React'); var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); var StyleSheet = require('StyleSheet'); @@ -26,11 +26,19 @@ var flattenStyle = require('flattenStyle'); var merge = require('merge'); var requireNativeComponent = require('requireNativeComponent'); var resolveAssetSource = require('resolveAssetSource'); +var Set = require('Set'); +var filterObject = require('fbjs/lib/filterObject'); +var PropTypes = React.PropTypes; var { ImageLoader, } = NativeModules; +let _requestId = 1; +function generateRequestId() { + return _requestId++; +} + /** * - A react component for displaying different types of images, * including network images, static resources, temporary local images, and @@ -63,6 +71,9 @@ var ImageViewAttributes = merge(ReactNativeViewAttributes.UIView, { shouldNotifyLoadEvents: true, }); +var ViewStyleKeys = new Set(Object.keys(ViewStylePropTypes)); +var ImageSpecificStyleKeys = new Set(Object.keys(ImageStylePropTypes).filter(x => !ViewStyleKeys.has(x))); + var Image = React.createClass({ propTypes: { ...View.propTypes, @@ -107,6 +118,10 @@ var Image = React.createClass({ * Invoked on load start */ onLoadStart: PropTypes.func, + /** + * Invoked on load error + */ + onError: PropTypes.func, /** * Invoked when load completes successfully */ @@ -119,6 +134,26 @@ var Image = React.createClass({ * Used to locate this view in end-to-end tests. */ testID: PropTypes.string, + /** + * Determines how to resize the image when the frame doesn't match the raw + * image dimensions. + * + * 'cover': Scale the image uniformly (maintain the image's aspect ratio) + * so that both dimensions (width and height) of the image will be equal + * to or larger than the corresponding dimension of the view (minus padding). + * + * 'contain': Scale the image uniformly (maintain the image's aspect ratio) + * so that both dimensions (width and height) of the image will be equal to + * or less than the corresponding dimension of the view (minus padding). + * + * 'stretch': Scale width and height independently, This may change the + * aspect ratio of the src. + * + * 'center': Scale the image down so that it is completely visible, + * if bigger than the area of the view. + * The image will not be scaled up. + */ + resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch', 'center']), }, statics: { @@ -142,9 +177,36 @@ var Image = React.createClass({ * Prefetches a remote image for later use by downloading it to the disk * cache */ - prefetch(url: string) { - return ImageLoader.prefetchImage(url); + prefetch(url: string, callback: ?Function) { + const requestId = generateRequestId(); + callback && callback(requestId); + return ImageLoader.prefetchImage(url, requestId); + }, + + /** + * Abort prefetch request + */ + abortPrefetch(requestId: number) { + ImageLoader.abortRequest(requestId); + }, + + /** + * Perform cache interrogation. + * + * @param urls the list of image URLs to check the cache for. + * @return a mapping from url to cache status, such as "disk" or "memory". If a requested URL is + * not in the mapping, it means it's not in the cache. + */ + async queryCache(urls: Array): Promise> { + return await ImageLoader.queryCache(urls); }, + + /** + * Resolves an asset reference into an object which has the properties `uri`, `width`, + * and `height`. The input may either be a number (opaque type returned by + * require('./foo.png')) or an `ImageSource` like { uri: '' } + */ + resolveAssetSource: resolveAssetSource, }, mixins: [NativeMethodsMixin], @@ -212,28 +274,31 @@ var Image = React.createClass({ sources = source; } - const {onLoadStart, onLoad, onLoadEnd} = this.props; + const {onLoadStart, onLoad, onLoadEnd, onError} = this.props; const nativeProps = merge(this.props, { style, - shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd), + shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd || onError), src: sources, loadingIndicatorSrc: loadingIndicatorSource ? loadingIndicatorSource.uri : null, }); if (nativeProps.children) { // TODO(6033040): Consider implementing this as a separate native component + const containerStyle = filterObject(style, (val, key) => !ImageSpecificStyleKeys.has(key)); + const imageStyle = filterObject(style, (val, key) => ImageSpecificStyleKeys.has(key)); const imageProps = merge(nativeProps, { - style: styles.absoluteImage, + style: [imageStyle, styles.absoluteImage], children: undefined, }); + return ( - + {this.props.children} ); } else { - return ; + return ; } } return null; @@ -257,9 +322,6 @@ var cfg = { nativeOnly: { src: true, loadingIndicatorSrc: true, - defaultImageSrc: true, - imageTag: true, - progressHandlerRegistered: true, shouldNotifyLoadEvents: true, }, }; diff --git a/ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs b/ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs index 44ecbf680f1..cfb9a2612b0 100644 --- a/ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs +++ b/ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs @@ -11,6 +11,7 @@ class ImageLoaderModule : NativeModuleBase private const string ErrorInvalidUri = "E_INVALID_URI"; private const string ErrorPrefetchFailure = "E_PREFETCH_FAILURE"; private const string ErrorGetSizeFailure = "E_GET_SIZE_FAILURE"; + private const string ErrorQueryCacheFailure = "E_QUERY_CACHE_FAILURE"; public override string Name { @@ -21,11 +22,17 @@ public override string Name } [ReactMethod] - public void prefetchImage(string uriString, IPromise promise) + public void prefetchImage(string uriString, int requestId, IPromise promise) { promise.Reject(ErrorPrefetchFailure, "Prefetch is not yet implemented."); } + [ReactMethod] + public void abortRequest(int requestId) + { + // No op as prefetch is not yet implemented. + } + [ReactMethod] public void getSize(string uriString, IPromise promise) { @@ -69,5 +76,11 @@ public void getSize(string uriString, IPromise promise) } }); } + + [ReactMethod] + public void queryCache(string[] urls, IPromise promise) + { + promise.Reject(ErrorQueryCacheFailure, "Prefetch is not yet implemented."); + } } } diff --git a/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs b/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs index f3f139aadf3..4f231f237f6 100644 --- a/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs +++ b/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs @@ -1,6 +1,7 @@ using Microsoft.Toolkit.Uwp.UI; using Newtonsoft.Json.Linq; using ReactNative.Bridge; +using ReactNative.Modules.Network; using System; using System.Reactive.Linq; using Windows.UI.Xaml.Media.Imaging; @@ -13,6 +14,8 @@ class ImageLoaderModule : NativeModuleBase private const string ErrorPrefetchFailure = "E_PREFETCH_FAILURE"; private const string ErrorGetSizeFailure = "E_GET_SIZE_FAILURE"; + private readonly TaskCancellationManager _prefetchRequests = new TaskCancellationManager(); + public override string Name { get @@ -22,7 +25,7 @@ public override string Name } [ReactMethod] - public void prefetchImage(string uriString, IPromise promise) + public void prefetchImage(string uriString, int requestId, IPromise promise) { if (string.IsNullOrEmpty(uriString)) { @@ -34,9 +37,13 @@ public void prefetchImage(string uriString, IPromise promise) { try { - // TODO: enable prefetch cancellation var uri = new Uri(uriString); - await ImageCache.Instance.PreCacheAsync(uri, true, true).ConfigureAwait(false); + + await _prefetchRequests.AddAndInvokeAsync( + requestId, + async token => await ImageCache.Instance.PreCacheAsync(uri, true, true, token).ConfigureAwait(false)) + .ConfigureAwait(false); + promise.Resolve(true); } catch (Exception ex) @@ -46,6 +53,12 @@ public void prefetchImage(string uriString, IPromise promise) }); } + [ReactMethod] + public void abortRequest(int requestId) + { + _prefetchRequests.Cancel(requestId); + } + [ReactMethod] public void getSize(string uriString, IPromise promise) { @@ -99,5 +112,27 @@ public void getSize(string uriString, IPromise promise) } }); } + + [ReactMethod] + public async void queryCache(string[] urls, IPromise promise) + { + // TODO: support query for items in memory + var result = new JObject(); + foreach (var url in urls) + { + var file = await ImageCache.Instance.GetFileFromCacheAsync(new Uri(url)).ConfigureAwait(false); + if (file != null) + { + result.Add(url, "disk"); + } + } + + promise.Resolve(result); + } + + public override void OnReactInstanceDispose() + { + _prefetchRequests.CancelAllTasks(); + } } }